Commit b1ef63e5 authored by lpsolit%gmail.com's avatar lpsolit%gmail.com

Bug 206037: [SECURITY] Fix escaping/quoting in edit*.cgi scripts - Patch by…

Bug 206037: [SECURITY] Fix escaping/quoting in edit*.cgi scripts - Patch by Frédéric Buclin <LpSolit@gmail.com> r=justdave a=justdave
parent 40aae68e
......@@ -123,6 +123,8 @@ use File::Basename;
ON_WINDOWS
MAX_TOKEN_AGE
SAFE_PROTOCOLS
);
@Bugzilla::Constants::EXPORT_OK = qw(contenttypes);
......@@ -302,6 +304,11 @@ use constant FIELD_TYPE_SINGLE_SELECT => 2;
# The maximum number of days a token will remain valid.
use constant MAX_TOKEN_AGE => 3;
# 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');
# States that are considered to be "open" for bugs.
use constant BUG_STATE_OPEN => ('NEW', 'REOPENED', 'ASSIGNED',
'UNCONFIRMED');
......
......@@ -125,6 +125,18 @@ use constant OPTIONAL_MODULES => [
name => 'SOAP::Lite',
version => 0
},
{
# Since Perl 5.8, we need the 'utf8_mode' method of HTML::Parser
# which has been introduced in version 3.39_92 and fixed in 3.40
# to not complain when running Perl 5.6.
# This module is required by HTML::Scrubber.
name => 'HTML::Parser',
version => ($] >= 5.008) ? '3.40' : 0
},
{
name => 'HTML::Scrubber',
version => 0
},
];
# These are only required if you want to use Bugzilla with
......@@ -305,6 +317,17 @@ sub check_requirements {
" " . install_command('Net::LDAP') . "\n\n";
}
# HTML filtering
if (!$have_mod{'HTML::Parser'} || !$have_mod{'HTML::Scrubber'}) {
print "If you want additional HTML tags within product and group",
" descriptions,\nyou should install:\n\n";
print " HTML::Scrubber: " . install_command('HTML::Scrubber') . "\n"
if !$have_mod{'HTML::Scrubber'};
print " HTML::Parser: " . install_command('HTML::Parser') . "\n"
if !$have_mod{'HTML::Parser'};
print "\n";
}
# mod_perl
if (!$have_mod{'mod_perl2'}) {
print "If you would like mod_perl support, you must install at",
......
......@@ -289,7 +289,8 @@ sub quoteUrls {
~egox;
# 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
......@@ -735,6 +736,8 @@ sub create {
return $var;
},
html_light => \&Bugzilla::Util::html_light_quote,
# iCalendar contentline filter
ics => [ sub {
my ($context, @args) = @_;
......
......@@ -34,7 +34,7 @@ use base qw(Exporter);
@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 get_netaddr correct_urlbase
lsearch
diff_arrays diff_strings
......@@ -95,6 +95,93 @@ sub html_quote {
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#<#&lt;#g;
$text =~ s#>#&gt;#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 originally came from CGI.pm, by Lincoln D. Stein
sub url_quote {
my ($toencode) = (@_);
......@@ -553,6 +640,12 @@ be done in the template where possible.
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.
......
......@@ -50,3 +50,8 @@ table.groups td.checkbox {
text-align: center;
white-space: nowrap;
}
.missing {
color: red;
border-color: inherit;
}
......@@ -223,7 +223,7 @@ sub directive_ok {
# 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|base64|url_quote|css_class_quote|
ics|quoteUrls|time|uri|xml|lower|
ics|quoteUrls|time|uri|xml|lower|html_light|
obsolete|inactive|closed|unitconvert|
txt|none)\b/x;
......
......@@ -42,8 +42,8 @@
<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>
......@@ -63,8 +63,8 @@
<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>
......
......@@ -49,8 +49,8 @@
</td>
<td>
[% IF settings.${name}.is_enabled %]
<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>
......@@ -64,8 +64,8 @@
[% END %]
</select>
[% ELSE %]
<select name="[% name %]" id="[% name %]" disabled="disabled">
<option value="[% default_name %]">
<select name="[% name FILTER html %]" id="[% name FILTER html %]" disabled="disabled">
<option value="[% default_name FILTER html %]">
Site Default ([% setting_descs.${default_val} OR default_val FILTER html %])
</option>
</select>
......
......@@ -36,7 +36,7 @@
<td valign="top">Description:</td>
<td valign="top">
[% IF classification.description %]
[% classification.description FILTER none %]
[% classification.description FILTER html_light %]
[% ELSE %]
<font color="red">description missing</font>
[% END %]
......
......@@ -59,7 +59,7 @@
<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 %]
......
......@@ -33,7 +33,7 @@
<td valign="top">Description:</td>
<td valign="top" colspan=3>
[% IF classification.description %]
[% classification.description FILTER none %]
[% classification.description FILTER html_light %]
[% ELSE %]
<font color="red">description missing</font>
[% END %]
......
......@@ -37,7 +37,7 @@
<td valign="top"><a href="editclassifications.cgi?action=edit&amp;classification=[% cl.name FILTER url_quote %]"><b>[% cl.name 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 %]
......
......@@ -44,7 +44,7 @@
</tr>
<tr>
<td valign="top">Component Description:</td>
<td valign="top">[% comp.description FILTER html %]</td>
<td valign="top">[% comp.description FILTER html_light %]</td>
</tr>
<tr>
<td valign="top">Default assignee:</td>
......@@ -66,7 +66,7 @@
</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') %]
......
......@@ -56,7 +56,7 @@
<table>
<tr>
<td>Updated description to:</td>
<td>'[% comp.description FILTER html %]'</td>
<td>'[% comp.description FILTER html_light %]'</td>
</tr>
</table>
[% END %]
......
......@@ -48,7 +48,7 @@
<tr>
<td>[% gid FILTER html %]</td>
<td>[% name FILTER html %]</td>
<td>[% description FILTER html %]</td>
<td>[% description FILTER html_light %]</td>
</tr>
</table>
......
......@@ -165,7 +165,7 @@
[% 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>
......
......@@ -47,6 +47,7 @@
}
{name => 'description'
heading => 'Description'
allow_html_content => 1
}
{name => 'userregexp'
heading => 'User RegExp'
......
......@@ -44,6 +44,7 @@
{
name => "description"
heading => "Description"
allow_html_content => 1
},
{
name => "bug_count"
......
......@@ -56,7 +56,7 @@
[%# descriptions are intentionally not filtered to allow html content %]
<td>
[% IF classification.description %]
[% classification.description FILTER none %]
[% classification.description FILTER html_light %]
[% ELSE %]
<span style="color: red">missing</span>
[% END %]
......@@ -78,7 +78,7 @@
[%# descriptions are intentionally not filtered to allow html content %]
<td valign="top">
[% IF product.description %]
[% product.description FILTER none %]
[% product.description FILTER html_light %]
[% ELSE %]
<span style="color: red">missing</span>
[% END %]
......@@ -132,7 +132,7 @@
[%# 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 %]
......
......@@ -40,7 +40,7 @@
<tr>
<th align="right">Description:</th>
<td><textarea rows="4" cols="64" wrap="virtual" name="description">
[% product.description FILTER none %]</textarea>
[% product.description FILTER html %]</textarea>
</td>
</tr>
......
......@@ -50,7 +50,7 @@
[% FOREACH component = product.components %]
<b>[% component.name FILTER html %]:</b>&nbsp;
[% IF component.description %]
[% component.description FILTER none %]
[% component.description FILTER html_light %]
[% ELSE %]
<font color="red">description missing</font>
[% END %]
......
......@@ -75,7 +75,7 @@
<p>
Updated description to:</p>
</p>
<p style="margin: 1em 3em 1em 3em">[% product.description FILTER html %]</p>
<p style="margin: 1em 3em 1em 3em">[% product.description FILTER html_light %]</p>
[% updated = 1 %]
[% END %]
......
......@@ -64,7 +64,7 @@ page, and the Default Value will automatically apply to everyone.
[% 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 %]>
......@@ -75,8 +75,8 @@ page, and the Default Value will automatically apply to everyone.
</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>
......
......@@ -32,7 +32,7 @@
# 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
......@@ -41,8 +41,8 @@
# 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:
......@@ -94,6 +94,7 @@
content = c.content
content_use_field = c.content_use_field
align = c.align
class = c.class
allow_html_content = c.allow_html_content
yesno_field = c.yesno_field
%]
......@@ -112,6 +113,8 @@
IF override.override_content_use_field %]
[% SET align = override.align
IF override.override_align %]
[% SET class = override.class
IF override.override_class %]
[% SET allow_html_content = override.allow_html_content
IF override.override_allow_html_content %]
[% SET yesno_field = override.yesno_field
......@@ -122,7 +125,8 @@
[% END %]
[% END %]
<td [% IF align %] align="[% align FILTER html %]" [% END %]>
<td [% IF align %] align="[% align FILTER html %]" [% END %]
[% IF class %] class="[% class FILTER html %]" [% END %]>
[% IF contentlink %]
[% link_uri = contentlink %]
......@@ -143,7 +147,7 @@
[% colname = row.${c.name} %]
[% field_descs.${colname} FILTER html %]
[% ELSIF content %]
[% content FILTER none %]
[% content FILTER html_light %]
[% ELSE %]
[% IF yesno_field %]
[% IF row.${c.name} %]
......@@ -153,7 +157,7 @@
[% END %]
[% ELSE %]
[% IF allow_html_content %]
[% row.${c.name} FILTER none %]
[% row.${c.name} FILTER html_light %]
[% ELSE %]
[% row.${c.name} FILTER html %]
[% END %]
......
......@@ -89,7 +89,7 @@
<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>
......
......@@ -38,11 +38,9 @@
heading => 'Edit user...'
contentlink => 'editusers.cgi?action=edit&amp;userid=%%userid%%' _
listselectionurlparams
allow_html_content => 1
}
{name => 'realname'
heading => 'Real name'
allow_html_content => 1
}
{heading => 'User Account Log'
content => 'View'
......@@ -64,23 +62,38 @@
%]
[% END %]
[%# Disabled users are crossed out. Missing realnames are noticed in red. %]
[% overrides.login_name = [] %]
[% overrides.realname = [] %]
[% 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>' %]
[% IF !thisuser.realname %]
[%# We cannot pass one class now and one class later. %]
[% SET classes = (thisuser.disabledtext ? "bz_inactive missing" : "missing") %]
[% overrides.realname.push({
match_value => "$thisuser.login_name"
match_field => 'login_name'
content => "missing"
override_content => 1
class => "$classes"
override_class => 1 })
%]
[% END %]
[% IF thisuser.disabledtext %]
[% thisuser.login_name = "<span class=\"bz_inactive\">$thisuser.login_name</span>" %]
[% thisuser.realname = "<span class=\"bz_inactive\">$thisuser.realname</span>" %]
[% overrides.login_name.push({
match_value => "$thisuser.login_name"
match_field => 'login_name'
class => "bz_inactive"
override_class => 1 })
%]
[% overrides.realname.push({
match_value => "$thisuser.login_name"
match_field => 'login_name'
class => "bz_inactive"
override_class => 1 })
%]
[% END %]
[% END %]
......@@ -89,6 +102,7 @@
[% PROCESS admin/table.html.tmpl
columns = columns
data = users
overrides = overrides
%]
<p>
......
......@@ -526,7 +526,7 @@ function handleWantsAttachment(wants_attachment) {
<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 %]
......
......@@ -198,7 +198,7 @@
<td>
[% get_resolution(bug.resolution) FILTER html %]
[% IF bug.resolution == "DUPLICATE" %]
of [% terms.bug %] [%+ "${bug.dup_id}" FILTER bug_link(bug.dup_id) %]
of [% terms.bug %] [%+ "${bug.dup_id}" FILTER bug_link(bug.dup_id) FILTER none %]
[% END %]
</td>
</tr>
......@@ -619,7 +619,7 @@
name="bit-[% group.bit %]" id="bit-[% group.bit %]"
[% " checked=\"checked\"" IF group.ison %]
[% " disabled=\"disabled\"" IF NOT group.ingroup %]>
<label for="bit-[% group.bit %]">[% group.description %]</label>
<label for="bit-[% group.bit %]">[% group.description FILTER html_light %]</label>
<br>
[% END %]
[% END %]
......@@ -683,7 +683,7 @@
</th>
<td>
[% FOREACH depbug = bug.${dep.fieldname} %]
[% depbug FILTER bug_link(depbug) %][% " " %]
[% depbug FILTER bug_link(depbug) FILTER none %][% " " %]
[% END %]
</td>
<td>
......
......@@ -303,7 +303,7 @@
<th>[% terms.Bug %] [%+ field_descs.${name} FILTER html %]:</th>
<td>
[% FOREACH depbug = bug.${name} %]
[% depbug FILTER bug_link(depbug) %][% ", " IF not loop.last() %]
[% depbug FILTER bug_link(depbug) FILTER none %][% ", " IF not loop.last() %]
[% END %]
</td>
......
......@@ -112,7 +112,6 @@
],
'reports/keywords.html.tmpl' => [
'keyword.description',
'keyword.bug_count',
],
......@@ -189,16 +188,10 @@
'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',
......@@ -225,10 +218,6 @@
'h.html',
],
'global/choose-classification.html.tmpl' => [
'class.description',
],
'global/choose-product.html.tmpl' => [
'target',
],
......@@ -314,13 +303,10 @@
'bug.bug_id',
'bug.votes',
'group.bit',
'group.description',
'dep.title',
'dep.fieldname',
'bug.${dep.fieldname}.join(\', \')',
'selname',
'depbug FILTER bug_link(depbug)',
'"${bug.dup_id}" FILTER bug_link(bug.dup_id)',
'" accesskey=\"$accesskey\"" IF accesskey',
'inputname',
'" colspan=\"$colspan\"" IF $colspan',
......@@ -342,7 +328,6 @@
'bug/show-multiple.html.tmpl' => [
'bug.bug_id',
'depbug FILTER bug_link(depbug)',
'attachment.id',
'flag.status',
],
......@@ -402,7 +387,6 @@
'bug/create/create.html.tmpl' => [
'g.bit',
'g.description',
'sel.name',
'sel.description',
'cloned_bug_id',
......@@ -484,10 +468,6 @@
'link_uri'
],
'admin/classifications/select.html.tmpl' => [
'cl.description',
],
'admin/products/groupcontrol/confirm-edit.html.tmpl' => [
'group.count',
],
......@@ -572,11 +552,6 @@
'comp.bug_count'
],
'admin/settings/edit.html.tmpl' => [
'name',
'checkbox_name'
],
'account/login.html.tmpl' => [
'target',
],
......@@ -587,11 +562,6 @@
'prefname',
],
'account/prefs/permissions.html.tmpl' => [
'bit_description.name',
'bit_description.desc',
],
'account/prefs/prefs.html.tmpl' => [
'current_tab.label',
'current_tab.name',
......@@ -601,9 +571,4 @@
'group.id',
],
'account/prefs/settings.html.tmpl' => [
'name',
'default_name'
],
);
......@@ -54,7 +54,7 @@
</th>
[% IF class.description %]
<td valign="top">&nbsp;[% class.description %]</td>
<td valign="top">&nbsp;[% class.description FILTER html_light %]</td>
[% END %]
</tr>
[% END %]
......
......@@ -51,7 +51,7 @@
[% p.name FILTER html %]</a>:&nbsp;
</th>
<td valign="top">[% p.description FILTER none %]</td>
<td valign="top">[% p.description FILTER html_light %]</td>
</tr>
[% END %]
......
......@@ -256,11 +256,8 @@
[% 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>
......
......@@ -30,8 +30,6 @@
[%############################################################################%]
[% DEFAULT title = "$terms.Bug List" %]
[% title = title FILTER html %]
[%############################################################################%]
[%# Bug Table #%]
......@@ -40,7 +38,7 @@
<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>
......
......@@ -36,7 +36,7 @@
[% END %]
<p>
[% product.description FILTER none %]
[% product.description FILTER html_light %]
</p>
<table>
......@@ -87,7 +87,7 @@
</tr>
<tr>
<td colspan="[% numcols - 1 %]">
[% comp.description FILTER none %]
[% comp.description FILTER html_light %]
</td>
</tr>
[% END %]
......@@ -24,7 +24,7 @@
# keywords: array keyword objects. May be empty. Each has has four members:
# id: id of the keyword
# name: the name of the keyword
# description: keyword description. May be HTML.
# description: keyword description. Can contain some limited HTML code.
# bug_count: number of bugs with that keyword
# caneditkeywords: boolean. True if this user can edit keywords
%]
......@@ -55,7 +55,7 @@
<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="center">
[% IF keyword.bug_count > 0 %]
<a href="buglist.cgi?keywords=[% keyword.name FILTER url_quote %]&amp;resolution=---">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment