Merge remote branch 'moodle/MOODLE_20_STABLE' into m20_MDL-18392_slash_cleanup

This commit is contained in:
Dan Marsden
2011-04-14 09:15:17 +12:00
94 changed files with 1569 additions and 666 deletions
+1 -1
View File
@@ -94,7 +94,7 @@ $oldversion = "$CFG->release ($CFG->version)";
$newversion = "$release ($version)";
// test environment first
if (!check_moodle_environment($version, $environment_results, false, ENV_SELECT_RELEASE)) {
if (!check_moodle_environment(normalize_version($release), $environment_results, false, ENV_SELECT_RELEASE)) {
$errors = environment_get_errors($environment_results);
cli_heading(get_string('environment', 'admin'));
foreach ($errors as $error) {
+1 -1
View File
@@ -152,7 +152,7 @@ if (!core_tables_exist()) {
echo $OUTPUT->box($releasenoteslink, 'generalbox releasenoteslink');
require_once($CFG->libdir.'/environmentlib.php');
if (!check_moodle_environment($release, $environment_results, true, ENV_SELECT_RELEASE)) {
if (!check_moodle_environment(normalize_version($release), $environment_results, true, ENV_SELECT_RELEASE)) {
print_upgrade_reload("index.php?agreelicense=1&lang=$CFG->lang");
} else {
echo $OUTPUT->notification(get_string('environmentok', 'admin'), 'notifysuccess');
+14 -5
View File
@@ -52,13 +52,22 @@ if (!during_initial_install()) { //do not use during installation
$temp->add(new admin_setting_configtext('coursesperpage', get_string('coursesperpage', 'admin'), get_string('configcoursesperpage', 'admin'), 20, PARAM_INT));
// front page default role
$roleoptions = array(0=>get_string('none')); // roles to choose from
if ($roles = get_all_roles()) {
foreach ($roles as $role) {
$roleoptions[$role->id] = strip_tags(format_string($role->name, true));
$options = array(0=>get_string('none')); // roles to choose from
$defaultfrontpageroleid = 0;
foreach (get_all_roles() as $role) {
if (empty($role->archetype) or $role->archetype === 'guest' or $role->archetype === 'frontpage' or $role->archetype === 'student') {
$options[$role->id] = strip_tags(format_string($role->name)) . ' ('. $role->shortname . ')';
if ($role->archetype === 'frontpage') {
$defaultfrontpageroleid = $role->id;
}
}
}
$temp->add(new admin_setting_configselect('defaultfrontpageroleid', get_string('frontpagedefaultrole', 'admin'), '', 0, $roleoptions));
if ($defaultfrontpageroleid and (!isset($CFG->defaultfrontpageroleid) or $CFG->defaultfrontpageroleid)) {
//frotpage role may not exist in old upgraded sites
unset($options[0]);
}
$temp->add(new admin_setting_configselect('defaultfrontpageroleid', get_string('frontpagedefaultrole', 'admin'), '', $defaultfrontpageroleid, $options));
$ADMIN->add('frontpage', $temp);
+25 -27
View File
@@ -38,17 +38,18 @@ if ($hassiteconfig
if ($ADMIN->fulltree) {
if (!during_initial_install()) {
$context = get_context_instance(CONTEXT_SYSTEM);
$allroles = array();
$generalroles = array();
$otherroles = array();
$guestroles = array();
$userroles = array();
$studentroles = array();
$teacherroles = array();
$creatornewroles = array();
$defaultteacherid = null;
$defaultuserid = null;
$defaultguestid = null;
foreach (get_all_roles() as $role) {
$rolename = strip_tags(format_string($role->name)) . ' ('. $role->shortname . ')';
$allroles[$role->id] = $rolename;
switch ($role->archetype) {
case 'manager':
$creatornewroles[$role->id] = $rolename;
@@ -56,58 +57,55 @@ if ($hassiteconfig
case 'coursecreator':
break;
case 'editingteacher':
$teacherroles[$role->id] = $rolename;
$defaultteacherid = isset($defaultteacherid) ? $defaultteacherid : $role->id;
$creatornewroles[$role->id] = $rolename;
break;
case 'teacher':
$creatornewroles[$role->id] = $rolename;
break;
case 'student':
$studentroles[$role->id] = $rolename;
break;
case 'guest':
$defaultguestid = isset($defaultguestid) ? $defaultguestid : $role->id;
$guestroles[$role->id] = $rolename;
break;
case 'user':
$defaultuserid = isset($defaultuserid) ? $defaultuserid : $role->id;
$userroles[$role->id] = $rolename;
break;
case 'frontpage':
break;
default:
$creatornewroles[$role->id] = $rolename;
$generalroles[$role->id] = $rolename;
$otherroles[$role->id] = $rolename;
break;
}
}
reset($guestroles);
$defaultguestid = key($guestroles);
reset($studentroles);
$defaultstudentid = key($studentroles);
reset($teacherroles);
$defaultteacherid = key($teacherroles);
if (empty($guestroles)) {
$guestroles[0] = get_string('none');
$defaultguestid = 0;
}
if ($userroles) {
reset($userroles);
$defaultuserid = key($userroles);
} else {
$userroles = array('0'=>get_string('none'));
if (empty($userroles)) {
$userroles[0] = get_string('none');
$defaultuserid = 0;
}
$temp->add(new admin_setting_configselect('notloggedinroleid', get_string('notloggedinroleid', 'admin'),
get_string('confignotloggedinroleid', 'admin'), $defaultguestid, ($guestroles + $generalroles)));
get_string('confignotloggedinroleid', 'admin'), $defaultguestid, ($guestroles + $otherroles)));
$temp->add(new admin_setting_configselect('guestroleid', get_string('guestroleid', 'admin'),
get_string('guestroleid_help', 'admin'), $defaultguestid, ($guestroles + $generalroles)));
get_string('guestroleid_help', 'admin'), $defaultguestid, ($guestroles + $otherroles)));
$temp->add(new admin_setting_configselect('defaultuserroleid', get_string('defaultuserroleid', 'admin'),
get_string('configdefaultuserroleid', 'admin'), $defaultuserid, ($userroles + $generalroles)));
}
$temp->add(new admin_setting_configcheckbox('nodefaultuserrolelists', get_string('nodefaultuserrolelists', 'admin'), get_string('confignodefaultuserrolelists', 'admin'), 0));
if (!during_initial_install()) {
get_string('configdefaultuserroleid', 'admin'), $defaultuserid, ($userroles + $otherroles)));
$temp->add(new admin_setting_configselect('creatornewroleid', get_string('creatornewroleid', 'admin'),
get_string('creatornewroleid_help', 'admin'), $defaultteacherid, $creatornewroles));
// release memory
unset($otherroles);
unset($guestroles);
unset($userroles);
unset($creatornewroles);
}
$temp->add(new admin_setting_configcheckbox('autologinguests', get_string('autologinguests', 'admin'), get_string('configautologinguests', 'admin'), 0));
+1 -1
View File
@@ -774,7 +774,7 @@ if ($formdata = $mform2->is_cancelled()) {
}
}
$manual->enrol_user($manualcache[$courseid], $user->id, $rid, $today, $timeend, true);
$manual->enrol_user($manualcache[$courseid], $user->id, $rid, $today, $timeend);
$a = new stdClass();
$a->course = $shortname;
+1 -13
View File
@@ -130,19 +130,7 @@ class backup_ui extends base_ui {
}
return $backupid;
}
/**
* Cancels the current backup and redirects the user back to the relevant place
*/
public function cancel_backup() {
global $PAGE;
// Determine the approriate URL to redirect the user to
if ($PAGE->context->contextlevel == CONTEXT_MODULE && $PAGE->cm !== null) {
$relevanturl = new moodle_url('/mod/'.$PAGE->cm->modname.'/view.php', array('id'=>$PAGE->cm->id));
} else {
$relevanturl = new moodle_url('/course/view.php', array('id'=>$PAGE->course->id));
}
redirect($relevanturl);
}
/**
* Gets an array of progress bar items that can be displayed through the backup renderer.
* @return array Array of items for the progress bar
+3 -3
View File
@@ -81,7 +81,7 @@ class backup_ui_stage_initial extends backup_ui_stage {
$form = $this->initialise_stage_form();
if ($form->is_cancelled()) {
$this->ui->cancel_backup();
$this->ui->cancel_process();
}
$data = $form->get_data();
@@ -180,7 +180,7 @@ class backup_ui_stage_schema extends backup_ui_stage {
$form = $this->initialise_stage_form();
// Check it wasn't cancelled
if ($form->is_cancelled()) {
$this->ui->cancel_backup();
$this->ui->cancel_process();
}
// Check it has been submit
@@ -288,7 +288,7 @@ class backup_ui_stage_confirmation extends backup_ui_stage {
$form = $this->initialise_stage_form();
// Check it hasn't been cancelled
if ($form->is_cancelled()) {
$this->ui->cancel_backup();
$this->ui->cancel_process();
}
$data = $form->get_data();
+15
View File
@@ -226,6 +226,21 @@ abstract class base_ui {
public static function load_controller($uniqueid=false) {
throw new coding_exception('load_controller() method needs to be overridden in each subclass of base_ui');
}
/**
* Cancels the current backup/restore and redirects the user back to the relevant place
*/
public function cancel_process() {
global $PAGE;
// Determine the appropriate URL to redirect the user to
if ($PAGE->context->contextlevel == CONTEXT_MODULE && $PAGE->cm !== null) {
$relevanturl = new moodle_url('/mod/'.$PAGE->cm->modname.'/view.php', array('id'=>$PAGE->cm->id));
} else {
$relevanturl = new moodle_url('/course/view.php', array('id'=>$PAGE->course->id));
}
redirect($relevanturl);
}
/**
* Gets an array of progress bar items that can be displayed through the backup renderer.
* @return array Array of items for the progress bar
+2 -2
View File
@@ -392,7 +392,7 @@ class restore_ui_stage_schema extends restore_ui_stage {
$form = $this->initialise_stage_form();
// Check it wasn't cancelled
if ($form->is_cancelled()) {
$this->ui->cancel_backup();
$this->ui->cancel_process();
}
// Check it has been submit
@@ -500,7 +500,7 @@ class restore_ui_stage_review extends restore_ui_stage {
$form = $this->initialise_stage_form();
// Check it hasn't been cancelled
if ($form->is_cancelled()) {
$this->ui->cancel_backup();
$this->ui->cancel_process();
}
$data = $form->get_data();
+3
View File
@@ -47,6 +47,9 @@ class block_comments extends block_base {
$args->env = 'block_comments';
$args->component = 'block_comments';
$args->linktext = get_string('showcomments');
$args->notoggle = true;
$args->autostart = true;
$args->displaycancel = true;
$comment = new comment($args);
$comment->set_view_permission(true);
+3 -2
View File
@@ -41,8 +41,9 @@ M.core_comment = {
this.courseid = args.courseid;
this.contextid = args.contextid;
this.env = args.env;
this.autostart = (args.autostart);
// expand comments?
if (args.autostart) {
if (this.autostart) {
this.view(args.page);
}
// load comments
@@ -347,7 +348,7 @@ bodyContent: '<div class="comment-delete-confirm"><a href="#" id="confirmdelete-
var d = container.getStyle('display');
if (d=='none'||d=='') {
// show
if (this.env != 'block_comments') {
if (this.autostart) {
this.load(page);
} else {
this.register_delete_buttons();
+163 -77
View File
@@ -80,6 +80,26 @@ class comment {
*/
private $linktext;
/**
* If set to true then comment sections won't be able to be opened and closed
* instead they will always be visible.
* @var bool
*/
protected $notoggle = false;
/**
* If set to true comments are automatically loaded as soon as the page loads.
* Normally this happens when the user expands the comment section.
* @var bool
*/
protected $autostart = false;
/**
* If set to true a cancel button will be shown on the form used to submit comments.
* @var bool
*/
protected $displaycancel = false;
// static variable will be used by non-js comments UI
private static $nonjs = false;
private static $comment_itemid = null;
@@ -87,6 +107,7 @@ class comment {
private static $comment_area = null;
private static $comment_page = null;
private static $comment_component = null;
/**
* Construct function of comment class, initialise
* class members
@@ -173,6 +194,21 @@ class comment {
$this->ignore_permission = false;
}
// setup notoggle
if (!empty($options->notoggle)) {
$this->set_notoggle($options->notoggle);
}
// setup notoggle
if (!empty($options->autostart)) {
$this->set_autostart($options->autostart);
}
// setup displaycancel
if (!empty($options->displaycancel)) {
$this->set_displaycancel($options->displaycancel);
}
if (!empty($options->showcount)) {
$count = $this->count();
if (empty($count)) {
@@ -264,6 +300,90 @@ EOD;
}
}
/**
* Gets a link for this page that will work with JS disabled.
*
* @global moodle_page $PAGE
* @param moodle_page $page
* @return moodle_url
*/
public function get_nojslink(moodle_page $page = null) {
if ($page === null) {
global $PAGE;
$page = $PAGE;
}
$link = new moodle_url($page->url, array(
'nonjscomment' => true,
'comment_itemid' => $this->itemid,
'comment_context' => $this->context->id,
'comment_area' => $this->commentarea,
));
$link->remove_params(array('nonjscomment', 'comment_page'));
return $link;
}
/**
* Sets the value of the notoggle option.
*
* If set to true then the user will not be able to expand and collase
* the comment section.
*
* @param bool $newvalue
*/
public function set_notoggle($newvalue = true) {
$this->notoggle = (bool)$newvalue;
}
/**
* Sets the value of the autostart option.
*
* If set to true then the comments will be loaded during page load.
* Normally this happens only once the user expands the comment section.
*
* @param bool $newvalue
*/
public function set_autostart($newvalue = true) {
$this->autostart = (bool)$newvalue;
}
/**
* Sets the displaycancel option
*
* If set to true then a cancel button will be shown when using the form
* to post comments.
*
* @param bool $newvalue
*/
public function set_displaycancel($newvalue = true) {
$this->displaycancel = (bool)$newvalue;
}
/**
* Initialises the JavaScript that enchances the comment API.
*
* @param moodle_page $page The moodle page object that the JavaScript should be
* initialised for.
*/
public function initialise_javascript(moodle_page $page) {
$options = new stdClass;
$options->client_id = $this->cid;
$options->commentarea = $this->commentarea;
$options->itemid = $this->itemid;
$options->page = 0;
$options->courseid = $this->courseid;
$options->contextid = $this->contextid;
$options->env = $this->env;
$options->component = $this->component;
$options->notoggle = $this->notoggle;
$options->autostart = $this->autostart;
$page->requires->js_init_call('M.core_comment.init', array($options), true);
return true;
}
/**
* Prepare comment code in html
* @param boolean $return
@@ -271,108 +391,74 @@ EOD;
*/
public function output($return = true) {
global $PAGE, $OUTPUT;
static $template_printed;
static $template_printed;
$this->link = $PAGE->url;
$murl = new moodle_url($this->link);
$murl->remove_params('nonjscomment');
$murl->param('nonjscomment', 'true');
$murl->param('comment_itemid', $this->itemid);
$murl->param('comment_context', $this->context->id);
$murl->param('comment_area', $this->commentarea);
$murl->remove_params('comment_page');
$this->link = $murl->out();
$options = new stdClass();
$options->client_id = $this->cid;
$options->commentarea = $this->commentarea;
$options->itemid = $this->itemid;
$options->page = 0;
$options->courseid = $this->courseid;
$options->contextid = $this->contextid;
$options->env = $this->env;
$options->component = $this->component;
if ($this->env == 'block_comments') {
$options->notoggle = true;
$options->autostart = true;
}
$PAGE->requires->js_init_call('M.core_comment.init', array($options), true);
$this->initialise_javascript($PAGE);
if (!empty(self::$nonjs)) {
// return non js comments interface
return $this->print_comments(self::$comment_page, $return, true);
}
$strsubmit = get_string('savecomment');
$strcancel = get_string('cancel');
$strshowcomments = get_string('showcommentsnonjs');
$sesskey = sesskey();
$html = '';
// print html template
// Javascript will use the template to render new comments
if (empty($template_printed) && !empty($this->viewcap)) {
$html .= '<div style="display:none" id="cmt-tmpl">' . $this->template . '</div>';
$html .= html_writer::tag('div', $this->template, array('style' => 'display:none', 'id' => 'cmt-tmpl'));
$template_printed = true;
}
if (!empty($this->viewcap)) {
// print commenting icon and tooltip
$icon = $OUTPUT->pix_url('t/collapsed');
$html .= <<<EOD
<div class="mdl-left">
<a class="showcommentsnonjs" href="{$this->link}">{$strshowcomments}</a>
EOD;
if ($this->env != 'block_comments') {
$html .= <<<EOD
<a id="comment-link-{$this->cid}" class="comment-link" href="#">
<img id="comment-img-{$this->cid}" src="$icon" alt="{$this->linktext}" title="{$this->linktext}" />
<span id="comment-link-text-{$this->cid}">{$this->linktext} {$this->count}</span>
</a>
EOD;
$html .= html_writer::start_tag('div', array('class' => 'mdl-left'));
$html .= html_writer::link($this->get_nojslink($PAGE), get_string('showcommentsnonjs'), array('class' => 'showcommentsnonjs'));
if (!$this->notoggle) {
// If toggling is enabled (notoggle=false) then print the controls to toggle
// comments open and closed
$html .= html_writer::start_tag('a', array('class' => 'comment-link', 'id' => 'comment-link-'.$this->cid, 'href' => '#'));
$html .= html_writer::empty_tag('img', array('id' => 'comment-img-'.$this->cid, 'src' => $OUTPUT->pix_url('t/collapsed'), 'alt' => $this->linktext, 'title' => $this->linktext));
$html .= html_writer::tag('span', $this->linktext.' '.$this->count, array('id' => 'comment-link-text-'.$this->cid));
$html .= html_writer::end_tag('a');
}
$html .= <<<EOD
<div id="comment-ctrl-{$this->cid}" class="comment-ctrl">
<ul id="comment-list-{$this->cid}" class="comment-list">
<li class="first"></li>
EOD;
// in comments block, we print comments list right away
if ($this->env == 'block_comments') {
$html .= html_writer::start_tag('div', array('id' => 'comment-ctrl-'.$this->cid, 'class' => 'comment-ctrl'));
$html .= html_writer::start_tag('ul', array('id' => 'comment-list-'.$this->cid, 'class' => 'comment-list'));
$html .= html_writer::tag('li', '', array('class' => 'first'));
if ($this->autostart) {
// If autostart has been enabled print the comments list immediatly
$html .= $this->print_comments(0, true, false);
$html .= '</ul>';
$html .= html_writer::end_tag('ul'); // .comment-list
$html .= $this->get_pagination(0);
} else {
$html .= <<<EOD
</ul>
<div id="comment-pagination-{$this->cid}" class="comment-pagination"></div>
EOD;
$html .= html_writer::end_tag('ul'); // .comment-list
$html .= html_writer::tag('div', '', array('id' => 'comment-pagination-'.$this->cid, 'class' => 'comment-pagination'));
}
// print posting textarea
if (!empty($this->postcap)) {
$html .= <<<EOD
<div class='comment-area'>
<div class="bd">
<textarea name="content" rows="2" cols="20" id="dlg-content-{$this->cid}"></textarea>
</div>
<div class="fd" id="comment-action-{$this->cid}">
<a href="#" id="comment-action-post-{$this->cid}"> {$strsubmit} </a>
EOD;
if ($this->env != 'block_comments') {
$html .= "<span> | </span><a href=\"#\" id=\"comment-action-cancel-{$this->cid}\"> {$strcancel} </a>";
// print posting textarea
$html .= html_writer::start_tag('div', array('class' => 'comment-area'));
$html .= html_writer::start_tag('div', array('class' => 'db'));
$html .= html_writer::tag('textarea', '', array('name' => 'content', 'rows' => 2, 'cols' => 20, 'id' => 'dlg-content-'.$this->cid));
$html .= html_writer::end_tag('div'); // .db
$html .= html_writer::start_tag('div', array('class' => 'fd', 'id' => 'comment-action-'.$this->cid));
$html .= html_writer::link('#', get_string('savecomment'), array('id' => 'comment-action-post-'.$this->cid));
if ($this->displaycancel) {
$html .= html_writer::tag('span', ' | ');
$html .= html_writer::link('#', get_string('cancel'), array('id' => 'comment-action-cancel-'.$this->cid));
}
$html .= <<<EOD
</div>
</div>
<div class="clearer"></div>
EOD;
$html .= html_writer::end_tag('div'); // .fd
$html .= html_writer::end_tag('div'); // .comment-area
$html .= html_writer::tag('div', '', array('class' => 'clearer'));
}
$html .= <<<EOD
</div><!-- end of comment-ctrl -->
</div>
EOD;
$html .= html_writer::end_tag('div'); // .comment-ctrl
$html .= html_writer::end_tag('div'); // .mdl-left
} else {
$html = '';
}
@@ -457,11 +543,11 @@ EOD;
$count = $this->count();
$pages = (int)ceil($count/$CFG->commentsperpage);
if ($pages == 1 || $pages == 0) {
return '';
return html_writer::tag('div', '', array('id' => 'comment-pagination-'.$this->cid, 'class' => 'comment-pagination'));
}
if (!empty(self::$nonjs)) {
// used in non-js interface
return $OUTPUT->paging_bar($count, $page, $CFG->commentsperpage, $this->link, 'comment_page');
return $OUTPUT->paging_bar($count, $page, $CFG->commentsperpage, $this->get_nojslink(), 'comment_page');
} else {
// return ajax paging bar
$str = '';
+6
View File
@@ -154,6 +154,8 @@ switch ($action) {
$roleid = optional_param('role', null, PARAM_INT);
$duration = optional_param('duration', 0, PARAM_INT);
$startdate = optional_param('startdate', 0, PARAM_INT);
$recovergrades = optional_param('recovergrades', 0, PARAM_INT);
if (empty($roleid)) {
$roleid = null;
}
@@ -185,6 +187,10 @@ switch ($action) {
$plugin = $plugins[$instance->enrol];
if ($plugin->allow_enrol($instance) && has_capability('enrol/'.$plugin->get_name().':enrol', $context)) {
$plugin->enrol_user($instance, $user->id, $roleid, $timestart, $timeend);
if ($recovergrades) {
require_once($CFG->libdir.'/gradelib.php');
grade_recover_history_grades($user->id, $instance->courseid);
}
} else {
throw new enrol_ajax_exception('enrolnotpermitted');
}
+2
View File
@@ -613,6 +613,8 @@ class course_enrolment_manager {
* @return bool
*/
public function edit_enrolment($userenrolment, $data) {
//Only allow editing if the user has the appropriate capability
//Already checked in /enrol/users.php but checking again in case this function is called from elsewhere
list($instance, $plugin) = $this->get_user_enrolment_components($userenrolment);
if ($instance && $plugin && $plugin->allow_manage($instance) && has_capability("enrol/$instance->enrol:manage", $this->context)) {
if (!isset($data->status)) {
+6 -2
View File
@@ -626,6 +626,7 @@ class course_enrolment_users_table extends course_enrolment_table {
* @return single_button|url_select
*/
public function get_enrolment_selector() {
global $CFG;
static $count = 0;
$instances = $this->manager->get_enrolment_instances();
@@ -659,6 +660,7 @@ class course_enrolment_users_table extends course_enrolment_table {
$control->class = 'singlebutton enrolusersbutton instance'.$count;
$control->formid = 'manuallyenrol_select_'+$count;
}
$course = $this->manager->get_course();
$timeformat = get_string('strftimedatefullshort');
$today = time();
@@ -686,7 +688,8 @@ class course_enrolment_users_table extends course_enrolment_table {
'startdatetoday',
'durationdays',
'enrolperiod',
'finishenrollingusers'), 'enrol');
'finishenrollingusers',
'recovergrades'), 'enrol');
$this->moodlepage->requires->string_for_js('assignroles', 'role');
$this->moodlepage->requires->string_for_js('startingfrom', 'moodle');
@@ -698,7 +701,8 @@ class course_enrolment_users_table extends course_enrolment_table {
'ajaxurl'=>'/enrol/ajax.php',
'url'=>$this->moodlepage->url->out(false),
'optionsStartDate'=>$startdateoptions,
'defaultRole'=>$instance->roleid);
'defaultRole'=>$instance->roleid,
'disableGradeHistory'=>$CFG->disablegradehistory);
$this->moodlepage->requires->yui_module($modules, $function, array($arguments));
}
return $control;
+3 -1
View File
@@ -62,7 +62,7 @@ if ($action) {
switch ($action) {
/**
* Unenrols a user from this course
* Unenrols a user from this course (including removing all of their grades)
*/
case 'unenrol':
$ue = $DB->get_record('user_enrolments', array('id'=>required_param('ue', PARAM_INT)), '*', MUST_EXIST);
@@ -166,6 +166,8 @@ if ($action) {
*/
case 'edit':
$ue = $DB->get_record('user_enrolments', array('id'=>required_param('ue', PARAM_INT)), '*', MUST_EXIST);
//Only show the edit form if the user has the appropriate capability
list($instance, $plugin) = $manager->get_user_enrolment_components($ue);
if ($instance && $plugin && $plugin->allow_manage($instance) && has_capability("enrol/$instance->enrol:manage", $manager->get_context())) {
$user = $DB->get_record('user', array('id'=>$ue->userid), '*', MUST_EXIST);
+15 -1
View File
@@ -20,7 +20,8 @@ YUI.add('moodle-enrol-enrolmentmanager', function(Y) {
DEFAULTROLE : 'defaultRole',
DEFAULTSTARTDATE : 'defaultStartDate',
DEFAULTDURATION : 'defaultDuration',
ASSIGNABLEROLES : 'assignableRoles'
ASSIGNABLEROLES : 'assignableRoles',
DISABLEGRADEHISTORY : 'disableGradeHistory'
};
/** CSS classes for nodes in structure **/
var CSS = {
@@ -48,6 +49,8 @@ YUI.add('moodle-enrol-enrolmentmanager', function(Y) {
ODD : 'odd',
EVEN : 'even',
HIDDEN : 'hidden',
RECOVERGRADESCHECK : 'uep-recovergradescheck',
RECOVERGRADESCHECKTITLE : 'uep-recovergradeschecktitle',
SEARCHOPTIONS : 'uep-searchoptions',
COLLAPSIBLEHEADING : 'collapsibleheading',
COLLAPSIBLEAREA : 'collapsiblearea',
@@ -70,6 +73,11 @@ YUI.add('moodle-enrol-enrolmentmanager', function(Y) {
_loadingNode : null,
_escCloseEvent : null,
initializer : function(config) {
recovergradescheckbox = null;
if (this.get(UEP.DISABLEGRADEHISTORY) != true) {
recovergradescheckbox = Y.Node.create('<div class="'+CSS.RECOVERGRADESCHECK+'"><input type="checkbox" id="recovergrades" name="recovergrades" /><span class="'+CSS.RECOVERGRADESCHECKTITLE+'"><label for="recovergrades">'+M.str.enrol.recovergrades+'</label></span></div>');
}
this.set(UEP.BASE, Y.Node.create('<div class="'+CSS.PANEL+' '+CSS.HIDDEN+'"></div>')
.append(Y.Node.create('<div class="'+CSS.WRAP+'"></div>')
.append(Y.Node.create('<div class="'+CSS.HEADER+' header"></div>')
@@ -80,6 +88,7 @@ YUI.add('moodle-enrol-enrolmentmanager', function(Y) {
.append(Y.Node.create('<div class="'+CSS.SEARCHOPTION+' '+CSS.ROLE+'">'+M.str.role.assignroles+'</div>')
.append(Y.Node.create('<select><option value="">'+M.str.enrol.none+'</option></select>'))
)
.append(recovergradescheckbox)
.append(Y.Node.create('<div class="'+CSS.SEARCHOPTIONS+'"></div>')
.append(Y.Node.create('<div class="'+CSS.COLLAPSIBLEHEADING+'"><img alt="" />'+M.str.enrol.enrolmentoptions+'</div>'))
.append(Y.Node.create('<div class="'+CSS.COLLAPSIBLEAREA+' '+CSS.HIDDEN+'"></div>')
@@ -367,6 +376,8 @@ YUI.add('moodle-enrol-enrolmentmanager', function(Y) {
params['role'] = this.get(UEP.BASE).one('.'+CSS.SEARCHOPTION+'.'+CSS.ROLE+' select').get('value');
params['startdate'] = this.get(UEP.BASE).one('.'+CSS.SEARCHOPTION+'.'+CSS.STARTDATE+' select').get('value');
params['duration'] = this.get(UEP.BASE).one('.'+CSS.SEARCHOPTION+'.'+CSS.DURATION+' select').get('value');
params['recovergrades'] = this.get(UEP.BASE).one('#recovergrades').get('checked')?1:0;
Y.io(M.cfg.wwwroot+this.get(UEP.AJAXURL), {
method:'POST',
data:build_querystring(params),
@@ -487,6 +498,9 @@ YUI.add('moodle-enrol-enrolmentmanager', function(Y) {
},
optionsStartDate : {
value : []
},
disableGradeHistory : {
value : 0
}
}
});
+1 -3
View File
@@ -183,7 +183,7 @@ $string['configdefaultallowedmodules'] = 'For the courses which fall into the ab
$string['configdefaulthomepage'] = 'This determines the home page for logged in users';
$string['configdefaultrequestcategory'] = 'Courses requested by users will be automatically placed in this category.';
$string['configdefaultrequestedcategory'] = 'Default category to put courses that were requested into, if they\'re approved.';
$string['configdefaultuserroleid'] = 'All logged in users will be given the capabilities of the role you specify here, at the site level, in ADDITION to any other roles they may have been given. The default is the Authenticated user role (or Guest role in older versions). Note that this will not conflict with other roles they have, it just ensures that all users have capabilities that are not assignable at the course level (eg post blog entries, manage own calendar, etc).';
$string['configdefaultuserroleid'] = 'All logged in users will be given the capabilities of the role you specify here, at the site level, in ADDITION to any other roles they may have been given. The default is the Authenticated user role. Note that this will not conflict with other roles they have unless you prohibit capabilities, it just ensures that all users have capabilities that are not assignable at the course level (eg post blog entries, manage own calendar, etc).';
$string['configdeleteincompleteusers'] = 'After this period, old not fully setup accounts are deleted.';
$string['configdeleteunconfirmed'] = 'If you are using email authentication, this is the period within which a response will be accepted from users. After this period, old unconfirmed accounts are deleted.';
$string['configdenyemailaddresses'] = 'To deny email addresses from particular domains list them here in the same way. All other domains will be accepted. To deny subdomains add the domain with a preceding \'.\'. eg <strong>hotmail.com yahoo.co.uk .live.com</strong>';
@@ -276,7 +276,6 @@ $string['configmypagelocked'] = 'This setting prevents the default page from bei
$string['confignavcourselimit'] = 'Limits the number of courses shown to the user when they are either not logged in or are not enrolled in any courses.';
$string['confignavshowallcourses'] = 'Setting this ensures that all courses on the site are shown in the navigation at all times.';
$string['confignavshowcategories'] = 'Show course categories in the navigation bar and navigation blocks. This does not occur with courses the user is currently enrolled in, they will still be listed under mycourses without categories.';
$string['confignodefaultuserrolelists'] = 'This setting prevents all users from being returned from the database from deprecated calls of get_course_user, etc., for the site course if the default role provides that access. Check this, if you suffer a performance hit.';
$string['confignoreplyaddress'] = 'Emails are sometimes sent out on behalf of a user (eg forum posts). The email address you specify here will be used as the "From" address in those cases when the recipients should not be able to reply directly to the user (eg when a user chooses to keep their address private).';
$string['confignotifyloginfailures'] = 'If login failures have been recorded, email notifications can be sent out. Who should see these notifications?';
$string['confignotifyloginthreshold'] = 'If notifications about failed logins are active, how many failed login attempts by one user or one IP address is it worth notifying about?';
@@ -747,7 +746,6 @@ $string['navshowcategories'] = 'Show course categories';
$string['neverdeleteruns'] = 'Never delete runs';
$string['nobookmarksforuser'] = 'You do not have any bookmarks.';
$string['nodatabase'] = 'No database';
$string['nodefaultuserrolelists'] = 'Don\'t return all default role users';
$string['nochanges'] = 'No changes';
$string['nolangupdateneeded'] = 'All your language packs are up to date, no update is needed';
$string['nomissingstrings'] = 'No missing strings';
+1
View File
@@ -77,6 +77,7 @@ $string['participationsuspended'] = 'Suspended';
$string['periodend'] = 'until {$a}';
$string['periodstart'] = 'from {$a}';
$string['periodstartend'] = 'from {$a->start} until {$a->end}';
$string['recovergrades'] = 'Recover user\'s old grades if possible';
$string['rolefromthiscourse'] = '{$a->role} (Assigned in this course)';
$string['rolefrommetacourse'] = '{$a->role} (Inherited from parent course)';
$string['rolefromcategory'] = '{$a->role} (Inherited from course category)';
+1
View File
@@ -123,6 +123,7 @@ $string['logout'] = 'Logout';
$string['manage'] = 'Manage repositories';
$string['manageurl'] = 'Manage';
$string['manageuserrepository'] = 'Manage individual repository';
$string['moving'] = 'Moving';
$string['noenter'] = 'Nothing entered';
$string['nofilesattached'] = 'No files attached';
$string['nofilesavailable'] = 'No files available';
+4 -4
View File
@@ -3142,8 +3142,8 @@ function get_enrolled_sql($context, $withcapability = '', $groupid = 0, $onlyact
}
}
$defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : null;
$defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : null;
$defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
$defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
$nobody = false;
@@ -4851,8 +4851,8 @@ function get_users_by_capability($context, $capability, $fields = '', $sort = ''
throw new coding_exception('Invalid context specified');
}
$defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : null;
$defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : null;
$defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
$defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
$ctxids = trim($context->path, '/');
$ctxids = str_replace('/', ',', $ctxids);
+10
View File
@@ -592,6 +592,9 @@ class completion_info {
* Should be called whenever a module is 'viewed' (it is up to the module how to
* determine that). Has no effect if viewing is not set as a completion condition.
*
* Note that this function must be called before you print the page header because
* it is possible that the navigation block may depend on it. If you call it after
* printing the header, it shows a developer debug warning.
* @uses COMPLETION_VIEW_NOT_REQUIRED
* @uses COMPLETION_VIEWED
* @uses COMPLETION_COMPLETE
@@ -600,6 +603,11 @@ class completion_info {
* @return void
*/
public function set_module_viewed($cm, $userid=0) {
global $PAGE;
if ($PAGE->headerprinted) {
debugging('set_module_viewed must be called before header is printed',
DEBUG_DEVELOPER);
}
// Don't do anything if view condition is not turned on
if ($cm->completionview == COMPLETION_VIEW_NOT_REQUIRED || !$this->is_enabled($cm)) {
return;
@@ -932,6 +940,8 @@ class completion_info {
if ($data->userid == $USER->id) {
$SESSION->completioncache[$cm->course][$cm->id] = $data;
$reset = 'reset';
get_fast_modinfo($reset);
}
}
+2 -2
View File
@@ -636,7 +636,7 @@ M.form_filemanager.init = function(Y, options) {
dialog = Y.one('#fm-move-dlg');
}
dialog.set('innerHTML', '<div class="hd">Moving</div><div class="bd"><div id="fm-move-div">'+M.str.repository.nopathselected+'</div><div id="fm-tree"></div></div>');
dialog.set('innerHTML', '<div class="hd">'+M.str.repository.moving+'</div><div class="bd"><div id="fm-move-div">'+M.str.repository.nopathselected+'</div><div id="fm-tree"></div></div>');
this.movefile_dialog = new YAHOO.widget.Dialog("fm-move-dlg", {
width : "600px",
@@ -717,7 +717,7 @@ M.form_filemanager.init = function(Y, options) {
var rootNode = treeview.getRoot();
treeview.setDynamicLoad(loadDataForNode);
treeview.removeChildren(rootNode);
var textnode = {label: "Files", path: '/'};
var textnode = {label: M.str.moodle.files, path: '/'};
var tmpNode = new YAHOO.widget.TextNode(textnode, rootNode, true);
treeview.draw();
}, this, true);
+6 -5
View File
@@ -246,7 +246,7 @@ function form_filemanager_render($options) {
$html = '';
$options = $fm->options;
$straddfile = get_string('add', 'repository') . '...';
$straddfile = get_string('addfile', 'repository');
$strmakedir = get_string('makeafolder', 'moodle');
$strdownload = get_string('downloadfolder', 'repository');
$strloading = get_string('loading', 'repository');
@@ -280,9 +280,9 @@ $icon_progress
<div id="filemanager-wrapper-{$client_id}" style="display:none">
<div class="fm-breadcrumb" id="fm-path-{$client_id}"></div>
<div class="filemanager-toolbar">
<input type="button" class="fm-btn-add" id="btnadd-{$client_id}" onclick="return false" value=" {$straddfile}" />
<input type="button" class="fm-btn-mkdir" id="btncrt-{$client_id}" onclick="return false" value=" $strmakedir" />
<input type="button" class="fm-btn-download" id="btndwn-{$client_id}" onclick="return false" {$extra} value=" $strdownload" />
<input type="button" class="fm-btn-add" id="btnadd-{$client_id}" onclick="return false" value="{$straddfile}" />
<input type="button" class="fm-btn-mkdir" id="btncrt-{$client_id}" onclick="return false" value="{$strmakedir}" />
<input type="button" class="fm-btn-download" id="btndwn-{$client_id}" onclick="return false" {$extra} value="{$strdownload}" />
<span> $maxsize </span>
</div>
<div class="filemanager-container" id="filemanager-{$client_id}">
@@ -311,7 +311,8 @@ FMHTML;
array('zip', 'editor'), array('unzip', 'moodle'), array('rename', 'moodle'), array('delete', 'moodle'),
array('cannotdeletefile', 'error'), array('confirmdeletefile', 'repository'),
array('nopathselected', 'repository'), array('popupblockeddownload', 'repository'),
array('draftareanofiles', 'repository'), array('path', 'moodle'), array('setmainfile', 'repository')
array('draftareanofiles', 'repository'), array('path', 'moodle'), array('setmainfile', 'repository'),
array('moving', 'repository'), array('files', 'moodle')
)
);
$PAGE->requires->js_module($module);
+84 -8
View File
@@ -913,6 +913,84 @@ function grade_force_site_regrading() {
$DB->set_field('grade_items', 'needsupdate', 1);
}
/**
* Recover a user's grades from grade_grades_history
* @param int $userid the user ID whose grades we want to recover
* @param int $courseid the relevant course
* @return bool true if successful or false if there was an error or no grades could be recovered
*/
function grade_recover_history_grades($userid, $courseid) {
global $CFG, $DB;
if ($CFG->disablegradehistory) {
debugging('Attempting to recover grades when grade history is disabled.');
return false;
}
//Were grades recovered? Flag to return.
$recoveredgrades = false;
//Check the user is enrolled in this course
//Dont bother checking if they have a gradeable role. They may get one later so recover
//whatever grades they have now just in case.
$course_context = get_context_instance(CONTEXT_COURSE, $courseid);
if (!is_enrolled($course_context, $userid)) {
debugging('Attempting to recover the grades of a user who is deleted or not enrolled. Skipping recover.');
return false;
}
//Check for existing grades for this user in this course
//Recovering grades when the user already has grades can lead to duplicate indexes and bad data
//In the future we could move the existing grades to the history table then recover the grades from before then
$sql = "SELECT gg.id
FROM {grade_grades} gg
JOIN {grade_items} gi ON gi.id = gg.itemid
WHERE gi.courseid = :courseid AND gg.userid = :userid";
$params = array('userid' => $userid, 'courseid' => $courseid);
if ($DB->record_exists_sql($sql, $params)) {
debugging('Attempting to recover the grades of a user who already has grades. Skipping recover.');
return false;
} else {
//Retrieve the user's old grades
//have history ID as first column to guarantee we a unique first column
$sql = "SELECT h.id, gi.itemtype, gi.itemmodule, gi.iteminstance as iteminstance, gi.itemnumber, h.source, h.itemid, h.userid, h.rawgrade, h.rawgrademax,
h.rawgrademin, h.rawscaleid, h.usermodified, h.finalgrade, h.hidden, h.locked, h.locktime, h.exported, h.overridden, h.excluded, h.feedback,
h.feedbackformat, h.information, h.informationformat, h.timemodified, itemcreated.tm AS timecreated
FROM {grade_grades_history} h
JOIN (SELECT itemid, MAX(id) AS id
FROM {grade_grades_history}
WHERE userid = :userid1
GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
JOIN {grade_items} gi ON gi.id = h.itemid
JOIN (SELECT itemid, MAX(timemodified) AS tm
FROM {grade_grades_history}
WHERE userid = :userid2 AND action = :insertaction
GROUP BY itemid) itemcreated ON itemcreated.itemid = h.itemid
WHERE gi.courseid = :courseid";
$params = array('userid1' => $userid, 'userid2' => $userid , 'insertaction' => GRADE_HISTORY_INSERT, 'courseid' => $courseid);
$oldgrades = $DB->get_records_sql($sql, $params);
//now move the old grades to the grade_grades table
foreach ($oldgrades as $oldgrade) {
unset($oldgrade->id);
$grade = new grade_grade($oldgrade, false);//2nd arg false as dont want to try and retrieve a record from the DB
$grade->insert($oldgrade->source);
//dont include default empty grades created when activities are created
if (!is_null($oldgrade->finalgrade) || !is_null($oldgrade->feedback)) {
$recoveredgrades = true;
}
}
}
//Some activities require manual grade synching (moving grades from the activity into the gradebook)
//If the student was deleted when synching was done they may have grades in the activity that haven't been moved across
grade_grab_course_grades($courseid, null, $userid);
return $recoveredgrades;
}
/**
* Updates all final grades in course.
*
@@ -1032,14 +1110,12 @@ function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null)
/**
* Refetches data from all course activities
*
* @global object
* @global object
* @param int $courseid
* @param string $modname
* @param int $courseid the course ID
* @param string $modname limit the grade fetch to a single module type
* @param int $userid limit the grade fetch to a single user
* @return void
*/
function grade_grab_course_grades($courseid, $modname=null) {
function grade_grab_course_grades($courseid, $modname=null, $userid=0) {
global $CFG, $DB;
if ($modname) {
@@ -1050,7 +1126,7 @@ function grade_grab_course_grades($courseid, $modname=null) {
if ($modinstances = $DB->get_records_sql($sql, $params)) {
foreach ($modinstances as $modinstance) {
grade_update_mod_grades($modinstance);
grade_update_mod_grades($modinstance, $userid);
}
}
return;
@@ -1075,7 +1151,7 @@ function grade_grab_course_grades($courseid, $modname=null) {
if ($modinstances = $DB->get_records_sql($sql, $params)) {
foreach ($modinstances as $modinstance) {
grade_update_mod_grades($modinstance);
grade_update_mod_grades($modinstance, $userid);
}
}
}
+116 -163
View File
@@ -141,11 +141,10 @@ function groups_get_grouping($groupingid, $fields='*', $strictness=IGNORE_MISSIN
* @param mixed $userid optional user id or array of ids, returns only groups of the user.
* @param int $groupingid optional returns only groups in the specified grouping.
* @param string $fields
* @return array|bool Returns an array of the group objects or false if no records
* or an error occurred. (userid field returned if array in $userid)
* @return array Returns an array of the group objects (userid field returned if array in $userid)
*/
function groups_get_all_groups($courseid, $userid=0, $groupingid=0, $fields='g.*') {
global $CFG, $DB;
global $DB;
if (empty($userid)) {
$userfrom = "";
@@ -178,15 +177,12 @@ function groups_get_all_groups($courseid, $userid=0, $groupingid=0, $fields='g.*
/**
* Returns info about user's groups in course.
*
* @global object
* @global object
* @global object
* @param int $courseid
* @param int $userid $USER if not specified
* @return array Array[groupingid][groupid] including grouping id 0 which means all groups
*/
function groups_get_user_groups($courseid, $userid=0) {
global $CFG, $USER, $DB;
global $USER, $DB;
if (empty($userid)) {
$userid = $USER->id;
@@ -392,15 +388,13 @@ function groups_get_activity_groupmode($cm, $course=null) {
/**
* Print group menu selector for course level.
*
* @global object
* @global object
* @param object $course course object
* @param string $urlroot return address
* @param stdClass $course course object
* @param string|moodle_url $urlroot return address
* @param boolean $return return as string instead of printing
* @return mixed void or string depending on $return param
*/
function groups_print_course_menu($course, $urlroot, $return=false) {
global $CFG, $USER, $SESSION, $OUTPUT;
global $USER, $OUTPUT;
if (!$groupmode = $course->groupmode) {
if ($return) {
@@ -411,44 +405,18 @@ function groups_print_course_menu($course, $urlroot, $return=false) {
}
$context = get_context_instance(CONTEXT_COURSE, $course->id);
if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $context)) {
$allowedgroups = groups_get_all_groups($course->id, 0, $course->defaultgroupingid);
// detect changes related to groups and fix active group
if (!empty($SESSION->activegroup[$course->id][VISIBLEGROUPS][0])) {
if (!array_key_exists($SESSION->activegroup[$course->id][VISIBLEGROUPS][0], $allowedgroups)) {
// active does not exist anymore
unset($SESSION->activegroup[$course->id][VISIBLEGROUPS][0]);
}
}
if (!empty($SESSION->activegroup[$course->id]['aag'][0])) {
if (!array_key_exists($SESSION->activegroup[$course->id]['aag'][0], $allowedgroups)) {
// active group does not exist anymore
unset($SESSION->activegroup[$course->id]['aag'][0]);
}
}
$aag = has_capability('moodle/site:accessallgroups', $context);
if ($groupmode == VISIBLEGROUPS or $aag) {
$allowedgroups = groups_get_all_groups($course->id, 0, $course->defaultgroupingid);
} else {
$allowedgroups = groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid);
// detect changes related to groups and fix active group
if (isset($SESSION->activegroup[$course->id][SEPARATEGROUPS][0])) {
if ($SESSION->activegroup[$course->id][SEPARATEGROUPS][0] == 0) {
if ($allowedgroups) {
// somebody must have assigned at least one group, we can select it now - yay!
unset($SESSION->activegroup[$course->id][SEPARATEGROUPS][0]);
}
} else {
if (!array_key_exists($SESSION->activegroup[$course->id][SEPARATEGROUPS][0], $allowedgroups)) {
// active group not allowed or does not exist anymore
unset($SESSION->activegroup[$course->id][SEPARATEGROUPS][0]);
}
}
}
}
$activegroup = groups_get_course_group($course, true);
$activegroup = groups_get_course_group($course, true, $allowedgroups);
$groupsmenu = array();
if (!$allowedgroups or $groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $context)) {
if (!$allowedgroups or $groupmode == VISIBLEGROUPS or $aag) {
$groupsmenu[0] = get_string('allparticipants');
}
@@ -464,6 +432,12 @@ function groups_print_course_menu($course, $urlroot, $return=false) {
$grouplabel = get_string('groupsseparate');
}
if ($aag and $course->defaultgroupingid) {
if ($grouping = groups_get_grouping($course->defaultgroupingid)) {
$grouplabel = $grouplabel . ' (' . format_string($grouping->name) . ')';
}
}
if (count($groupsmenu) == 1) {
$groupname = reset($groupsmenu);
$output = $grouplabel.': '.$groupname;
@@ -485,11 +459,8 @@ function groups_print_course_menu($course, $urlroot, $return=false) {
/**
* Print group menu selector for activity.
*
* @global object
* @global object
* @global object
* @param object $cm course module object
* @param string $urlroot return address that users get to if they choose an option;
* @param stdClass $cm course module object
* @param string|moodle_url $urlroot return address that users get to if they choose an option;
* should include any parameters needed, e.g. "$CFG->wwwroot/mod/forum/view.php?id=34"
* @param boolean $return return as string instead of printing
* @param boolean $hideallparticipants If true, this prevents the 'All participants'
@@ -501,15 +472,20 @@ function groups_print_course_menu($course, $urlroot, $return=false) {
* @return mixed void or string depending on $return param
*/
function groups_print_activity_menu($cm, $urlroot, $return=false, $hideallparticipants=false) {
global $CFG, $USER, $SESSION, $OUTPUT;
global $USER, $OUTPUT;
// Display error if urlroot is not absolute (this causes the non-JS version
// to break)
if (strpos($urlroot, 'http') !== 0) { // Will also work for https
debugging('groups_print_activity_menu requires absolute URL for ' .
'$urlroot, not <tt>' . s($urlroot) . '</tt>. Example: ' .
'groups_print_activity_menu($cm, $CFG->wwwroot . \'/mod/mymodule/view.php?id=13\');',
DEBUG_DEVELOPER);
if ($urlroot instanceof moodle_url) {
// no changes necessary
} else {
if (strpos($urlroot, 'http') !== 0) { // Will also work for https
// Display error if urlroot is not absolute (this causes the non-JS version to break)
debugging('groups_print_activity_menu requires absolute URL for ' .
'$urlroot, not <tt>' . s($urlroot) . '</tt>. Example: ' .
'groups_print_activity_menu($cm, $CFG->wwwroot . \'/mod/mymodule/view.php?id=13\');',
DEBUG_DEVELOPER);
}
$urlroot = new moodle_url($urlroot);
}
if (!$groupmode = groups_get_activity_groupmode($cm)) {
@@ -521,45 +497,18 @@ function groups_print_activity_menu($cm, $urlroot, $return=false, $hideallpartic
}
$context = get_context_instance(CONTEXT_MODULE, $cm->id);
if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $context)) {
$allowedgroups = groups_get_all_groups($cm->course, 0, $cm->groupingid); // any group in grouping (all if groupings not used)
// detect changes related to groups and fix active group
if (!empty($SESSION->activegroup[$cm->course][VISIBLEGROUPS][$cm->groupingid])) {
if (!array_key_exists($SESSION->activegroup[$cm->course][VISIBLEGROUPS][$cm->groupingid], $allowedgroups)) {
// active group does not exist anymore
unset($SESSION->activegroup[$cm->course][VISIBLEGROUPS][$cm->groupingid]);
}
}
if (!empty($SESSION->activegroup[$cm->course]['aag'][$cm->groupingid])) {
if (!array_key_exists($SESSION->activegroup[$cm->course]['aag'][$cm->groupingid], $allowedgroups)) {
// active group does not exist anymore
unset($SESSION->activegroup[$cm->course]['aag'][$cm->groupingid]);
}
}
$aag = has_capability('moodle/site:accessallgroups', $context);
if ($groupmode == VISIBLEGROUPS or $aag) {
$allowedgroups = groups_get_all_groups($cm->course, 0, $cm->groupingid); // any group in grouping
} else {
$allowedgroups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid); // only assigned groups
// detect changes related to groups and fix active group
if (isset($SESSION->activegroup[$cm->course][SEPARATEGROUPS][$cm->groupingid])) {
if ($SESSION->activegroup[$cm->course][SEPARATEGROUPS][$cm->groupingid] == 0) {
if ($allowedgroups) {
// somebody must have assigned at least one group, we can select it now - yay!
unset($SESSION->activegroup[$cm->course][SEPARATEGROUPS][$cm->groupingid]);
}
} else {
if (!array_key_exists($SESSION->activegroup[$cm->course][SEPARATEGROUPS][$cm->groupingid], $allowedgroups)) {
// active group not allowed or does not exist anymore
unset($SESSION->activegroup[$cm->course][SEPARATEGROUPS][$cm->groupingid]);
}
}
}
}
$activegroup = groups_get_activity_group($cm, true);
$activegroup = groups_get_activity_group($cm, true, $allowedgroups);
$groupsmenu = array();
if ((!$allowedgroups or $groupmode == VISIBLEGROUPS or
has_capability('moodle/site:accessallgroups', $context)) and !$hideallparticipants) {
if ((!$allowedgroups or $groupmode == VISIBLEGROUPS or $aag) and !$hideallparticipants) {
$groupsmenu[0] = get_string('allparticipants');
}
@@ -575,11 +524,17 @@ function groups_print_activity_menu($cm, $urlroot, $return=false, $hideallpartic
$grouplabel = get_string('groupsseparate');
}
if ($aag and $cm->groupingid) {
if ($grouping = groups_get_grouping($cm->groupingid)) {
$grouplabel = $grouplabel . ' (' . format_string($grouping->name) . ')';
}
}
if (count($groupsmenu) == 1) {
$groupname = reset($groupsmenu);
$output = $grouplabel.': '.$groupname;
} else {
$select = new single_select(new moodle_url($urlroot), 'group', $groupsmenu, $activegroup, null, 'selectgroup');
$select = new single_select($urlroot, 'group', $groupsmenu, $activegroup, null, 'selectgroup');
$select->label = $grouplabel;
$output = $OUTPUT->render($select);
}
@@ -596,142 +551,96 @@ function groups_print_activity_menu($cm, $urlroot, $return=false, $hideallpartic
/**
* Returns group active in course, changes the group by default if 'group' page param present
*
* @global object
* @global object
* @global object
* @param object $course course bject
* @param stdClass $course course bject
* @param boolean $update change active group if group param submitted
* @param array $allowedgroups list of groups user may access (INTERNAL, to be used only from groups_print_course_menu())
* @return mixed false if groups not used, int if groups used, 0 means all groups (access must be verified in SEPARATE mode)
*/
function groups_get_course_group($course, $update=false) {
global $CFG, $USER, $SESSION;
function groups_get_course_group($course, $update=false, $allowedgroups=null) {
global $USER, $SESSION;
if (!$groupmode = $course->groupmode) {
// NOGROUPS used
return false;
}
// init activegroup array
if (!isset($SESSION->activegroup)) {
$SESSION->activegroup = array();
}
if (!array_key_exists($course->id, $SESSION->activegroup)) {
$SESSION->activegroup[$course->id] = array(SEPARATEGROUPS=>array(), VISIBLEGROUPS=>array(), 'aag'=>array());
}
$context = get_context_instance(CONTEXT_COURSE, $course->id);
if (has_capability('moodle/site:accessallgroups', $context)) {
$groupmode = 'aag';
}
// grouping used the first time - add first user group as default
if (!array_key_exists(0, $SESSION->activegroup[$course->id][$groupmode])) {
if ($groupmode == 'aag') {
$SESSION->activegroup[$course->id][$groupmode][0] = 0; // all groups by default if user has accessallgroups
} else if ($usergroups = groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid)) {
$firstgroup = reset($usergroups);
$SESSION->activegroup[$course->id][$groupmode][0] = $firstgroup->id;
if (!is_array($allowedgroups)) {
if ($groupmode == VISIBLEGROUPS or $groupmode === 'aag') {
$allowedgroups = groups_get_all_groups($course->id, 0, $course->defaultgroupingid);
} else {
// this happen when user not assigned into group in SEPARATEGROUPS mode or groups do not exist yet
// mod authors must add extra checks for this when SEPARATEGROUPS mode used (such as when posting to forum)
$SESSION->activegroup[$course->id][$groupmode][0] = 0;
$allowedgroups = groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid);
}
}
_group_verify_activegroup($course->id, $groupmode, $course->defaultgroupingid, $allowedgroups);
// set new active group if requested
$changegroup = optional_param('group', -1, PARAM_INT);
if ($update and $changegroup != -1) {
if ($changegroup == 0) {
// do not allow changing to all groups without accessallgroups capability
if ($groupmode == VISIBLEGROUPS or $groupmode == 'aag') {
$SESSION->activegroup[$course->id][$groupmode][0] = 0;
if ($groupmode == VISIBLEGROUPS or $groupmode === 'aag') {
$SESSION->activegroup[$course->id][$groupmode][$course->defaultgroupingid] = 0;
}
} else {
// first make list of allowed groups
if ($groupmode == VISIBLEGROUPS or $groupmode == 'aag') {
$allowedgroups = groups_get_all_groups($course->id, 0, $course->defaultgroupingid);
} else {
$allowedgroups = groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid);
}
if ($allowedgroups and array_key_exists($changegroup, $allowedgroups)) {
$SESSION->activegroup[$course->id][$groupmode][0] = $changegroup;
$SESSION->activegroup[$course->id][$groupmode][$course->defaultgroupingid] = $changegroup;
}
}
}
return $SESSION->activegroup[$course->id][$groupmode][0];
return $SESSION->activegroup[$course->id][$groupmode][$course->defaultgroupingid];
}
/**
* Returns group active in activity, changes the group by default if 'group' page param present
*
* @global object
* @global object
* @global object
* @param object $cm course module object
* @param stdClass $cm course module object
* @param boolean $update change active group if group param submitted
* @param array $allowedgroups list of groups user may access (INTERNAL, to be used only from groups_print_activity_menu())
* @return mixed false if groups not used, int if groups used, 0 means all groups (access must be verified in SEPARATE mode)
*/
function groups_get_activity_group($cm, $update=false) {
global $CFG, $USER, $SESSION;
function groups_get_activity_group($cm, $update=false, $allowedgroups=null) {
global $USER, $SESSION;
if (!$groupmode = groups_get_activity_groupmode($cm)) {
// NOGROUPS used
return false;
}
// init activegroup array
if (!isset($SESSION->activegroup)) {
$SESSION->activegroup = array();
}
if (!array_key_exists($cm->course, $SESSION->activegroup)) {
$SESSION->activegroup[$cm->course] = array(SEPARATEGROUPS=>array(), VISIBLEGROUPS=>array(), 'aag'=>array());
}
$context = get_context_instance(CONTEXT_MODULE, $cm->id);
if (has_capability('moodle/site:accessallgroups', $context)) {
$groupmode = 'aag';
}
// grouping used the first time - add first user group as default
if (!array_key_exists($cm->groupingid, $SESSION->activegroup[$cm->course][$groupmode])) {
if ($groupmode == 'aag') {
$SESSION->activegroup[$cm->course][$groupmode][$cm->groupingid] = 0; // all groups by default if user has accessallgroups
} else if ($usergroups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) {
$firstgroup = reset($usergroups);
$SESSION->activegroup[$cm->course][$groupmode][$cm->groupingid] = $firstgroup->id;
if (!is_array($allowedgroups)) {
if ($groupmode == VISIBLEGROUPS or $groupmode === 'aag') {
$allowedgroups = groups_get_all_groups($cm->course, 0, $cm->groupingid);
} else {
// this happen when user not assigned into group in SEPARATEGROUPS mode or groups do not exist yet
// mod authors must add extra checks for this when SEPARATEGROUPS mode used (such as when posting to forum)
$SESSION->activegroup[$cm->course][$groupmode][$cm->groupingid] = 0;
$allowedgroups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid);
}
}
_group_verify_activegroup($cm->course, $groupmode, $cm->groupingid, $allowedgroups);
// set new active group if requested
$changegroup = optional_param('group', -1, PARAM_INT);
if ($update and $changegroup != -1) {
if ($changegroup == 0) {
// allgroups visible only in VISIBLEGROUPS or when accessallgroups
if ($groupmode == VISIBLEGROUPS or $groupmode == 'aag') {
if ($groupmode == VISIBLEGROUPS or $groupmode === 'aag') {
$SESSION->activegroup[$cm->course][$groupmode][$cm->groupingid] = 0;
}
} else {
// first make list of allowed groups
if ($groupmode == VISIBLEGROUPS or $groupmode == 'aag') {
$allowedgroups = groups_get_all_groups($cm->course, 0, $cm->groupingid); // any group in grouping (all if groupings not used)
} else {
$allowedgroups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid); // only assigned groups
}
if ($allowedgroups and array_key_exists($changegroup, $allowedgroups)) {
$SESSION->activegroup[$cm->course][$groupmode][$cm->groupingid] = $changegroup;
}
@@ -745,7 +654,6 @@ function groups_get_activity_group($cm, $update=false) {
* Gets a list of groups that the user is allowed to access within the
* specified activity.
*
* @global object
* @param object $cm Course-module
* @param int $userid User ID (defaults to current user)
* @return array An array of group objects, or false if none
@@ -776,8 +684,6 @@ function groups_get_activity_allowed_groups($cm,$userid=0) {
*
* $USER If $userid is null, use the global object.
*
* @global object
* @global object
* @param int $cm The course module
* @param int $userid The user to check against the group.
* @return boolean True if the user can view the course module, false otherwise.
@@ -799,3 +705,50 @@ function groups_course_module_visible($cm, $userid=null) {
}
return false;
}
/**
* Internal method, sets up $SESSION->activegroup and verifies previous value
*
* @private
* @param int $courseid
* @param int|string $groupmode SEPARATEGROUPS, VISIBLEGROUPS or 'aag' (access all groups)
* @param int $groupingid 0 means all groups
* @param all $allowedgroups list of groups user can see
* @return void
*/
function _group_verify_activegroup($courseid, $groupmode, $groupingid, array $allowedgroups) {
global $SESSION;
// init activegroup array if necessary
if (!isset($SESSION->activegroup)) {
$SESSION->activegroup = array();
}
if (!array_key_exists($courseid, $SESSION->activegroup)) {
$SESSION->activegroup[$courseid] = array(SEPARATEGROUPS=>array(), VISIBLEGROUPS=>array(), 'aag'=>array());
}
// make sure that the current group info is ok
if (array_key_exists($groupingid, $SESSION->activegroup[$courseid][$groupmode]) and !array_key_exists($SESSION->activegroup[$courseid][$groupmode][$groupingid], $allowedgroups)) {
// active group does not exist anymore or is 0
if ($SESSION->activegroup[$courseid][$groupmode][$groupingid] > 0 or $groupmode == SEPARATEGROUPS) {
// do not do this if all groups selected and groupmode is not separate
unset($SESSION->activegroup[$courseid][$groupmode][$groupingid]);
}
}
// set up defaults if necessary
if (!array_key_exists($groupingid, $SESSION->activegroup[$courseid][$groupmode])) {
if ($groupmode == 'aag') {
$SESSION->activegroup[$courseid][$groupmode][$groupingid] = 0; // all groups by default if user has accessallgroups
} else if ($allowedgroups) {
$firstgroup = reset($allowedgroups);
$SESSION->activegroup[$courseid][$groupmode][$groupingid] = $firstgroup->id;
} else {
// this happen when user not assigned into group in SEPARATEGROUPS mode or groups do not exist yet
// mod authors must add extra checks for this when SEPARATEGROUPS mode used (such as when posting to forum)
$SESSION->activegroup[$courseid][$groupmode][$groupingid] = 0;
}
}
}
+3 -3
View File
@@ -19,7 +19,7 @@
*/
/*
HTML Purifier 4.2.0 - Standards Compliant HTML Filtering
HTML Purifier 4.3.0 - Standards Compliant HTML Filtering
Copyright (C) 2006-2008 Edward Z. Yang
This library is free software; you can redistribute it and/or
@@ -55,10 +55,10 @@ class HTMLPurifier
{
/** Version of HTML Purifier */
public $version = '4.2.0';
public $version = '4.3.0';
/** Constant with version of HTML Purifier */
const VERSION = '4.2.0';
const VERSION = '4.3.0';
/** Global configuration object */
public $config;
@@ -119,6 +119,7 @@ require_once $__dir . '/HTMLPurifier/AttrTransform/Lang.php';
require_once $__dir . '/HTMLPurifier/AttrTransform/Length.php';
require_once $__dir . '/HTMLPurifier/AttrTransform/Name.php';
require_once $__dir . '/HTMLPurifier/AttrTransform/NameSync.php';
require_once $__dir . '/HTMLPurifier/AttrTransform/Nofollow.php';
require_once $__dir . '/HTMLPurifier/AttrTransform/SafeEmbed.php';
require_once $__dir . '/HTMLPurifier/AttrTransform/SafeObject.php';
require_once $__dir . '/HTMLPurifier/AttrTransform/SafeParam.php';
@@ -145,6 +146,7 @@ require_once $__dir . '/HTMLPurifier/HTMLModule/Image.php';
require_once $__dir . '/HTMLPurifier/HTMLModule/Legacy.php';
require_once $__dir . '/HTMLPurifier/HTMLModule/List.php';
require_once $__dir . '/HTMLPurifier/HTMLModule/Name.php';
require_once $__dir . '/HTMLPurifier/HTMLModule/Nofollow.php';
require_once $__dir . '/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php';
require_once $__dir . '/HTMLPurifier/HTMLModule/Object.php';
require_once $__dir . '/HTMLPurifier/HTMLModule/Presentation.php';
@@ -2,11 +2,43 @@
/**
* Validates a font family list according to CSS spec
* @todo whitelisting allowed fonts would be nice
*/
class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef
{
protected $mask = null;
public function __construct() {
$this->mask = '- ';
for ($c = 'a'; $c <= 'z'; $c++) $this->mask .= $c;
for ($c = 'A'; $c <= 'Z'; $c++) $this->mask .= $c;
for ($c = '0'; $c <= '9'; $c++) $this->mask .= $c; // cast-y, but should be fine
// special bytes used by UTF-8
for ($i = 0x80; $i <= 0xFF; $i++) {
// We don't bother excluding invalid bytes in this range,
// because the our restriction of well-formed UTF-8 will
// prevent these from ever occurring.
$this->mask .= chr($i);
}
/*
PHP's internal strcspn implementation is
O(length of string * length of mask), making it inefficient
for large masks. However, it's still faster than
preg_match 8)
for (p = s1;;) {
spanp = s2;
do {
if (*spanp == c || p == s1_end) {
return p - s1;
}
} while (spanp++ < (s2_end - 1));
c = *++p;
}
*/
// possible optimization: invert the mask.
}
public function validate($string, $config, $context) {
static $generic_names = array(
'serif' => true,
@@ -15,6 +47,7 @@ class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef
'fantasy' => true,
'cursive' => true
);
$allowed_fonts = $config->get('CSS.AllowedFonts');
// assume that no font names contain commas in them
$fonts = explode(',', $string);
@@ -24,7 +57,9 @@ class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef
if ($font === '') continue;
// match a generic name
if (isset($generic_names[$font])) {
$final .= $font . ', ';
if ($allowed_fonts === null || isset($allowed_fonts[$font])) {
$final .= $font . ', ';
}
continue;
}
// match a quoted name
@@ -40,6 +75,10 @@ class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef
// $font is a pure representation of the font name
if ($allowed_fonts !== null && !isset($allowed_fonts[$font])) {
continue;
}
if (ctype_alnum($font) && $font !== '') {
// very simple font, allow it in unharmed
$final .= $font . ', ';
@@ -50,17 +89,103 @@ class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef
// shouldn't show up regardless
$font = str_replace(array("\n", "\t", "\r", "\x0C"), ' ', $font);
// These ugly transforms don't pose a security
// risk (as \\ and \" might). We could try to be clever and
// use single-quote wrapping when there is a double quote
// present, but I have choosen not to implement that.
// (warning: this code relies on the selection of quotation
// mark below)
$font = str_replace('\\', '\\5C ', $font);
$font = str_replace('"', '\\22 ', $font);
// Here, there are various classes of characters which need
// to be treated differently:
// - Alphanumeric characters are essentially safe. We
// handled these above.
// - Spaces require quoting, though most parsers will do
// the right thing if there aren't any characters that
// can be misinterpreted
// - Dashes rarely occur, but they fairly unproblematic
// for parsing/rendering purposes.
// The above characters cover the majority of Western font
// names.
// - Arbitrary Unicode characters not in ASCII. Because
// most parsers give little thought to Unicode, treatment
// of these codepoints is basically uniform, even for
// punctuation-like codepoints. These characters can
// show up in non-Western pages and are supported by most
// major browsers, for example: "MS 明朝" is a
// legitimate font-name
// <http://ja.wikipedia.org/wiki/MS_明朝>. See
// the CSS3 spec for more examples:
// <http://www.w3.org/TR/2011/WD-css3-fonts-20110324/localizedfamilynames.png>
// You can see live samples of these on the Internet:
// <http://www.google.co.jp/search?q=font-family+MS+明朝|ゴシック>
// However, most of these fonts have ASCII equivalents:
// for example, 'MS Mincho', and it's considered
// professional to use ASCII font names instead of
// Unicode font names. Thanks Takeshi Terada for
// providing this information.
// The following characters, to my knowledge, have not been
// used to name font names.
// - Single quote. While theoretically you might find a
// font name that has a single quote in its name (serving
// as an apostrophe, e.g. Dave's Scribble), I haven't
// been able to find any actual examples of this.
// Internet Explorer's cssText translation (which I
// believe is invoked by innerHTML) normalizes any
// quoting to single quotes, and fails to escape single
// quotes. (Note that this is not IE's behavior for all
// CSS properties, just some sort of special casing for
// font-family). So a single quote *cannot* be used
// safely in the font-family context if there will be an
// innerHTML/cssText translation. Note that Firefox 3.x
// does this too.
// - Double quote. In IE, these get normalized to
// single-quotes, no matter what the encoding. (Fun
// fact, in IE8, the 'content' CSS property gained
// support, where they special cased to preserve encoded
// double quotes, but still translate unadorned double
// quotes into single quotes.) So, because their
// fixpoint behavior is identical to single quotes, they
// cannot be allowed either. Firefox 3.x displays
// single-quote style behavior.
// - Backslashes are reduced by one (so \\ -> \) every
// iteration, so they cannot be used safely. This shows
// up in IE7, IE8 and FF3
// - Semicolons, commas and backticks are handled properly.
// - The rest of the ASCII punctuation is handled properly.
// We haven't checked what browsers do to unadorned
// versions, but this is not important as long as the
// browser doesn't /remove/ surrounding quotes (as IE does
// for HTML).
//
// With these results in hand, we conclude that there are
// various levels of safety:
// - Paranoid: alphanumeric, spaces and dashes(?)
// - International: Paranoid + non-ASCII Unicode
// - Edgy: Everything except quotes, backslashes
// - NoJS: Standards compliance, e.g. sod IE. Note that
// with some judicious character escaping (since certain
// types of escaping doesn't work) this is theoretically
// OK as long as innerHTML/cssText is not called.
// We believe that international is a reasonable default
// (that we will implement now), and once we do more
// extensive research, we may feel comfortable with dropping
// it down to edgy.
// complicated font, requires quoting
$final .= "\"$font\", "; // note that this will later get turned into &quot;
// Edgy: alphanumeric, spaces, dashes and Unicode. Use of
// str(c)spn assumes that the string was already well formed
// Unicode (which of course it is).
if (strspn($font, $this->mask) !== strlen($font)) {
continue;
}
// Historical:
// In the absence of innerHTML/cssText, these ugly
// transforms don't pose a security risk (as \\ and \"
// might--these escapes are not supported by most browsers).
// We could try to be clever and use single-quote wrapping
// when there is a double quote present, but I have choosen
// not to implement that. (NOTE: you can reduce the amount
// of escapes by one depending on what quoting style you use)
// $font = str_replace('\\', '\\5C ', $font);
// $font = str_replace('"', '\\22 ', $font);
// $font = str_replace("'", '\\27 ', $font);
// font possibly with spaces, requires quoting
$final .= "'$font', ";
}
$final = rtrim($final, ', ');
if ($final === '') return false;
@@ -43,6 +43,15 @@ class HTMLPurifier_AttrDef_CSS_URI extends HTMLPurifier_AttrDef_URI
// extra sanity check; should have been done by URI
$result = str_replace(array('"', "\\", "\n", "\x0c", "\r"), "", $result);
// suspicious characters are ()'; we're going to percent encode
// them for safety.
$result = str_replace(array('(', ')', "'"), array('%28', '%29', '%27'), $result);
// there's an extra bug where ampersands lose their escaping on
// an innerHTML cycle, so a very unlucky query parameter could
// then change the meaning of the URL. Unfortunately, there's
// not much we can do about that...
return "url(\"$result\")";
}
@@ -23,6 +23,12 @@ class HTMLPurifier_AttrDef_URI_Host extends HTMLPurifier_AttrDef
public function validate($string, $config, $context) {
$length = strlen($string);
// empty hostname is OK; it's usually semantically equivalent:
// the default host as defined by a URI scheme is used:
//
// If the URI scheme defines a default for host, then that
// default applies when the host subcomponent is undefined
// or when the registered name is empty (zero length).
if ($string === '') return '';
if ($length > 1 && $string[0] === '[' && $string[$length-1] === ']') {
//IPv6
@@ -0,0 +1,41 @@
<?php
// must be called POST validation
/**
* Adds rel="nofollow" to all outbound links. This transform is
* only attached if Attr.Nofollow is TRUE.
*/
class HTMLPurifier_AttrTransform_Nofollow extends HTMLPurifier_AttrTransform
{
private $parser;
public function __construct() {
$this->parser = new HTMLPurifier_URIParser();
}
public function transform($attr, $config, $context) {
if (!isset($attr['href'])) {
return $attr;
}
// XXX Kind of inefficient
$url = $this->parser->parse($attr['href']);
$scheme = $url->getSchemeObj($config, $context);
if (!is_null($url->host) && $scheme !== false && $scheme->browsable) {
if (isset($attr['rel'])) {
$attr['rel'] .= ' nofollow';
} else {
$attr['rel'] = 'nofollow';
}
}
return $attr;
}
}
// vim: et sw=4 sts=4
@@ -19,6 +19,7 @@ class HTMLPurifier_AttrTransform_SafeParam extends HTMLPurifier_AttrTransform
public function __construct() {
$this->uri = new HTMLPurifier_AttrDef_URI(true); // embedded
$this->wmode = new HTMLPurifier_AttrDef_Enum(array('window', 'opaque', 'transparent'));
}
public function transform($attr, $config, $context) {
@@ -41,7 +42,7 @@ class HTMLPurifier_AttrTransform_SafeParam extends HTMLPurifier_AttrTransform
}
break;
case 'wmode':
$attr['value'] = 'window';
$attr['value'] = $this->wmode->validate($attr['value'], $config, $context);
break;
case 'movie':
case 'src':
+8 -2
View File
@@ -37,7 +37,12 @@ class HTMLPurifier_Bootstrap
public static function autoload($class) {
$file = HTMLPurifier_Bootstrap::getPath($class);
if (!$file) return false;
require HTMLPURIFIER_PREFIX . '/' . $file;
// Technically speaking, it should be ok and more efficient to
// just do 'require', but Antonio Parraga reports that with
// Zend extensions such as Zend debugger and APC, this invariant
// may be broken. Since we have efficient alternatives, pay
// the cost here and avoid the bug.
require_once HTMLPURIFIER_PREFIX . '/' . $file;
return true;
}
@@ -65,10 +70,11 @@ class HTMLPurifier_Bootstrap
if ( ($funcs = spl_autoload_functions()) === false ) {
spl_autoload_register($autoload);
} elseif (function_exists('spl_autoload_unregister')) {
$buggy = version_compare(PHP_VERSION, '5.2.11', '<');
$compat = version_compare(PHP_VERSION, '5.1.2', '<=') &&
version_compare(PHP_VERSION, '5.1.0', '>=');
foreach ($funcs as $func) {
if (is_array($func)) {
if ($buggy && is_array($func)) {
// :TRICKY: There are some compatibility issues and some
// places where we need to error out
$reflector = new ReflectionMethod($func[0], $func[1]);
@@ -219,6 +219,10 @@ class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition
$this->doSetupTricky($config);
}
if ($config->get('CSS.Trusted')) {
$this->doSetupTrusted($config);
}
$allow_important = $config->get('CSS.AllowImportant');
// wrap all attr-defs with decorator that handles !important
foreach ($this->info as $k => $v) {
@@ -260,6 +264,23 @@ class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition
$this->info['overflow'] = new HTMLPurifier_AttrDef_Enum(array('visible', 'hidden', 'auto', 'scroll'));
}
protected function doSetupTrusted($config) {
$this->info['position'] = new HTMLPurifier_AttrDef_Enum(array(
'static', 'relative', 'absolute', 'fixed'
));
$this->info['top'] =
$this->info['left'] =
$this->info['right'] =
$this->info['bottom'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
new HTMLPurifier_AttrDef_CSS_Length(),
new HTMLPurifier_AttrDef_CSS_Percentage(),
new HTMLPurifier_AttrDef_Enum(array('auto')),
));
$this->info['z-index'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
new HTMLPurifier_AttrDef_Integer(),
new HTMLPurifier_AttrDef_Enum(array('auto')),
));
}
/**
* Performs extra config-based processing. Based off of
+178 -49
View File
@@ -20,7 +20,7 @@ class HTMLPurifier_Config
/**
* HTML Purifier's version
*/
public $version = '4.2.0';
public $version = '4.3.0';
/**
* Bool indicator whether or not to automatically finalize
@@ -76,7 +76,8 @@ class HTMLPurifier_Config
/**
* Set to false if you do not want line and file numbers in errors
* (useful when unit testing)
* (useful when unit testing). This will also compress some errors
* and exceptions.
*/
public $chatty = true;
@@ -318,26 +319,64 @@ class HTMLPurifier_Config
* Retrieves object reference to the HTML definition.
* @param $raw Return a copy that has not been setup yet. Must be
* called before it's been setup, otherwise won't work.
* @param $optimized If true, this method may return null, to
* indicate that a cached version of the modified
* definition object is available and no further edits
* are necessary. Consider using
* maybeGetRawHTMLDefinition, which is more explicitly
* named, instead.
*/
public function getHTMLDefinition($raw = false) {
return $this->getDefinition('HTML', $raw);
public function getHTMLDefinition($raw = false, $optimized = false) {
return $this->getDefinition('HTML', $raw, $optimized);
}
/**
* Retrieves object reference to the CSS definition
* @param $raw Return a copy that has not been setup yet. Must be
* called before it's been setup, otherwise won't work.
* @param $optimized If true, this method may return null, to
* indicate that a cached version of the modified
* definition object is available and no further edits
* are necessary. Consider using
* maybeGetRawCSSDefinition, which is more explicitly
* named, instead.
*/
public function getCSSDefinition($raw = false) {
return $this->getDefinition('CSS', $raw);
public function getCSSDefinition($raw = false, $optimized = false) {
return $this->getDefinition('CSS', $raw, $optimized);
}
/**
* Retrieves object reference to the URI definition
* @param $raw Return a copy that has not been setup yet. Must be
* called before it's been setup, otherwise won't work.
* @param $optimized If true, this method may return null, to
* indicate that a cached version of the modified
* definition object is available and no further edits
* are necessary. Consider using
* maybeGetRawURIDefinition, which is more explicitly
* named, instead.
*/
public function getURIDefinition($raw = false, $optimized = false) {
return $this->getDefinition('URI', $raw, $optimized);
}
/**
* Retrieves a definition
* @param $type Type of definition: HTML, CSS, etc
* @param $raw Whether or not definition should be returned raw
* @param $optimized Only has an effect when $raw is true. Whether
* or not to return null if the result is already present in
* the cache. This is off by default for backwards
* compatibility reasons, but you need to do things this
* way in order to ensure that caching is done properly.
* Check out enduser-customize.html for more details.
* We probably won't ever change this default, as much as the
* maybe semantics is the "right thing to do."
*/
public function getDefinition($type, $raw = false) {
public function getDefinition($type, $raw = false, $optimized = false) {
if ($optimized && !$raw) {
throw new HTMLPurifier_Exception("Cannot set optimized = true when raw = false");
}
if (!$this->finalized) $this->autoFinalize();
// temporarily suspend locks, so we can handle recursive definition calls
$lock = $this->lock;
@@ -346,52 +385,137 @@ class HTMLPurifier_Config
$cache = $factory->create($type, $this);
$this->lock = $lock;
if (!$raw) {
// see if we can quickly supply a definition
// full definition
// ---------------
// check if definition is in memory
if (!empty($this->definitions[$type])) {
if (!$this->definitions[$type]->setup) {
$this->definitions[$type]->setup($this);
$cache->set($this->definitions[$type], $this);
$def = $this->definitions[$type];
// check if the definition is setup
if ($def->setup) {
return $def;
} else {
$def->setup($this);
if ($def->optimized) $cache->add($def, $this);
return $def;
}
return $this->definitions[$type];
}
// memory check missed, try cache
$this->definitions[$type] = $cache->get($this);
if ($this->definitions[$type]) {
// definition in cache, return it
return $this->definitions[$type];
// check if definition is in cache
$def = $cache->get($this);
if ($def) {
// definition in cache, save to memory and return it
$this->definitions[$type] = $def;
return $def;
}
} elseif (
!empty($this->definitions[$type]) &&
!$this->definitions[$type]->setup
) {
// raw requested, raw in memory, quick return
return $this->definitions[$type];
// initialize it
$def = $this->initDefinition($type);
// set it up
$this->lock = $type;
$def->setup($this);
$this->lock = null;
// save in cache
$cache->add($def, $this);
// return it
return $def;
} else {
// raw definition
// --------------
// check preconditions
$def = null;
if ($optimized) {
if (is_null($this->get($type . '.DefinitionID'))) {
// fatally error out if definition ID not set
throw new HTMLPurifier_Exception("Cannot retrieve raw version without specifying %$type.DefinitionID");
}
}
if (!empty($this->definitions[$type])) {
$def = $this->definitions[$type];
if ($def->setup && !$optimized) {
$extra = $this->chatty ? " (try moving this code block earlier in your initialization)" : "";
throw new HTMLPurifier_Exception("Cannot retrieve raw definition after it has already been setup" . $extra);
}
if ($def->optimized === null) {
$extra = $this->chatty ? " (try flushing your cache)" : "";
throw new HTMLPurifier_Exception("Optimization status of definition is unknown" . $extra);
}
if ($def->optimized !== $optimized) {
$msg = $optimized ? "optimized" : "unoptimized";
$extra = $this->chatty ? " (this backtrace is for the first inconsistent call, which was for a $msg raw definition)" : "";
throw new HTMLPurifier_Exception("Inconsistent use of optimized and unoptimized raw definition retrievals" . $extra);
}
}
// check if definition was in memory
if ($def) {
if ($def->setup) {
// invariant: $optimized === true (checked above)
return null;
} else {
return $def;
}
}
// if optimized, check if definition was in cache
// (because we do the memory check first, this formulation
// is prone to cache slamming, but I think
// guaranteeing that either /all/ of the raw
// setup code or /none/ of it is run is more important.)
if ($optimized) {
// This code path only gets run once; once we put
// something in $definitions (which is guaranteed by the
// trailing code), we always short-circuit above.
$def = $cache->get($this);
if ($def) {
// save the full definition for later, but don't
// return it yet
$this->definitions[$type] = $def;
return null;
}
}
// check invariants for creation
if (!$optimized) {
if (!is_null($this->get($type . '.DefinitionID'))) {
if ($this->chatty) {
$this->triggerError("Due to a documentation error in previous version of HTML Purifier, your definitions are not being cached. If this is OK, you can remove the %$type.DefinitionRev and %$type.DefinitionID declaration. Otherwise, modify your code to use maybeGetRawDefinition, and test if the returned value is null before making any edits (if it is null, that means that a cached version is available, and no raw operations are necessary). See <a href='http://htmlpurifier.org/docs/enduser-customize.html#optimized'>Customize</a> for more details", E_USER_WARNING);
} else {
$this->triggerError("Useless DefinitionID declaration", E_USER_WARNING);
}
}
}
// initialize it
$def = $this->initDefinition($type);
$def->optimized = $optimized;
return $def;
}
throw new HTMLPurifier_Exception("The impossible happened!");
}
private function initDefinition($type) {
// quick checks failed, let's create the object
if ($type == 'HTML') {
$this->definitions[$type] = new HTMLPurifier_HTMLDefinition();
$def = new HTMLPurifier_HTMLDefinition();
} elseif ($type == 'CSS') {
$this->definitions[$type] = new HTMLPurifier_CSSDefinition();
$def = new HTMLPurifier_CSSDefinition();
} elseif ($type == 'URI') {
$this->definitions[$type] = new HTMLPurifier_URIDefinition();
$def = new HTMLPurifier_URIDefinition();
} else {
throw new HTMLPurifier_Exception("Definition of $type type not supported");
}
// quick abort if raw
if ($raw) {
if (is_null($this->get($type . '.DefinitionID'))) {
// fatally error out if definition ID not set
throw new HTMLPurifier_Exception("Cannot retrieve raw version without specifying %$type.DefinitionID");
}
return $this->definitions[$type];
}
// set it up
$this->lock = $type;
$this->definitions[$type]->setup($this);
$this->lock = null;
// save in cache
$cache->set($this->definitions[$type], $this);
return $this->definitions[$type];
$this->definitions[$type] = $def;
return $def;
}
public function maybeGetRawDefinition($name) {
return $this->getDefinition($name, true, true);
}
public function maybeGetRawHTMLDefinition() {
return $this->getDefinition('HTML', true, true);
}
public function maybeGetRawCSSDefinition() {
return $this->getDefinition('CSS', true, true);
}
public function maybeGetRawURIDefinition() {
return $this->getDefinition('URI', true, true);
}
/**
@@ -549,17 +673,22 @@ class HTMLPurifier_Config
/**
* Produces a nicely formatted error message by supplying the
* stack frame information from two levels up and OUTSIDE of
* HTMLPurifier_Config.
* stack frame information OUTSIDE of HTMLPurifier_Config.
*/
protected function triggerError($msg, $no) {
// determine previous stack frame
$backtrace = debug_backtrace();
if ($this->chatty && isset($backtrace[1])) {
$frame = $backtrace[1];
$extra = " on line {$frame['line']} in file {$frame['file']}";
} else {
$extra = '';
$extra = '';
if ($this->chatty) {
$trace = debug_backtrace();
// zip(tail(trace), trace) -- but PHP is not Haskell har har
for ($i = 0, $c = count($trace); $i < $c - 1; $i++) {
if ($trace[$i + 1]['class'] === 'HTMLPurifier_Config') {
continue;
}
$frame = $trace[$i];
$extra = " invoked on line {$frame['line']} in file {$frame['file']}";
break;
}
}
trigger_error($msg . $extra, $no);
}
@@ -60,7 +60,13 @@ class HTMLPurifier_ConfigSchema {
* Unserializes the default ConfigSchema.
*/
public static function makeFromSerial() {
return unserialize(file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema.ser'));
$contents = file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema.ser');
$r = unserialize($contents);
if (!$r) {
$hash = sha1($contents);
trigger_error("Unserialization of configuration schema failed, sha1 of file was $hash", E_USER_ERROR);
}
return $r;
}
/**
@@ -0,0 +1,12 @@
CSS.AllowedFonts
TYPE: lookup/null
VERSION: 4.3.0
DEFAULT: NULL
--DESCRIPTION--
<p>
Allows you to manually specify a set of allowed fonts. If
<code>NULL</code>, all fonts are allowed. This directive
affects generic names (serif, sans-serif, monospace, cursive,
fantasy) as well as specific font families.
</p>
--# vim: et sw=4 sts=4
@@ -0,0 +1,9 @@
CSS.Trusted
TYPE: bool
VERSION: 4.2.1
DEFAULT: false
--DESCRIPTION--
Indicates whether or not the user's CSS input is trusted or not. If the
input is trusted, a more expansive set of allowed properties. See
also %HTML.Trusted.
--# vim: et sw=4 sts=4
@@ -0,0 +1,11 @@
Cache.SerializerPermissions
TYPE: int
VERSION: 4.3.0
DEFAULT: 0755
--DESCRIPTION--
<p>
Directory permissions of the files and directories created inside
the DefinitionCache/Serializer or other custom serializer path.
</p>
--# vim: et sw=4 sts=4
@@ -0,0 +1,7 @@
HTML.Nofollow
TYPE: bool
VERSION: 4.3.0
DEFAULT: FALSE
--DESCRIPTION--
If enabled, nofollow rel attributes are added to all outgoing links.
--# vim: et sw=4 sts=4
@@ -5,4 +5,5 @@ DEFAULT: false
--DESCRIPTION--
Indicates whether or not the user input is trusted or not. If the input is
trusted, a more expansive set of allowed tags and attributes will be used.
See also %CSS.Trusted.
--# vim: et sw=4 sts=4
@@ -0,0 +1,15 @@
Output.FixInnerHTML
TYPE: bool
VERSION: 4.3.0
DEFAULT: true
--DESCRIPTION--
<p>
If true, HTML Purifier will protect against Internet Explorer's
mishandling of the <code>innerHTML</code> attribute by appending
a space to any attribute that does not contain angled brackets, spaces
or quotes, but contains a backtick. This slightly changes the
semantics of any given attribute, so if this is unacceptable and
you do not use <code>innerHTML</code> on any of your pages, you can
turn this directive off.
</p>
--# vim: et sw=4 sts=4
@@ -12,6 +12,17 @@ abstract class HTMLPurifier_Definition
*/
public $setup = false;
/**
* If true, write out the final definition object to the cache after
* setup. This will be true only if all invocations to get a raw
* definition object are also optimized. This does not cause file
* system thrashing because on subsequent calls the cached object
* is used and any writes to the raw definition object are short
* circuited. See enduser-customize.html for the high-level
* picture.
*/
public $optimized = null;
/**
* What type of definition is it?
*/
@@ -9,14 +9,14 @@ class HTMLPurifier_DefinitionCache_Serializer extends
$file = $this->generateFilePath($config);
if (file_exists($file)) return false;
if (!$this->_prepareDir($config)) return false;
return $this->_write($file, serialize($def));
return $this->_write($file, serialize($def), $config);
}
public function set($def, $config) {
if (!$this->checkDefType($def)) return;
$file = $this->generateFilePath($config);
if (!$this->_prepareDir($config)) return false;
return $this->_write($file, serialize($def));
return $this->_write($file, serialize($def), $config);
}
public function replace($def, $config) {
@@ -24,7 +24,7 @@ class HTMLPurifier_DefinitionCache_Serializer extends
$file = $this->generateFilePath($config);
if (!file_exists($file)) return false;
if (!$this->_prepareDir($config)) return false;
return $this->_write($file, serialize($def));
return $this->_write($file, serialize($def), $config);
}
public function get($config) {
@@ -97,22 +97,33 @@ class HTMLPurifier_DefinitionCache_Serializer extends
* Convenience wrapper function for file_put_contents
* @param $file File name to write to
* @param $data Data to write into file
* @param $config Config object
* @return Number of bytes written if success, or false if failure.
*/
private function _write($file, $data) {
return file_put_contents($file, $data);
private function _write($file, $data, $config) {
$result = file_put_contents($file, $data);
if ($result !== false) {
// set permissions of the new file (no execute)
$chmod = $config->get('Cache.SerializerPermissions');
if (!$chmod) {
$chmod = 0644; // invalid config or simpletest
}
$chmod = $chmod & 0666;
chmod($file, $chmod);
}
return $result;
}
/**
* Prepares the directory that this type stores the serials in
* @param $config Config object
* @return True if successful
*/
private function _prepareDir($config) {
$directory = $this->generateDirectoryPath($config);
//$chmod = $config->get('Cache.SerializerPermissions'); // it would be nice to get this accepted in upstream
global $CFG; $chmod = $CFG->directorypermissions;
$chmod = $config->get('Cache.SerializerPermissions');
if (!$chmod) {
$chmod = 0755;
$chmod = 0755; // invalid config or simpletest
}
if (!is_dir($directory)) {
$base = $this->generateBaseDirectoryPath($config);
@@ -136,6 +147,9 @@ class HTMLPurifier_DefinitionCache_Serializer extends
/**
* Tests permissions on a directory and throws out friendly
* error messages and attempts to chmod it itself if possible
* @param $dir Directory path
* @param $chmod Permissions
* @return True if directory writable
*/
private function _testPermissions($dir, $chmod) {
// early abort, if it is writable, everything is hunky-dory
@@ -152,10 +166,9 @@ class HTMLPurifier_DefinitionCache_Serializer extends
if (fileowner($dir) === posix_getuid()) {
// we can chmod it ourselves
$chmod = $chmod | 0700;
chmod($dir, $chmod);
return true;
if (chmod($dir, $chmod)) return true;
} elseif (filegroup($dir) === posix_getgid()) {
$chmod = $chmod | 0007;
$chmod = $chmod | 0070;
} else {
// PHP's probably running as nobody, so we'll
// need to give global permissions
View File
File diff suppressed because one or more lines are too long
+38 -13
View File
@@ -36,6 +36,11 @@ class HTMLPurifier_Generator
*/
private $_flashCompat;
/**
* Cache of %Output.FixInnerHTML
*/
private $_innerHTMLFix;
/**
* Stack for keeping track of object information when outputting IE
* compatibility code.
@@ -54,6 +59,7 @@ class HTMLPurifier_Generator
public function __construct($config, $context) {
$this->config = $config;
$this->_scriptFix = $config->get('Output.CommentScriptContents');
$this->_innerHTMLFix = $config->get('Output.FixInnerHTML');
$this->_sortAttr = $config->get('Output.SortAttr');
$this->_flashCompat = $config->get('Output.FlashCompat');
$this->_def = $config->getHTMLDefinition();
@@ -132,19 +138,7 @@ class HTMLPurifier_Generator
$_extra = '';
if ($this->_flashCompat) {
if ($token->name == "object" && !empty($this->_flashStack)) {
$flash = array_pop($this->_flashStack);
$compat_token = new HTMLPurifier_Token_Empty("embed");
foreach ($flash->attr as $name => $val) {
if ($name == "classid") continue;
if ($name == "type") continue;
if ($name == "data") $name = "src";
$compat_token->attr[$name] = $val;
}
foreach ($flash->param as $name => $val) {
if ($name == "movie") $name = "src";
$compat_token->attr[$name] = $val;
}
$_extra = "<!--[if IE]>".$this->generateFromToken($compat_token)."<![endif]-->";
// doesn't do anything for now
}
}
return $_extra . '</' . $token->name . '>';
@@ -202,6 +196,37 @@ class HTMLPurifier_Generator
continue;
}
}
// Workaround for Internet Explorer innerHTML bug.
// Essentially, Internet Explorer, when calculating
// innerHTML, omits quotes if there are no instances of
// angled brackets, quotes or spaces. However, when parsing
// HTML (for example, when you assign to innerHTML), it
// treats backticks as quotes. Thus,
// <img alt="``" />
// becomes
// <img alt=`` />
// becomes
// <img alt='' />
// Fortunately, all we need to do is trigger an appropriate
// quoting style, which we do by adding an extra space.
// This also is consistent with the W3C spec, which states
// that user agents may ignore leading or trailing
// whitespace (in fact, most don't, at least for attributes
// like alt, but an extra space at the end is barely
// noticeable). Still, we have a configuration knob for
// this, since this transformation is not necesary if you
// don't process user input with innerHTML or you don't plan
// on supporting Internet Explorer.
if ($this->_innerHTMLFix) {
if (strpos($value, '`') !== false) {
// check if correct quoting style would not already be
// triggered
if (strcspn($value, '"\' <>') === strlen($value)) {
// protect!
$value .= ' ';
}
}
}
$html .= $key.'="'.$this->escape($value).'" ';
}
return rtrim($html);
@@ -0,0 +1,19 @@
<?php
/**
* Module adds the nofollow attribute transformation to a tags. It
* is enabled by HTML.Nofollow
*/
class HTMLPurifier_HTMLModule_Nofollow extends HTMLPurifier_HTMLModule
{
public $name = 'Nofollow';
public function setup($config) {
$a = $this->addBlankElement('a');
$a->attr_transform_post[] = new HTMLPurifier_AttrTransform_Nofollow();
}
}
// vim: et sw=4 sts=4
@@ -21,7 +21,7 @@ class HTMLPurifier_HTMLModule_SafeEmbed extends HTMLPurifier_HTMLModule
'allowscriptaccess' => 'Enum#never',
'allownetworking' => 'Enum#internal',
'flashvars' => 'Text',
'wmode' => 'Enum#window',
'wmode' => 'Enum#window,transparent,opaque',
'name' => 'ID',
)
);
@@ -29,7 +29,6 @@ class HTMLPurifier_HTMLModule_SafeObject extends HTMLPurifier_HTMLModule
'width' => 'Pixels#' . $max,
'height' => 'Pixels#' . $max,
'data' => 'URI#embedded',
'classid' => 'Enum#clsid:d27cdb6e-ae6d-11cf-96b8-444553540000',
'codebase' => new HTMLPurifier_AttrDef_Enum(array(
'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0')),
)
@@ -216,19 +216,19 @@ class HTMLPurifier_HTMLModuleManager
}
}
// add proprietary module (this gets special treatment because
// it is completely removed from doctypes, etc.)
// custom modules
if ($config->get('HTML.Proprietary')) {
$modules[] = 'Proprietary';
}
// add SafeObject/Safeembed modules
if ($config->get('HTML.SafeObject')) {
$modules[] = 'SafeObject';
}
if ($config->get('HTML.SafeEmbed')) {
$modules[] = 'SafeEmbed';
}
if ($config->get('HTML.Nofollow')) {
$modules[] = 'Nofollow';
}
// merge in custom modules
$modules = array_merge($modules, $this->userModules);
+3 -3
View File
@@ -235,7 +235,7 @@ class HTMLPurifier_Lexer
*/
protected static function removeIEConditional($string) {
return preg_replace(
'#<!--\[if [^>]+\]>.*<!\[endif\]-->#si', // probably should generalize for all strings
'#<!--\[if [^>]+\]>.*?<!\[endif\]-->#si', // probably should generalize for all strings
'',
$string
);
@@ -273,11 +273,11 @@ class HTMLPurifier_Lexer
$html = $this->escapeCommentedCDATA($html);
}
$html = $this->removeIEConditional($html);
// escape CDATA
$html = $this->escapeCDATA($html);
$html = $this->removeIEConditional($html);
// extract body from document if applicable
if ($config->get('Core.ConvertDocumentToFragment')) {
$e = false;
+53 -23
View File
@@ -72,23 +72,57 @@ class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer
}
/**
* Recursive function that tokenizes a node, putting it into an accumulator.
*
* Iterative function that tokenizes a node, putting it into an accumulator.
* To iterate is human, to recurse divine - L. Peter Deutsch
* @param $node DOMNode to be tokenized.
* @param $tokens Array-list of already tokenized tokens.
* @param $collect Says whether or start and close are collected, set to
* false at first recursion because it's the implicit DIV
* tag you're dealing with.
* @returns Tokens of node appended to previously passed tokens.
*/
protected function tokenizeDOM($node, &$tokens, $collect = false) {
protected function tokenizeDOM($node, &$tokens) {
$level = 0;
$nodes = array($level => array($node));
$closingNodes = array();
do {
while (!empty($nodes[$level])) {
$node = array_shift($nodes[$level]); // FIFO
$collect = $level > 0 ? true : false;
$needEndingTag = $this->createStartNode($node, $tokens, $collect);
if ($needEndingTag) {
$closingNodes[$level][] = $node;
}
if ($node->childNodes && $node->childNodes->length) {
$level++;
$nodes[$level] = array();
foreach ($node->childNodes as $childNode) {
array_push($nodes[$level], $childNode);
}
}
}
$level--;
if ($level && isset($closingNodes[$level])) {
while($node = array_pop($closingNodes[$level])) {
$this->createEndNode($node, $tokens);
}
}
} while ($level > 0);
}
/**
* @param $node DOMNode to be tokenized.
* @param $tokens Array-list of already tokenized tokens.
* @param $collect Says whether or start and close are collected, set to
* false at first recursion because it's the implicit DIV
* tag you're dealing with.
* @returns bool if the token needs an endtoken
*/
protected function createStartNode($node, &$tokens, $collect) {
// intercept non element nodes. WE MUST catch all of them,
// but we're not getting the character reference nodes because
// those should have been preprocessed
if ($node->nodeType === XML_TEXT_NODE) {
$tokens[] = $this->factory->createText($node->data);
return;
return false;
} elseif ($node->nodeType === XML_CDATA_SECTION_NODE) {
// undo libxml's special treatment of <script> and <style> tags
$last = end($tokens);
@@ -106,48 +140,44 @@ class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer
}
}
$tokens[] = $this->factory->createText($this->parseData($data));
return;
return false;
} elseif ($node->nodeType === XML_COMMENT_NODE) {
// this is code is only invoked for comments in script/style in versions
// of libxml pre-2.6.28 (regular comments, of course, are still
// handled regularly)
$tokens[] = $this->factory->createComment($node->data);
return;
return false;
} elseif (
// not-well tested: there may be other nodes we have to grab
$node->nodeType !== XML_ELEMENT_NODE
) {
return;
return false;
}
$attr = $node->hasAttributes() ?
$this->transformAttrToAssoc($node->attributes) :
array();
$attr = $node->hasAttributes() ? $this->transformAttrToAssoc($node->attributes) : array();
// We still have to make sure that the element actually IS empty
if (!$node->childNodes->length) {
if ($collect) {
$tokens[] = $this->factory->createEmpty($node->tagName, $attr);
}
return false;
} else {
if ($collect) { // don't wrap on first iteration
if ($collect) {
$tokens[] = $this->factory->createStart(
$tag_name = $node->tagName, // somehow, it get's dropped
$attr
);
}
foreach ($node->childNodes as $node) {
// remember, it's an accumulator. Otherwise, we'd have
// to use array_merge
$this->tokenizeDOM($node, $tokens, true);
}
if ($collect) {
$tokens[] = $this->factory->createEnd($tag_name);
}
return true;
}
}
protected function createEndNode($node, &$tokens) {
$tokens[] = $this->factory->createEnd($node->tagName);
}
/**
* Converts a DOMNamedNodeMap of DOMAttr objects into an assoc array.
*
@@ -2,6 +2,14 @@
/**
* Takes tokens makes them well-formed (balance end tags, etc.)
*
* Specification of the armor attributes this strategy uses:
*
* - MakeWellFormed_TagClosedError: This armor field is used to
* suppress tag closed errors for certain tokens [TagClosedSuppress],
* in particular, if a tag was generated automatically by HTML
* Purifier, we may rely on our infrastructure to close it for us
* and shouldn't report an error to the user [TagClosedAuto].
*/
class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
{
@@ -43,6 +51,12 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
// local variables
$generator = new HTMLPurifier_Generator($config, $context);
$escape_invalid_tags = $config->get('Core.EscapeInvalidTags');
// used for autoclose early abortion
$global_parent_allowed_elements = array();
if (isset($definition->info[$definition->info_parent])) {
// may be unset under testing circumstances
$global_parent_allowed_elements = $definition->info[$definition->info_parent]->child->getAllowedElements($config);
}
$e = $context->get('ErrorCollector', true);
$t = false; // token index
$i = false; // injector index
@@ -102,7 +116,7 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
// -- end INJECTOR --
// a note on punting:
// a note on reprocessing:
// In order to reduce code duplication, whenever some code needs
// to make HTML changes in order to make things "correct", the
// new HTML gets sent through the purifier, regardless of its
@@ -149,7 +163,7 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
$top_nesting = array_pop($this->stack);
$this->stack[] = $top_nesting;
// send error
// send error [TagClosedSuppress]
if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) {
$e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting);
}
@@ -193,12 +207,12 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
$ok = false;
if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) {
// claims to be a start tag but is empty
$token = new HTMLPurifier_Token_Empty($token->name, $token->attr);
$token = new HTMLPurifier_Token_Empty($token->name, $token->attr, $token->line, $token->col, $token->armor);
$ok = true;
} elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) {
// claims to be empty but really is a start tag
$this->swap(new HTMLPurifier_Token_End($token->name));
$this->insertBefore(new HTMLPurifier_Token_Start($token->name, $token->attr));
$this->insertBefore(new HTMLPurifier_Token_Start($token->name, $token->attr, $token->line, $token->col, $token->armor));
// punt (since we had to modify the input stream in a non-trivial way)
$reprocess = true;
continue;
@@ -211,6 +225,19 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
// ...unless they also have to close their parent
if (!empty($this->stack)) {
// Performance note: you might think that it's rather
// inefficient, recalculating the autoclose information
// for every tag that a token closes (since when we
// do an autoclose, we push a new token into the
// stream and then /process/ that, before
// re-processing this token.) But this is
// necessary, because an injector can make an
// arbitrary transformations to the autoclosing
// tokens we introduce, so things may have changed
// in the meantime. Also, doing the inefficient thing is
// "easy" to reason about (for certain perverse definitions
// of "easy")
$parent = array_pop($this->stack);
$this->stack[] = $parent;
@@ -243,24 +270,51 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
}
if ($autoclose) {
// errors need to be updated
$new_token = new HTMLPurifier_Token_End($parent->name);
$new_token->start = $parent;
if ($carryover) {
$element = clone $parent;
$element->armor['MakeWellFormed_TagClosedError'] = true;
$element->carryover = true;
$this->processToken(array($new_token, $token, $element));
} else {
$this->insertBefore($new_token);
}
if ($e && !isset($parent->armor['MakeWellFormed_TagClosedError'])) {
if (!$carryover) {
$e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent);
} else {
$e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag carryover', $parent);
// check if this autoclose is doomed to fail
// (this rechecks $parent, which his harmless)
$autoclose_ok = isset($global_parent_allowed_elements[$token->name]);
if (!$autoclose_ok) {
foreach ($this->stack as $ancestor) {
$elements = $definition->info[$ancestor->name]->child->getAllowedElements($config);
if (isset($elements[$token->name])) {
$autoclose_ok = true;
break;
}
if ($definition->info[$token->name]->wrap) {
$wrapname = $definition->info[$token->name]->wrap;
$wrapdef = $definition->info[$wrapname];
$wrap_elements = $wrapdef->child->getAllowedElements($config);
if (isset($wrap_elements[$token->name]) && isset($elements[$wrapname])) {
$autoclose_ok = true;
break;
}
}
}
}
if ($autoclose_ok) {
// errors need to be updated
$new_token = new HTMLPurifier_Token_End($parent->name);
$new_token->start = $parent;
if ($carryover) {
$element = clone $parent;
// [TagClosedAuto]
$element->armor['MakeWellFormed_TagClosedError'] = true;
$element->carryover = true;
$this->processToken(array($new_token, $token, $element));
} else {
$this->insertBefore($new_token);
}
// [TagClosedSuppress]
if ($e && !isset($parent->armor['MakeWellFormed_TagClosedError'])) {
if (!$carryover) {
$e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent);
} else {
$e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag carryover', $parent);
}
}
} else {
$this->remove();
}
$reprocess = true;
continue;
}
@@ -366,7 +420,7 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
if ($e) {
for ($j = $c - 1; $j > 0; $j--) {
// notice we exclude $j == 0, i.e. the current ending tag, from
// the errors...
// the errors... [TagClosedSuppress]
if (!isset($skipped_tags[$j]->armor['MakeWellFormed_TagClosedError'])) {
$e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$j]);
}
@@ -381,6 +435,7 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
$new_token->start = $skipped_tags[$j];
array_unshift($replace, $new_token);
if (isset($definition->info[$new_token->name]) && $definition->info[$new_token->name]->formatting) {
// [TagClosedAuto]
$element = clone $skipped_tags[$j];
$element->carryover = true;
$element->armor['MakeWellFormed_TagClosedError'] = true;
@@ -449,7 +504,8 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
}
/**
* Inserts a token before the current token. Cursor now points to this token
* Inserts a token before the current token. Cursor now points to
* this token. You must reprocess after this.
*/
private function insertBefore($token) {
array_splice($this->tokens, $this->t, 0, array($token));
@@ -457,14 +513,15 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
/**
* Removes current token. Cursor now points to new token occupying previously
* occupied space.
* occupied space. You must reprocess after this.
*/
private function remove() {
array_splice($this->tokens, $this->t, 1);
}
/**
* Swap current token with new token. Cursor points to new token (no change).
* Swap current token with new token. Cursor points to new token (no
* change). You must reprocess after this.
*/
private function swap($token) {
$this->tokens[$this->t] = $token;
@@ -63,13 +63,15 @@ class HTMLPurifier_TagTransform_Font extends HTMLPurifier_TagTransform
// handle size transform
if (isset($attr['size'])) {
// normalize large numbers
if ($attr['size']{0} == '+' || $attr['size']{0} == '-') {
$size = (int) $attr['size'];
if ($size < -2) $attr['size'] = '-2';
if ($size > 4) $attr['size'] = '+4';
} else {
$size = (int) $attr['size'];
if ($size > 7) $attr['size'] = '7';
if ($attr['size'] !== '') {
if ($attr['size']{0} == '+' || $attr['size']{0} == '-') {
$size = (int) $attr['size'];
if ($size < -2) $attr['size'] = '-2';
if ($size > 4) $attr['size'] = '+4';
} else {
$size = (int) $attr['size'];
if ($size > 7) $attr['size'] = '7';
}
}
if (isset($this->_size_lookup[$attr['size']])) {
$prepend_style .= 'font-size:' .
+2 -1
View File
@@ -33,7 +33,7 @@ class HTMLPurifier_Token_Tag extends HTMLPurifier_Token
* @param $name String name.
* @param $attr Associative array of attributes.
*/
public function __construct($name, $attr = array(), $line = null, $col = null) {
public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) {
$this->name = ctype_lower($name) ? $name : strtolower($name);
foreach ($attr as $key => $value) {
// normalization only necessary when key is not lowercase
@@ -50,6 +50,7 @@ class HTMLPurifier_Token_Tag extends HTMLPurifier_Token
$this->attr = $attr;
$this->line = $line;
$this->col = $col;
$this->armor = $armor;
}
}
+62 -31
View File
@@ -67,14 +67,6 @@ class HTMLPurifier_URI
$chars_gen_delims = ':/?#[]@';
$chars_pchar = $chars_sub_delims . ':@';
// validate scheme (MUST BE FIRST!)
if (!is_null($this->scheme) && is_null($this->host)) {
$def = $config->getDefinition('URI');
if ($def->defaultScheme === $this->scheme) {
$this->scheme = null;
}
}
// validate host
if (!is_null($this->host)) {
$host_def = new HTMLPurifier_AttrDef_URI_Host();
@@ -82,6 +74,21 @@ class HTMLPurifier_URI
if ($this->host === false) $this->host = null;
}
// validate scheme
// NOTE: It's not appropriate to check whether or not this
// scheme is in our registry, since a URIFilter may convert a
// URI that we don't allow into one we do. So instead, we just
// check if the scheme can be dropped because there is no host
// and it is our default scheme.
if (!is_null($this->scheme) && is_null($this->host) || $this->host === '') {
// support for relative paths is pretty abysmal when the
// scheme is present, so axe it when possible
$def = $config->getDefinition('URI');
if ($def->defaultScheme === $this->scheme) {
$this->scheme = null;
}
}
// validate username
if (!is_null($this->userinfo)) {
$encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . ':');
@@ -96,32 +103,48 @@ class HTMLPurifier_URI
// validate path
$path_parts = array();
$segments_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/');
if (!is_null($this->host)) {
if (!is_null($this->host)) { // this catches $this->host === ''
// path-abempty (hier and relative)
// http://www.example.com/my/path
// //www.example.com/my/path (looks odd, but works, and
// recognized by most browsers)
// (this set is valid or invalid on a scheme by scheme
// basis, so we'll deal with it later)
// file:///my/path
// ///my/path
$this->path = $segments_encoder->encode($this->path);
} elseif ($this->path !== '' && $this->path[0] === '/') {
// path-absolute (hier and relative)
if (strlen($this->path) >= 2 && $this->path[1] === '/') {
// This shouldn't ever happen!
$this->path = '';
} else {
} elseif ($this->path !== '') {
if ($this->path[0] === '/') {
// path-absolute (hier and relative)
// http:/my/path
// /my/path
if (strlen($this->path) >= 2 && $this->path[1] === '/') {
// This could happen if both the host gets stripped
// out
// http://my/path
// //my/path
$this->path = '';
} else {
$this->path = $segments_encoder->encode($this->path);
}
} elseif (!is_null($this->scheme)) {
// path-rootless (hier)
// http:my/path
// Short circuit evaluation means we don't need to check nz
$this->path = $segments_encoder->encode($this->path);
}
} elseif (!is_null($this->scheme) && $this->path !== '') {
// path-rootless (hier)
// Short circuit evaluation means we don't need to check nz
$this->path = $segments_encoder->encode($this->path);
} elseif (is_null($this->scheme) && $this->path !== '') {
// path-noscheme (relative)
// (once again, not checking nz)
$segment_nc_encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . '@');
$c = strpos($this->path, '/');
if ($c !== false) {
$this->path =
$segment_nc_encoder->encode(substr($this->path, 0, $c)) .
$segments_encoder->encode(substr($this->path, $c));
} else {
$this->path = $segment_nc_encoder->encode($this->path);
// path-noscheme (relative)
// my/path
// (once again, not checking nz)
$segment_nc_encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . '@');
$c = strpos($this->path, '/');
if ($c !== false) {
$this->path =
$segment_nc_encoder->encode(substr($this->path, 0, $c)) .
$segments_encoder->encode(substr($this->path, $c));
} else {
$this->path = $segment_nc_encoder->encode($this->path);
}
}
} else {
// path-empty (hier and relative)
@@ -150,6 +173,9 @@ class HTMLPurifier_URI
public function toString() {
// reconstruct authority
$authority = null;
// there is a rendering difference between a null authority
// (http:foo-bar) and an empty string authority
// (http:///foo-bar).
if (!is_null($this->host)) {
$authority = '';
if(!is_null($this->userinfo)) $authority .= $this->userinfo . '@';
@@ -157,7 +183,12 @@ class HTMLPurifier_URI
if(!is_null($this->port)) $authority .= ':' . $this->port;
}
// reconstruct the result
// Reconstruct the result
// One might wonder about parsing quirks from browsers after
// this reconstruction. Unfortunately, parsing behavior depends
// on what *scheme* was employed (file:///foo is handled *very*
// differently than http:///foo), so unfortunately we have to
// defer to the schemes to do the right thing.
$result = '';
if (!is_null($this->scheme)) $result .= $this->scheme . ':';
if (!is_null($authority)) $result .= '//' . $authority;
+54 -7
View File
@@ -3,11 +3,13 @@
/**
* Validator for the components of a URI for a specific scheme
*/
class HTMLPurifier_URIScheme
abstract class HTMLPurifier_URIScheme
{
/**
* Scheme's default port (integer)
* Scheme's default port (integer). If an explicit port number is
* specified that coincides with the default port, it will be
* elided.
*/
public $default_port = null;
@@ -24,17 +26,62 @@ class HTMLPurifier_URIScheme
public $hierarchical = false;
/**
* Validates the components of a URI
* @note This implementation should be called by children if they define
* a default port, as it does port processing.
* @param $uri Instance of HTMLPurifier_URI
* Whether or not the URI may omit a hostname when the scheme is
* explicitly specified, ala file:///path/to/file. As of writing,
* 'file' is the only scheme that browsers support his properly.
*/
public $may_omit_host = false;
/**
* Validates the components of a URI for a specific scheme.
* @param $uri Reference to a HTMLPurifier_URI object
* @param $config HTMLPurifier_Config object
* @param $context HTMLPurifier_Context object
* @return Bool success or failure
*/
public abstract function doValidate(&$uri, $config, $context);
/**
* Public interface for validating components of a URI. Performs a
* bunch of default actions. Don't overload this method.
* @param $uri Reference to a HTMLPurifier_URI object
* @param $config HTMLPurifier_Config object
* @param $context HTMLPurifier_Context object
* @return Bool success or failure
*/
public function validate(&$uri, $config, $context) {
if ($this->default_port == $uri->port) $uri->port = null;
return true;
// kludge: browsers do funny things when the scheme but not the
// authority is set
if (!$this->may_omit_host &&
// if the scheme is present, a missing host is always in error
(!is_null($uri->scheme) && ($uri->host === '' || is_null($uri->host))) ||
// if the scheme is not present, a *blank* host is in error,
// since this translates into '///path' which most browsers
// interpret as being 'http://path'.
(is_null($uri->scheme) && $uri->host === '')
) {
do {
if (is_null($uri->scheme)) {
if (substr($uri->path, 0, 2) != '//') {
$uri->host = null;
break;
}
// URI is '////path', so we cannot nullify the
// host to preserve semantics. Try expanding the
// hostname instead (fall through)
}
// first see if we can manually insert a hostname
$host = $config->get('URI.Host');
if (!is_null($host)) {
$uri->host = $host;
} else {
// we can't do anything sensible, reject the URL.
return false;
}
} while (false);
}
return $this->doValidate($uri, $config, $context);
}
}
@@ -13,8 +13,11 @@ class HTMLPurifier_URIScheme_data extends HTMLPurifier_URIScheme {
'image/gif' => true,
'image/png' => true,
);
// this is actually irrelevant since we only write out the path
// component
public $may_omit_host = true;
public function validate(&$uri, $config, $context) {
public function doValidate(&$uri, $config, $context) {
$result = explode(',', $uri->path, 2);
$is_base64 = false;
$charset = null;
@@ -9,8 +9,14 @@ class HTMLPurifier_URIScheme_file extends HTMLPurifier_URIScheme {
// machines, so placing them as an img src is incorrect.
public $browsable = false;
public function validate(&$uri, $config, $context) {
parent::validate($uri, $config, $context);
// Basically the *only* URI scheme for which this is true, since
// accessing files on the local machine is very common. In fact,
// browsers on some operating systems don't understand the
// authority, though I hear it is used on Windows to refer to
// network shares.
public $may_omit_host = true;
public function doValidate(&$uri, $config, $context) {
// Authentication method is not supported
$uri->userinfo = null;
// file:// makes no provisions for accessing the resource
@@ -9,8 +9,7 @@ class HTMLPurifier_URIScheme_ftp extends HTMLPurifier_URIScheme {
public $browsable = true; // usually
public $hierarchical = true;
public function validate(&$uri, $config, $context) {
parent::validate($uri, $config, $context);
public function doValidate(&$uri, $config, $context) {
$uri->query = null;
// typecode check
@@ -9,8 +9,7 @@ class HTMLPurifier_URIScheme_http extends HTMLPurifier_URIScheme {
public $browsable = true;
public $hierarchical = true;
public function validate(&$uri, $config, $context) {
parent::validate($uri, $config, $context);
public function doValidate(&$uri, $config, $context) {
$uri->userinfo = null;
return true;
}
@@ -12,9 +12,9 @@
class HTMLPurifier_URIScheme_mailto extends HTMLPurifier_URIScheme {
public $browsable = false;
public $may_omit_host = true;
public function validate(&$uri, $config, $context) {
parent::validate($uri, $config, $context);
public function doValidate(&$uri, $config, $context) {
$uri->userinfo = null;
$uri->host = null;
$uri->port = null;
@@ -6,9 +6,9 @@
class HTMLPurifier_URIScheme_news extends HTMLPurifier_URIScheme {
public $browsable = false;
public $may_omit_host = true;
public function validate(&$uri, $config, $context) {
parent::validate($uri, $config, $context);
public function doValidate(&$uri, $config, $context) {
$uri->userinfo = null;
$uri->host = null;
$uri->port = null;
@@ -8,8 +8,7 @@ class HTMLPurifier_URIScheme_nntp extends HTMLPurifier_URIScheme {
public $default_port = 119;
public $browsable = false;
public function validate(&$uri, $config, $context) {
parent::validate($uri, $config, $context);
public function doValidate(&$uri, $config, $context) {
$uri->userinfo = null;
$uri->query = null;
return true;
+2 -3
View File
@@ -1,6 +1,5 @@
Description of HTML Purifier v4.2.0 library import into Moodle
Description of HTML Purifier v4.3.0 library import into Moodle
Changes:
* DefinitionCache/Serializer.php - using our directory permissions
No Changes..
skodak
+1 -1
View File
@@ -529,7 +529,7 @@ function install_cli_database(array $options, $interactive) {
}
// test environment first
if (!check_moodle_environment($version, $environment_results, false, ENV_SELECT_RELEASE)) {
if (!check_moodle_environment(normalize_version($release), $environment_results, false, ENV_SELECT_RELEASE)) {
$errors = environment_get_errors($environment_results);
cli_heading(get_string('environment', 'admin'));
foreach ($errors as $error) {
+4
View File
@@ -772,6 +772,10 @@ function setup_get_remote_url() {
//LiteSpeed - not officially supported
$rurl['fullpath'] = $_SERVER['REQUEST_URI']; // TODO: verify this is always properly encoded
} else if ($_SERVER['SERVER_SOFTWARE'] === 'HTTPD') {
//obscure name found on some servers - this is definitely not supported
$rurl['fullpath'] = $_SERVER['REQUEST_URI']; // TODO: verify this is always properly encoded
} else {
throw new moodle_exception('unsupportedwebserver', 'error', '', $_SERVER['SERVER_SOFTWARE']);
}
+133
View File
@@ -0,0 +1,133 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Unit tests for the HTMLPurifier integration
*
* @package core
* @subpackage simpletest
* @copyright 2011 Petr Skoda (http://skodak.org)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
class purifier_test extends UnitTestCase {
public static $includecoverage = array('lib/htmlpurifier/HTMLPurifier.php');
private $enablehtmlpurifier = null;
private $cachetext = null;
function setUp() {
global $CFG;
$this->enablehtmlpurifier = $CFG->enablehtmlpurifier;
$CFG->enablehtmlpurifier = 1;
$this->cachetext = $CFG->cachetext;
$CFG->cachetext = 0;
}
function tearDown() {
global $CFG;
$CFG->enablehtmlpurifier = $this->enablehtmlpurifier;
$CFG->cachetext = $this->cachetext;
}
/**
* Verify _blank target is allowed
* @return void
*/
public function test_allow_blank_target() {
$text = '<a href="http://moodle.org" target="_blank">Some link</a>';
$result = format_text($text, FORMAT_HTML);
$this->assertIdentical($text, $result);
$result = format_text('<a href="http://moodle.org" target="some">Some link</a>', FORMAT_HTML);
$this->assertIdentical('<a href="http://moodle.org">Some link</a>', $result);
}
/**
* Verify our nolink tag accepted
* @return void
*/
public function test_nolink() {
// we can not use format text because nolink changes result
$text = '<nolink><div>no filters</div></nolink>';
$result = purify_html($text, array());
$this->assertIdentical($text, $result);
}
/**
* Verify our tex tag accepted
* @return void
*/
public function test_tex() {
$text = '<tex>a+b=c</tex>';
$result = purify_html($text, array());
$this->assertIdentical($text, $result);
}
/**
* Verify our algebra tag accepted
* @return void
*/
public function test_algebra() {
$text = '<algebra>a+b=c</algebra>';
$result = purify_html($text, array());
$this->assertIdentical($text, $result);
}
/**
* Verify our hacky multilang works
* @return void
*/
public function test_multilang() {
$text = '<lang lang="en">hmmm</lang><lang lang="anything">hm</lang>';
$result = purify_html($text, array());
$this->assertIdentical($text, $result);
$text = '<span lang="en" class="multilang">hmmm</span><span lang="anything" class="multilang">hm</span>';
$result = purify_html($text, array());
$this->assertIdentical($text, $result);
$text = '<span lang="en">hmmm</span>';
$result = purify_html($text, array());
$this->assertNotIdentical($text, $result);
}
/**
* Tests the 'allowid' option for format_text.
*/
public function test_format_text_allowid() {
// Start off by not allowing ids (default)
$options = array(
'nocache' => true
);
$result = format_text('<div id="example">Frog</div>', FORMAT_HTML, $options);
$this->assertIdentical('<div>Frog</div>', $result);
// Now allow ids
$options['allowid'] = true;
$result = format_text('<div id="example">Frog</div>', FORMAT_HTML, $options);
$this->assertIdentical('<div id="example">Frog</div>', $result);
}
//TODO: add XSS smoke tests here
}
-25
View File
@@ -144,31 +144,6 @@ class web_test extends UnitTestCase {
$this->assertEqual('lala xx', clean_text($text, FORMAT_HTML));
}
/**
* Tests the 'allowid' option for format_text.
*/
public function test_format_text_allowid() {
global $CFG;
// This test relates only to html purifier, so switch to it if not in use
$oldcfg = $CFG->enablehtmlpurifier;
$CFG->enablehtmlpurifier = true;
// Start off by not allowing ids (default)
$options = array(
'nocache' => true
);
$result = format_text('<div id="example">Frog</div>', FORMAT_HTML, $options);
$this->assertEqual('<div>Frog</div>', $result);
// Now allow ids
$options['allowid'] = true;
$result = format_text('<div id="example">Frog</div>', FORMAT_HTML, $options);
$this->assertEqual('<div id="example">Frog</div>', $result);
// Switch config back
$CFG->enablehtmlpurifier = $oldcfg;
}
}
+1 -1
View File
@@ -67,7 +67,7 @@
<location>htmlpurifier</location>
<name>HTML Purifier</name>
<license>LGPL</license>
<version>4.2.0</version>
<version>4.3.0</version>
<licenseversion>2.1+</licenseversion>
</library>
<library>
+14 -12
View File
@@ -1509,20 +1509,21 @@ function clean_text($text, $format = FORMAT_HTML, $options = array()) {
function purify_html($text, $options = array()) {
global $CFG;
// this can not be done only once because we sometimes need to reset the cache
$cachedir = $CFG->dataroot.'/cache/htmlpurifier';
check_dir_exists($cachedir);
$type = !empty($options['allowid']) ? 'allowid' : 'normal';
static $purifiers = array();
if (empty($purifiers[$type])) {
// make sure the serializer dir exists, it should be fine if it disappears later during cache reset
$cachedir = $CFG->dataroot.'/cache/htmlpurifier';
check_dir_exists($cachedir);
require_once $CFG->libdir.'/htmlpurifier/HTMLPurifier.safe-includes.php';
$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.DefinitionID', 'moodlehtml');
$config->set('HTML.DefinitionRev', 1);
$config->set('HTML.DefinitionRev', 2);
$config->set('Cache.SerializerPath', $cachedir);
//$config->set('Cache.SerializerPermission', $CFG->directorypermissions); // it would be nice to get this upstream
$config->set('Cache.SerializerPermissions', $CFG->directorypermissions);
$config->set('Core.NormalizeNewlines', false);
$config->set('Core.ConvertDocumentToFragment', true);
$config->set('Core.Encoding', 'UTF-8');
@@ -1540,12 +1541,13 @@ function purify_html($text, $options = array()) {
$config->set('Attr.EnableID', true);
}
$def = $config->getHTMLDefinition(true);
$def->addElement('nolink', 'Block', 'Flow', array()); // skip our filters inside
$def->addElement('tex', 'Inline', 'Inline', array()); // tex syntax, equivalent to $$xx$$
$def->addElement('algebra', 'Inline', 'Inline', array()); // algebra syntax, equivalent to @@xx@@
$def->addElement('lang', 'Block', 'Flow', array(), array('lang'=>'CDATA')); // old anf future style multilang - only our hacked lang attribute
$def->addAttribute('span', 'xxxlang', 'CDATA'); // current problematic multilang
if ($def = $config->maybeGetRawHTMLDefinition()) {
$def->addElement('nolink', 'Block', 'Flow', array()); // skip our filters inside
$def->addElement('tex', 'Inline', 'Inline', array()); // tex syntax, equivalent to $$xx$$
$def->addElement('algebra', 'Inline', 'Inline', array()); // algebra syntax, equivalent to @@xx@@
$def->addElement('lang', 'Block', 'Flow', array(), array('lang'=>'CDATA')); // old and future style multilang - only our hacked lang attribute
$def->addAttribute('span', 'xxxlang', 'CDATA'); // current problematic multilang
}
$purifier = new HTMLPurifier($config);
$purifiers[$type] = $purifier;
+3 -11
View File
@@ -464,18 +464,10 @@ class xhprof_table_sql extends table_sql {
protected $listurlmode = false;
/**
* Overwrite this method to be able to inject extra class to
* some (reference rows). Original API doesn't seem to allow that
* Get row classes to be applied based on row contents
*/
function build_table(){
if ($this->rawdata){
foreach($this->rawdata as $row){
$formattedrow = $this->format_row($row);
// reference row, add 'referencerun' class
$classname = $row->runreference ? 'referencerun' : '';
$this->add_data_keyed($formattedrow, $classname);
}
}
function get_row_class($row) {
return $row->runreference ? 'referencerun' : ''; // apply class to reference runs
}
/**
+11 -13
View File
@@ -46,21 +46,19 @@ class message_output_email extends message_output {
return true;
}
//hold onto email preference because /admin/cron.php sends a lot of messages at once
static $useremailaddresses = array();
//the user the email is going to
$recipient = null;
//check user preference for where user wants email sent
if (!array_key_exists($eventdata->userto->id, $useremailaddresses)) {
$useremailaddresses[$eventdata->userto->id] = get_user_preferences('message_processor_email_email', $eventdata->userto->email, $eventdata->userto->id);
//check if the recipient has a different email address specified in their messaging preferences Vs their user profile
$emailmessagingpreference = get_user_preferences('message_processor_email_email', null, $eventdata->userto);
if (!empty($emailmessagingpreference)) {
//clone to avoid altering the actual user object
$recipient = clone($eventdata->userto);
$recipient->email = $emailmessagingpreference;
} else {
$recipient = $eventdata->userto;
}
$usertoemailaddress = $useremailaddresses[$eventdata->userto->id];
if ( !empty($usertoemailaddress)) {
$userto->email = $usertoemailaddress;
}
$result = email_to_user($eventdata->userto, $eventdata->userfrom,
$eventdata->subject, $eventdata->fullmessage, $eventdata->fullmessagehtml);
$result = email_to_user($recipient, $eventdata->userfrom, $eventdata->subject, $eventdata->fullmessage, $eventdata->fullmessagehtml);
return $result;
}
+1 -1
View File
@@ -114,7 +114,7 @@ class mod_assignment_mod_form extends moodleform_mod {
}
function validataion($data, $files) {
function validation($data, $files) {
// Allow plugin assignment types to do any extra validation after the form has been submitted
$errors = parent::validation($data, $files);
$errors = array_merge($errors, $this->get_assignment_instance()->form_validation($data, $files));
+4 -3
View File
@@ -79,6 +79,10 @@ $strnextsession = get_string('nextsession', 'chat');
$title = $course->shortname . ': ' . format_string($chat->name);
// Mark viewed by user (if required)
$completion = new completion_info($course);
$completion->set_module_viewed($cm);
// Initialize $PAGE
$PAGE->set_url('/mod/chat/view.php', array('id' => $cm->id));
$PAGE->set_title($title);
@@ -178,7 +182,4 @@ if ($chatusers = chat_get_users($chat->id, $currentgroup, $cm->groupingid)) {
echo $OUTPUT->box_end();
}
$completion = new completion_info($course);
$completion->set_module_viewed($cm);
echo $OUTPUT->footer();
+4 -5
View File
@@ -50,6 +50,10 @@
$PAGE->set_title(format_string($choice->name));
$PAGE->set_heading($course->fullname);
// Mark viewed by user (if required)
$completion = new completion_info($course);
$completion->set_module_viewed($cm);
/// Submit any new data if there is any
if (data_submitted() && is_enrolled($context, NULL, 'mod/choice:choose') && confirm_sesskey()) {
$timenow = time();
@@ -168,8 +172,3 @@
}
echo $OUTPUT->footer();
/// Mark as viewed
$completion=new completion_info($course);
$completion->set_module_viewed($cm);
+1
View File
@@ -90,6 +90,7 @@ if ($mode == 'singletemplate') {
$PAGE->requires->js('/mod/data/data.js');
$PAGE->set_title($data->name);
$PAGE->set_heading($course->fullname);
$PAGE->set_pagelayout('report');
echo $OUTPUT->header();
echo $OUTPUT->heading(format_string($data->name));
+4 -5
View File
@@ -289,6 +289,10 @@
$PAGE->requires->js('/mod/data/js.php?d='.$data->id, true);
}
// Mark as viewed
$completion = new completion_info($course);
$completion->set_module_viewed($cm);
/// Print the page header
// Note: MDL-19010 there will be further changes to printing header and blocks.
// The code will be much nicer than this eventually.
@@ -730,11 +734,6 @@ if ($showactivity) {
if (($maxcount || $mode == 'asearch') && $mode != 'single') {
data_print_preference_form($data, $perpage, $search, $sort, $order, $search_array, $advanced, $mode);
}
/// Mark as viewed
$completion=new completion_info($course);
$completion->set_module_viewed($cm);
}
echo $OUTPUT->footer();
+4 -4
View File
@@ -121,6 +121,10 @@
print_error('error');
}
// Mark activity viewed for completion-tracking
$completion = new completion_info($course);
$completion->set_module_viewed($cm);
/// Print the page header
$strfeedbacks = get_string("modulenameplural", "feedback");
$strfeedback = get_string("modulename", "feedback");
@@ -502,9 +506,5 @@
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
// Mark activity viewed for completion-tracking
$completion=new completion_info($course);
$completion->set_module_viewed($cm);
echo $OUTPUT->footer();
+4 -4
View File
@@ -1786,10 +1786,10 @@ function feedback_get_page_to_continue($feedbackid, $courseid = false, $guestid
$params = array();
if($courseid) {
$courseselect = "fv.course_id = :courseid";
$courseselect = "AND fv.course_id = :courseid";
$params['courseid'] = $courseid;
}else {
$courseselect = "1";
$courseselect = '';
}
if($guestid) {
@@ -1807,7 +1807,7 @@ function feedback_get_page_to_continue($feedbackid, $courseid = false, $guestid
WHERE fc.id = fv.completed
$userselect
AND fc.feedback = :feedbackid
AND $courseselect
$courseselect
AND fi.id = fv.item
$usergroup";
$params['feedbackid'] = $feedbackid;
@@ -2779,4 +2779,4 @@ function feedback_init_feedback_session() {
$SESSION->feedback = new stdClass();
}
}
}
}
+5 -2
View File
@@ -97,6 +97,10 @@
rss_add_http_header($context, 'mod_forum', $forum, $rsstitle);
}
// Mark viewed if required
$completion = new completion_info($course);
$completion->set_module_viewed($cm);
/// Print header.
/// Add ajax-related libs for ratings if required MDL-20119
$PAGE->requires->yui2_lib('event');
@@ -239,8 +243,7 @@
break;
}
$completion=new completion_info($course);
$completion->set_module_viewed($cm);
echo $OUTPUT->footer($course);
+4 -5
View File
@@ -125,6 +125,10 @@ if (!$cm->visible and !has_capability('moodle/course:viewhiddenactivities', $con
}
add_to_log($course->id, "glossary", "view", "view.php?id=$cm->id&amp;tab=$tab", $glossary->id, $cm->id);
// Mark as viewed
$completion = new completion_info($course);
$completion->set_module_viewed($cm);
/// stablishing flag variables
if ( $sortorder = strtolower($sortorder) ) {
if ($sortorder != 'asc' and $sortorder != 'desc') {
@@ -495,8 +499,3 @@ glossary_print_tabbed_table_end();
/// Finish the page
echo $OUTPUT->footer();
/// Mark as viewed
$completion=new completion_info($course);
$completion->set_module_viewed($cm);
+1 -1
View File
@@ -2030,7 +2030,7 @@ abstract class lesson_page extends lesson_base {
$result->feedback = $OUTPUT->box(format_text($this->get_contents(), $this->properties->contentsformat, $options), 'generalbox boxaligncenter');
$result->feedback .= '<div class="correctanswer generalbox"><em>'.get_string("youranswer", "lesson").'</em> : '.$result->studentanswer; // already in clean html
$result->feedback .= $OUTPUT->box($result->response, $class); // already conerted to HTML
echo "</div>";
$result->feedback .= '</div>';
}
}
}
+4 -4
View File
@@ -45,6 +45,10 @@ if ($backtocourse) {
redirect(new moodle_url('/course/view.php', array('id'=>$course->id)));
}
// Mark as viewed
$completion = new completion_info($course);
$completion->set_module_viewed($cm);
$url = new moodle_url('/mod/lesson/view.php', array('id'=>$id));
if ($pageid !== null) {
$url->param('pageid', $pageid);
@@ -564,7 +568,3 @@ if ($pageid != LESSON_EOL) {
echo $lessoncontent;
echo $lessonoutput->footer();
}
/// Mark as viewed
$completion=new completion_info($course);
$completion->set_module_viewed($cm);
+4 -6
View File
@@ -55,6 +55,10 @@
/// Log this request.
add_to_log($course->id, "quiz", "view", "view.php?id=$cm->id", $quiz->id, $cm->id);
// Mark module as viewed
$completion = new completion_info($course);
$completion->set_module_viewed($cm);
/// Initialize $PAGE, compute blocks
$PAGE->set_url('/mod/quiz/view.php', array('id' => $cm->id));
@@ -382,10 +386,4 @@
}
echo $OUTPUT->box_end();
// Mark module as viewed (note, we do this here and not in finish_page,
// otherwise the 'not enrolled' error conditions would result in marking
// 'viewed', I think it's better if they don't.)
$completion=new completion_info($course);
$completion->set_module_viewed($cm);
echo $OUTPUT->footer();
+4 -4
View File
@@ -146,6 +146,10 @@
$SESSION->scorm_mode = $mode;
$SESSION->scorm_attempt = $attempt;
// Mark module viewed
$completion = new completion_info($course);
$completion->set_module_viewed($cm);
//
// Print the page header
//
@@ -249,10 +253,6 @@ if (!isset($result->toctitle)) {
$PAGE->requires->js_init_call('M.mod_scorm.init', array($scorm->hidenav, $scorm->hidetoc, $result->toctitle, $name, $sco->id));
$completion=new completion_info($course);
$completion->set_module_viewed($cm);
if (!empty($forcejs)) {
echo $OUTPUT->box(get_string("forcejavascriptmessage", "scorm"), "generalbox boxaligncenter forcejavascriptmessage");
}
+4 -3
View File
@@ -51,6 +51,10 @@ require_capability('mod/workshop:view', $PAGE->context);
$workshop = new workshop($workshop, $cm, $course);
$workshop->log('view');
// Mark viewed
$completion = new completion_info($course);
$completion->set_module_viewed($cm);
if (!is_null($editmode) && $PAGE->user_allowed_editing()) {
$USER->editing = $editmode;
}
@@ -538,7 +542,4 @@ case workshop::PHASE_CLOSED:
default:
}
$completion = new completion_info($course);
$completion->set_module_viewed($cm);
echo $output->footer();
+11 -10
View File
@@ -831,16 +831,17 @@ function tag_compute_correlations($mincorrelation = 2) {
// correlationid : This is the id of the row in the tag_correlation table that
// relates to the tagid field and will be NULL if there are no
// existing correlations
$sql = 'SELECT ta.tagid, tb.tagid AS correlation, co.id AS correlationid, co.correlatedtags
FROM {tag_instance} ta
LEFT JOIN {tag_instance} tb
ON (ta.itemtype = tb.itemtype AND ta.itemid = tb.itemid AND ta.tagid <> tb.tagid)
LEFT JOIN {tag_correlation} co
ON co.tagid = ta.tagid
WHERE tb.tagid IS NOT NULL
GROUP BY ta.tagid, tb.tagid
HAVING COUNT(*) > :mincorrelation
ORDER BY ta.tagid ASC, COUNT(*) DESC, tb.tagid ASC';
$sql = 'SELECT pairs.tagid, pairs.correlation, pairs.ocurrences,
co.id AS correlationid, co.correlatedtags
FROM (
SELECT ta.tagid, tb.tagid AS correlation, COUNT(*) AS ocurrences
FROM {tag_instance} ta
JOIN {tag_instance} tb ON (ta.itemtype = tb.itemtype AND ta.itemid = tb.itemid AND ta.tagid <> tb.tagid)
GROUP BY ta.tagid, tb.tagid
HAVING COUNT(*) > :mincorrelation
) pairs
LEFT JOIN {tag_correlation} co ON co.tagid = pairs.tagid
ORDER BY pairs.tagid ASC, pairs.ocurrences DESC, pairs.correlation ASC';
$rs = $DB->get_recordset_sql($sql, array('mincorrelation' => $mincorrelation));
// Set up an empty tag correlation object
-6
View File
@@ -106,17 +106,11 @@ dfn {
}
pre, code {
margin: 1.5em 0;
white-space: pre;
}
pre, code, tt {
font: 1.2em monospace;
line-height: 1.5;
}
tt {
display: block;
margin: 1.5em 0;
line-height: 1.5;
}
-6
View File
@@ -115,19 +115,13 @@ dfn {
}
pre, code {
margin: 1.5em 0;
white-space: pre;
}
pre, code, tt {
font: 1.2em monospace;
line-height: 1.5;
}
tt {
display: block;
margin: 1.5em 0;
line-height: 1.5;
}
.small {
+2 -2
View File
@@ -30,10 +30,10 @@
defined('MOODLE_INTERNAL') || die();
$version = 2011033002.01; // 20110330 = branching date YYYYMMDD - do not modify!
$version = 2011033002.02; // 20110330 = branching date YYYYMMDD - do not modify!
// RR = release version - do not change in weeklies
// .XX = incremental changes
$release = '2.0.2+ (Build: 20110406)'; // Human-friendly version name
$release = '2.0.2+ (Build: 20110413)'; // Human-friendly version name
$maturity = MATURITY_STABLE; // this version's maturity level