FULLTEXT_BUGLIST_LIMIT
SENDMAIL_EXE
+
+ SAFE_PROTOCOLS
);
@Bugzilla::Constants::EXPORT_OK = qw(contenttypes);
# Path to sendmail.exe (Windows only)
use constant SENDMAIL_EXE => '/usr/lib/sendmail.exe';
+# Protocols which are considered as safe.
+use constant SAFE_PROTOCOLS => ('afs', 'cid', 'ftp', 'gopher', 'http', 'https',
+ 'irc', 'mid', 'news', 'nntp', 'prospero', 'telnet',
+ 'view-source', 'wais');
+
1;
$var =~ s/\@/\@/g;
return $var;
},
-
+
+ html_light => \&Bugzilla::Util::html_light_quote,
+
# iCalendar contentline filter
ics => [ sub {
my ($context, @args) = @_;
@Bugzilla::Util::EXPORT = qw(is_tainted trick_taint detaint_natural
detaint_signed
html_quote url_quote value_quote xml_quote
- css_class_quote
+ css_class_quote html_light_quote
i_am_cgi
lsearch max min
diff_arrays diff_strings
return $var;
}
+sub html_light_quote {
+ my ($text) = @_;
+
+ # List of allowed HTML elements having no attributes.
+ my @allow = qw(b strong em i u p br abbr acronym ins del cite code var
+ dfn samp kbd big small sub sup tt dd dt dl ul li ol);
+
+ # Are HTML::Scrubber and HTML::Parser installed?
+ eval { require HTML::Scrubber;
+ require HTML::Parser;
+ };
+
+ # We need utf8_mode() from HTML::Parser 3.40 if running Perl >= 5.8.
+ if ($@ || ($] >= 5.008 && $HTML::Parser::VERSION < 3.40)) { # Package(s) not installed.
+ my $safe = join('|', @allow);
+ my $chr = chr(1);
+
+ # First, escape safe elements.
+ $text =~ s#<($safe)>#$chr$1$chr#go;
+ $text =~ s#</($safe)>#$chr/$1$chr#go;
+ # Now filter < and >.
+ $text =~ s#<#<#g;
+ $text =~ s#>#>#g;
+ # Restore safe elements.
+ $text =~ s#$chr/($safe)$chr#</$1>#go;
+ $text =~ s#$chr($safe)$chr#<$1>#go;
+ return $text;
+ }
+ else { # Packages installed.
+ # We can be less restrictive. We can accept elements with attributes.
+ push(@allow, qw(a blockquote q span));
+
+ # Allowed protocols.
+ my $safe_protocols = join('|', SAFE_PROTOCOLS);
+ my $protocol_regexp = qr{(^(?:$safe_protocols):|^[^:]+$)}i;
+
+ # Deny all elements and attributes unless explicitly authorized.
+ my @default = (0 => {
+ id => 1,
+ name => 1,
+ class => 1,
+ '*' => 0, # Reject all other attributes.
+ }
+ );
+
+ # Specific rules for allowed elements. If no specific rule is set
+ # for a given element, then the default is used.
+ my @rules = (a => {
+ href => $protocol_regexp,
+ title => 1,
+ id => 1,
+ name => 1,
+ class => 1,
+ '*' => 0, # Reject all other attributes.
+ },
+ blockquote => {
+ cite => $protocol_regexp,
+ id => 1,
+ name => 1,
+ class => 1,
+ '*' => 0, # Reject all other attributes.
+ },
+ 'q' => {
+ cite => $protocol_regexp,
+ id => 1,
+ name => 1,
+ class => 1,
+ '*' => 0, # Reject all other attributes.
+ },
+ );
+
+ my $scrubber = HTML::Scrubber->new(default => \@default,
+ allow => \@allow,
+ rules => \@rules,
+ comment => 0,
+ process => 0);
+
+ # Avoid filling the web server error log with Perl 5.8.x.
+ # In HTML::Scrubber 0.08, the HTML::Parser object is stored in
+ # the "_p" key, but this may change in future versions.
+ if ($] >= 5.008 && ref($scrubber->{_p}) eq 'HTML::Parser') {
+ $scrubber->{_p}->utf8_mode(1);
+ }
+ return $scrubber->scrub($text);
+ }
+}
+
# This orignally came from CGI.pm, by Lincoln D. Stein
sub url_quote {
my ($toencode) = (@_);
Returns a value quoted for use in HTML, with &, E<lt>, E<gt>, and E<34> being
replaced with their appropriate HTML entities.
+=item C<html_light_quote($val)>
+
+Returns a string where only explicitly allowed HTML elements and attributes
+are kept. All HTML elements and attributes not being in the whitelist are either
+escaped (if HTML::Scrubber is not installed) or removed.
+
=item C<url_quote($val)>
Quotes characters so that they may be included as part of a url.
my $gdgraph = have_vers("GD::Graph",0);
my $gdtextalign = have_vers("GD::Text::Align",0);
my $patchreader = have_vers("PatchReader","0.9.4");
+my $html_parser = have_vers("HTML::Parser", ($] >= 5.008) ? "3.40" : 0);
+my $scrubber = have_vers("HTML::Scrubber", 0);
print "\n" unless $silent;
print "install the \nPatchReader module:\n";
print "PatchReader: " . install_command("PatchReader") . "\n";
}
+if ((!$scrubber || !$html_parser) && !$silent) {
+ print "If you want additional HTML tags within product and group ";
+ print "descriptions,\nyou should install:\n";
+ print "HTML::Scrubber: " . install_command("HTML::Scrubber") . "\n"
+ if !$scrubber;
+ print "HTML::Parser: " . install_command("HTML::Parser") . "\n"
+ if !$html_parser;
+ print "\n";
+}
if (%missing) {
print "\n\n";
# Dawn Endico <endico@mozilla.org>
# Joe Robins <jmrobins@tgix.com>
# Gavin Shelley <bugzilla@chimpychompy.org>
-# Frédéric Buclin <LpSolit@gmail.com>
+# Frédéric Buclin <LpSolit@gmail.com>
# Greg Hendricks <ghendricks@novell.com>
#
# Direct any questions on this source code to
}
unless (TestProduct $prod) {
- print "Sorry, product '$prod' does not exist.";
+ print "Sorry, product '" . html_quote($prod) . "' does not exist.";
PutTrailer();
exit;
}
}
unless (TestClassification $cl) {
- print "Sorry, classification '$cl' does not exist.";
+ print "Sorry, classification '" . html_quote($cl) . "' does not exist.";
PutTrailer();
exit;
}
my $res = $dbh->selectrow_array($query, undef, ($prod, $cl));
unless ($res) {
- print "Sorry, classification->product '$cl'->'$prod' does not exist.";
+ print "Sorry, classification->product '" . html_quote($cl) .
+ "'->'" . html_quote($prod) . "' does not exist.";
PutTrailer();
exit;
}
# Check for exact case sensitive match:
if ($existing_product eq $product) {
- print "The product '$product' already exists. Please press\n";
+ print "The product '" . html_quote($product) . "' already exists. Please press\n";
print "<b>Back</b> and try again.\n";
PutTrailer($localtrailer);
exit;
# Next check for a case-insensitive match:
if (lc($existing_product) eq lc($product)) {
- print "The new product '$product' differs from existing product ";
- print "'$existing_product' only in case. Please press\n";
+ print "The new product '" . html_quote($product) . "' differs from existing product '";
+ print html_quote($existing_product) . "' only in case. Please press\n";
print "<b>Back</b> and try again.\n";
PutTrailer($localtrailer);
exit;
my $version = trim($cgi->param('version') || '');
if ($version eq '') {
- print "You must enter a version for product '$product'. Please press\n";
+ print "You must enter a version for product '" . html_quote($product) . "'. Please press\n";
print "<b>Back</b> and try again.\n";
PutTrailer($localtrailer);
exit;
my $description = trim($cgi->param('description') || '');
if ($description eq '') {
- print "You must enter a description for product '$product'. Please press\n";
+ print "You must enter a description for product '" . html_quote($product) . "'. Please press\n";
print "<b>Back</b> and try again.\n";
PutTrailer($localtrailer);
exit;
while ( MoreSQLData() ) {
my ($component, $description) = FetchSQLData();
$description ||= "<FONT COLOR=\"red\">description missing</FONT>";
- print "<tr><th align=right valign=top>$component:</th>";
- print "<td valign=top>$description</td></tr>\n";
+ print "<tr><th align=right valign=top>" . html_quote($component) . ":</th>";
+ print "<td valign=top>" . html_light_quote($description) . "</td></tr>\n";
}
print "</table>\n";
} else {
while ( MoreSQLData() ) {
my ($version) = FetchSQLData();
print "<BR>" if $br;
- print $version;
+ print html_quote($version);
$br = 1;
}
} else {
while ( MoreSQLData() ) {
my ($milestone) = FetchSQLData();
print "<BR>" if $br;
- print $milestone;
+ print html_quote($milestone);
$br = 1;
}
} else {
if (lc($product) ne lc($productold) &&
TestProduct($product)) {
- print "Sorry, product name '$product' is already in use.";
+ print "Sorry, product name '" . html_quote($product) . "' is already in use.";
$dbh->bz_unlock_tables(UNLOCK_ABORT);
PutTrailer($localtrailer);
exit;
my ($who, $id) = (@$ref);
RemoveVotes($id, $who, "The rules for voting on this product has changed;\nyou had too many votes for a single bug.");
my $name = DBID_to_name($who);
- print qq{<br>Removed votes for bug <A HREF="show_bug.cgi?id=$id">$id</A> from $name\n};
+ print "<br>Removed votes for bug <A HREF=\"show_bug.cgi?id=$id\">$id</A> from " .
+ html_quote($name) . "\n";
}
}
RemoveVotes($id, $who,
"The rules for voting on this product has changed; you had too many\ntotal votes, so all votes have been removed.");
my $name = DBID_to_name($who);
- print qq{<br>Removed votes for bug <A HREF="show_bug.cgi?id=$id">$id</A> from $name\n};
+ print "<br>Removed votes for bug <A HREF=\"show_bug.cgi?id=$id\">$id</A> from " .
+ html_quote($name) . "\n";
}
}
}
my $tmp;
# non-mailto protocols
- my $protocol_re = qr/(afs|cid|ftp|gopher|http|https|irc|mid|news|nntp|prospero|telnet|view-source|wais)/i;
+ my $safe_protocols = join('|', SAFE_PROTOCOLS);
+ my $protocol_re = qr/($safe_protocols)/i;
$text =~ s~\b(${protocol_re}: # The protocol:
[^\s<>\"]+ # Any non-whitespace
text-align: center;
white-space: nowrap;
}
+
+.missing {
+ color: red;
+ border-color: inherit;
+}
# Note: If a single directive prints two things, and only one is
# filtered, we may not catch that case.
return 1 if $directive =~ /FILTER\ (html|csv|js|url_quote|css_class_quote|
- ics|quoteUrls|time|uri|xml|lower|
+ ics|quoteUrls|time|uri|xml|lower|html_light|
obsolete|inactive|closed|unitconvert|
none)\b/x;
<table align="center">
[% FOREACH bit_description = has_bits %]
<tr>
- <td>[% bit_description.name %]</td>
- <td>[% bit_description.desc %]</td>
+ <td>[% bit_description.name FILTER html %]</td>
+ <td>[% bit_description.desc FILTER html_light %]</td>
</tr>
[% END %]
</table>
<table align="center">
[% FOREACH bit_description = set_bits %]
<tr>
- <td>[% bit_description.name %]</td>
- <td>[% bit_description.desc %]</td>
+ <td>[% bit_description.name FILTER html %]</td>
+ <td>[% bit_description.desc FILTER html_light %]</td>
</tr>
[% END %]
</table>
[% setting_descs.$name OR name FILTER html %]
</td>
<td>
- <select name="[% name %]" id="[% name %]">
- <option value="[% default_name %]"
+ <select name="[% name FILTER html %]" id="[% name FILTER html %]">
+ <option value="[% default_name FILTER html %]"
[% ' selected="selected"' IF settings.${name}.is_default %]>
Site Default ([% setting_descs.${default_val} OR default_val FILTER html %])
</option>
<td valign="top">Description:</td>
<td valign="top">
[% IF description %]
- [% description %]
+ [% description FILTER html_light %]
[% ELSE %]
<font color="red">description missing</font>
[% END %]
</tr>
<tr>
<th align="right">Description:</th>
- <td><textarea rows=4 cols=64 name="description">[% description %]</textarea></TD>
+ <td><textarea rows=4 cols=64 name="description">[% description FILTER html %]</textarea></TD>
</tr>
<tr valign=top>
<th align="right"><a href="editproducts.cgi?classification=[% classification FILTER url_quote %]">Edit products</a></th>
<th align=right valign=top>[% product.name FILTER html %]</th>
<td valign=top>
[% IF product.description %]
- [% product.description FILTER none %]
+ [% product.description FILTER html_light %]
[% ELSE %]
<font color="red">description missing</font>
[% END %]
<td valign="top">Description:</td>
<td valign="top" colspan=3>
[% IF description %]
- [% description %]
+ [% description FILTER html_light %]
[% ELSE %]
<font color="red">description missing</font>
[% END %]
<td valign="top"><a href="editclassifications.cgi?action=edit&classification=[% cl.classification FILTER url_quote %]"><b>[% cl.classification FILTER html %]</b></a></td>
<td valign="top">
[% IF cl.description %]
- [% cl.description %]
+ [% cl.description FILTER html_light %]
[% ELSE %]
<font color="red">none</font>
[% END %]
</tr>
<tr>
<td valign="top">Component Description:</td>
- <td valign="top">[% description FILTER html %]</td>
+ <td valign="top">[% description FILTER html_light %]</td>
</tr>
<tr>
<td valign="top">Default assignee:</td>
</tr>
<tr>
<td valign="top">Product Description:</td>
- <td valign="top">[% product_description FILTER html %]</td>
+ <td valign="top">[% product_description FILTER html_light %]</td>
[% END %]
[% IF Param('usetargetmilestone') %]
<table>
<tr>
<td>Updated description to:</td>
- <td>'[% description FILTER html %]'</td>
+ <td>'[% description FILTER html_light %]'</td>
</tr>
</table>
[% END %]
<tr>
<td>[% gid FILTER html %]</td>
<td>[% name FILTER html %]</td>
- <td>[% description FILTER html %]</td>
+ <td>[% description FILTER html_light %]</td>
</tr>
</table>
[% group.grpnam FILTER html %]
</a>
</td>
- <td align="left" class="groupdesc">[% group.grpdesc FILTER html %]</td>
+ <td align="left" class="groupdesc">[% group.grpdesc FILTER html_light %]</td>
</tr>
[% END %]
</table>
<a href="editgroups.cgi?action=changeform&group=[% group.id FILTER url_quote %]">
[% group.name FILTER html %]</a>
</td>
- <td>[% group.description FILTER html %]</td>
+ <td>[% group.description FILTER html_light %]</td>
<td>[% group.regexp FILTER html %] </td>
<td align="center">
},
{
name => "description"
- heading => "Description"
+ heading => "Description"
+ allow_html_content => 1
},
{
name => "bug_count"
[% classification_url_part = "" %]
[% END %]
-[% UNLESS class_description %]
- [% class_description = '<span style="color: red">missing</span>' %]
-[% END %]
-[% UNLESS prod_description %]
- [% prod_description = '<span style="color: red">missing</span>' %]
-[% END %]
-
[% IF disallownew %]
[% disallownew = "closed" %]
[% ELSE %]
</tr>
<tr>
<td>Classification Description:</td>
- [%# descriptions are intentionally not filtered to allow html content %]
- <td>[% class_description FILTER none %]</td>
+ <td>
+ [% IF class_description %]
+ [% class_description FILTER html_light %]
+ [% ELSE %]
+ <span style="color: red">missing</span>
+ [% END %]
+ </td>
</tr>
[% END %]
</tr>
<tr>
<td valign="top">Description:</td>
- [%# descriptions are intentionally not filtered to allow html content %]
- <td valign="top">[% prod_description FILTER none %]</td>
+ <td valign="top">
+ [% IF prod_description %]
+ [% prod_description FILTER html_light %]
+ [% ELSE %]
+ <span style="color: red">missing</span>
+ [% END %]
+ </td>
</tr>
[% IF Param('usetargetmilestone') %]
[% FOREACH c = components %]
<tr>
<th align="right">[% c.name FILTER html %]:</th>
- [%# descriptions are intentionally not filtered to allow html content %]
<td>
[% IF c.description %]
- [% c.description FILTER none %]
+ [% c.description FILTER html_light %]
[% ELSE %]
<span style="color: red">missing</span>
[% END %]
[% setting_descs.$name OR name FILTER html %]
</td>
<td>
- <select name="[% name %]" id="[% name %]">
+ <select name="[% name FILTER html %]" id="[% name FILTER html %]">
[% FOREACH x = settings.${name}.legal_values %]
<option value="[% x FILTER html %]"
[% " selected=\"selected\"" IF x == settings.${name}.default_value %]>
</td>
<td align="center">
<input type="checkbox"
- name="[% checkbox_name %]"
- id="[% checkbox_name %]"
+ name="[% checkbox_name FILTER html %]"
+ id="[% checkbox_name FILTER html %]"
[% " checked=\"checked\"" IF settings.${name}.is_enabled %]>
<br>
</td>
# with the key xxx in data hash of the current row.
# content: If specified, the content of this variable is used
# instead of the data pulled from the current row.
- # NOTE: This value is not HTML filtered at output!
+ # NOTE: This value is only partially HTML filtered!
# content_use_field: If defined and true, then each value in the
# column corresponds with a key in the
# field_descs field, and that value from the
# This content WILL be HTML-filtered in this case.
# align: left/center/right. Controls the horizontal alignment of the
# text in the column.
- # allow_html_content: if defined, then this column allows html content
- # so it will not be filtered
+ # allow_html_content: if defined, then this column allows some html content
+ # and so it will be only partially filtered.
# yesno_field: Turn the data from 0/!0 into Yes/No
#
# data:
[% FOREACH row = data %]
<tr>
[% FOREACH c = columns %]
- <td [% IF c.align %] align="[% c.align FILTER html %]" [% END %]>
-
+ <td [% IF c.align %] align="[% c.align FILTER html %]" [% END %]
+ [% IF row.class %] class="[% row.class FILTER html %]" [% END %]>
+
[% IF c.contentlink %]
[% link_uri = c.contentlink %]
[% WHILE link_uri.search('%%(.+?)%%')%]
[% colname = row.${c.name} %]
[% field_descs.${colname} FILTER html %]
[% ELSIF c.content %]
- [% c.content %]
+ [% c.content FILTER html_light %]
[% ELSE %]
[% IF c.yesno_field %]
[% IF row.${c.name} %]
[% END %]
[% ELSE %]
[% IF c.allow_html_content %]
- [% row.${c.name} FILTER none %]
+ [% row.${c.name} FILTER html_light %]
[% ELSE %]
[% row.${c.name} FILTER html %]
[% END %]
<td class="groupname">
<label for="group_[% group.id %]">
<strong>[% group.name FILTER html %]:</strong>
- [%+ group.description FILTER html %]
+ [%+ group.description FILTER html_light %]
</label>
</td>
</tr>
heading => 'Edit user...'
contentlink => 'editusers.cgi?action=edit&userid=%%userid%%' _
listselectionurlparams
- allow_html_content => 1
}
{name => 'realname'
heading => 'Real name'
- allow_html_content => 1
}
]
%]
[% END %]
[% FOREACH thisuser = users %]
- [%# We FILTER html here because we need admin/table.html.tmpl to accept HTML
- # for styling, so we cannot let admin/table.html.tmpl do the FILTER.
- #%]
- [% thisuser.login_name = BLOCK %]
- [% thisuser.login_name FILTER html %]
- [% END %]
- [% IF thisuser.realname %]
- [% thisuser.realname = BLOCK %]
- [% thisuser.realname FILTER html %]
- [% END %]
- [% ELSE %]
- [% SET thisuser.realname = '<span style="color: red">missing</span>' %]
- [% END %]
[% IF thisuser.disabledtext %]
- [% thisuser.login_name = "<span class=\"bz_inactive\">$thisuser.login_name</span>" %]
- [% thisuser.realname = "<span class=\"bz_inactive\">$thisuser.realname</span>" %]
+ [% SET thisuser.class = "bz_inactive" %]
+ [% END %]
+ [% UNLESS thisuser.realname %]
+ [% SET thisuser.realname = "missing" %]
+ [% SET thisuser.class = thisuser.disabledtext ? "bz_inactive missing" : "missing" %]
[% END %]
[% END %]
<input type="checkbox" id="bit-[% g.bit %]"
name="bit-[% g.bit %]" value="1"
[% " checked=\"checked\"" IF g.checked %]>
- <label for="bit-[% g.bit %]">[% g.description %]</label><br>
+ <label for="bit-[% g.bit %]">[% g.description FILTER html_light %]</label><br>
[% END %]
<br>
[% END %]
<input type="checkbox" name="bit-[% group.bit %]" value="1"
[% " checked=\"checked\"" IF group.ison %]
[% " disabled=\"disabled\"" IF NOT group.ingroup %]>
- [% group.description %]
+ [% group.description FILTER html_light %]
<br>
[% END %]
[% END %]
'reports/components.html.tmpl' => [
'numcols',
- 'comp.description',
],
'reports/duplicates-table.html.tmpl' => [
],
'reports/keywords.html.tmpl' => [
- 'keyword.description',
'keyword.bugcount',
],
'list/edit-multiple.html.tmpl' => [
'group.id',
- 'group.description',
- 'group.description FILTER inactive',
'knum',
'menuname',
],
-'list/list-simple.html.tmpl' => [
- 'title',
-],
-
'list/list.rdf.tmpl' => [
'template_version',
'bug.bug_id',
'VERSION',
],
-'global/choose-classification.html.tmpl' => [
- 'classdesc.$p',
-],
-
'global/choose-product.html.tmpl' => [
'target',
- 'proddesc.$p',
],
# You are not permitted to add any values here. Everything in this file should
'bug.bug_id',
'bug.votes',
'group.bit',
- 'group.description',
'dep.title',
'dep.fieldname',
'accesskey',
'bug/create/create.html.tmpl' => [
'g.bit',
- 'g.description',
'sel.name',
'sel.description',
'cloned_bug_id'
'admin/table.html.tmpl' => [
'link_uri',
- 'c.content'
-],
-
-'admin/classifications/del.html.tmpl' => [
- 'description',
-],
-
-'admin/classifications/edit.html.tmpl' => [
- 'description',
-],
-
-'admin/classifications/reclassify.html.tmpl' => [
- 'description',
-],
-
-'admin/classifications/select.html.tmpl' => [
- 'cl.description',
],
'admin/products/groupcontrol/confirm-edit.html.tmpl' => [
'bug_count'
],
-'admin/settings/edit.html.tmpl' => [
- 'name',
- 'checkbox_name'
-],
-
'account/login.html.tmpl' => [
'target',
],
'prefname',
],
-'account/prefs/permissions.html.tmpl' => [
- 'bit_description.name',
- 'bit_description.desc',
-],
-
'account/prefs/prefs.html.tmpl' => [
'tab.name',
'tab.description',
'current_tab.description',
],
-'account/prefs/settings.html.tmpl' => [
- 'name',
- 'default_name'
-],
-
);
</th>
[% IF classdesc.$p %]
- <td valign="top"> [% classdesc.$p %]</td>
+ <td valign="top"> [% classdesc.$p FILTER html_light %]</td>
[% END %]
</tr>
[% END %]
</th>
[% IF proddesc.$p %]
- <td valign="top"> [% proddesc.$p %]</td>
+ <td valign="top"> [% proddesc.$p FILTER html_light %]</td>
[% END %]
</tr>
[% END %]
[% END %]
<td>
- [% IF group.isactive %]
- [% group.description %]
- [% ELSE %]
- [% group.description FILTER inactive %]
- [% END %]
+ [% SET inactive = !group.isactive %]
+ [% group.description FILTER html_light FILTER inactive(inactive) %]
</td>
</tr>
[%############################################################################%]
[% DEFAULT title = "$terms.Bug List" %]
-[% title = title FILTER html %]
-
[%############################################################################%]
[%# Bug Table #%]
<html>
<head>
- <title>[% title %]</title>
+ <title>[% title FILTER html %]</title>
<base href="[% Param("urlbase") %]">
<link href="skins/standard/buglist.css" rel="stylesheet" type="text/css">
</head>
# product: string. The product this is the components list for.
# components: List of hashes. May be empty. Each hash has four members:
# name: string. Name of the component.
- # description: string. Description of the component. May contain HTML.
+ # description: string. Description of the component. Can contain some limited HTML code.
# initialowner: string. Component's default assignee.
# initialqacontact: string. Component's default QA contact.
#%]
</tr>
<tr>
<td colspan="[% numcols - 1 %]">
- [% comp.description %]
+ [% comp.description FILTER html_light %]
</td>
</tr>
[% END %]
[%# INTERFACE:
# keywords: array of hashes. May be empty. Each has has three members:
# name: the name of the keyword
- # description: keyword description. May be HTML.
+ # description: keyword description. Can contain some limited HTML code.
# bugcount: number of bugs with that keyword
# caneditkeywords: boolean. True if this user can edit keywords
%]
<a name="[% keyword.name FILTER html %]">
[% keyword.name FILTER html %]</a>
</th>
- <td>[% keyword.description %]</td>
+ <td>[% keyword.description FILTER html_light %]</td>
<td align="right">
[% IF keyword.bugcount > 0 %]
<a href="buglist.cgi?keywords=[% keyword.name FILTER url_quote %]">