summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDave Lawrence <dlawrence@mozilla.com>2012-05-01 17:37:15 +0200
committerDave Lawrence <dlawrence@mozilla.com>2012-05-01 17:37:15 +0200
commitb9ce8358e9822243421a37548e3d1f0371b97455 (patch)
tree37e983d92587ee30f93c35109200567e499ec491
parent228c7bf9ea03a5faafe22f003e5d56da1b7b2162 (diff)
parent07c6bfa4cea83c8284b04add26729f552c93bafc (diff)
downloadbugzilla-b9ce8358e9822243421a37548e3d1f0371b97455.tar.gz
bugzilla-b9ce8358e9822243421a37548e3d1f0371b97455.tar.xz
merged with bugzilla/4.2
-rw-r--r--Bugzilla/Config/Advanced.pm3
-rw-r--r--Bugzilla/Config/Common.pm11
-rw-r--r--Bugzilla/Constants.pm2
-rw-r--r--Bugzilla/DB/Schema.pm2
-rw-r--r--Bugzilla/Error.pm90
-rw-r--r--Bugzilla/Hook.pm114
-rw-r--r--Bugzilla/Search.pm39
-rw-r--r--Bugzilla/Search/Quicksearch.pm72
-rw-r--r--Bugzilla/Util.pm108
-rwxr-xr-xbuglist.cgi23
-rw-r--r--docs/en/xml/Bugzilla-Guide.xml4
-rw-r--r--docs/en/xml/using.xml10
-rw-r--r--extensions/Example/Extension.pm35
-rw-r--r--extensions/Voting/Extension.pm5
-rw-r--r--js/comments.js6
-rw-r--r--js/field.js29
-rwxr-xr-xquery.cgi1
-rw-r--r--skins/standard/global.css33
-rw-r--r--skins/standard/global/down.pngbin335 -> 0 bytes
-rw-r--r--skins/standard/global/left.pngbin339 -> 0 bytes
-rw-r--r--skins/standard/global/right.pngbin339 -> 0 bytes
-rw-r--r--skins/standard/global/up.pngbin318 -> 0 bytes
-rw-r--r--template/en/default/bug/comments.html.tmpl2
-rw-r--r--template/en/default/bug/field.html.tmpl39
-rw-r--r--template/en/default/list/change-columns.html.tmpl23
-rw-r--r--template/en/default/list/list.js.tmpl37
-rw-r--r--template/en/default/pages/release-notes.html.tmpl52
27 files changed, 489 insertions, 251 deletions
diff --git a/Bugzilla/Config/Advanced.pm b/Bugzilla/Config/Advanced.pm
index b70dcb9e3..a5ae3048a 100644
--- a/Bugzilla/Config/Advanced.pm
+++ b/Bugzilla/Config/Advanced.pm
@@ -46,7 +46,8 @@ use constant get_param_list => (
{
name => 'inbound_proxies',
type => 't',
- default => ''
+ default => '',
+ checker => \&check_ip
},
{
diff --git a/Bugzilla/Config/Common.pm b/Bugzilla/Config/Common.pm
index 9fffe02ee..00c699217 100644
--- a/Bugzilla/Config/Common.pm
+++ b/Bugzilla/Config/Common.pm
@@ -48,7 +48,7 @@ use base qw(Exporter);
qw(check_multi check_numeric check_regexp check_url check_group
check_sslbase check_priority check_severity check_platform
check_opsys check_shadowdb check_urlbase check_webdotbase
- check_user_verify_class
+ check_user_verify_class check_ip
check_mail_delivery_method check_notification check_utf8
check_bug_status check_smtp_auth check_theschwartz_available
check_maxattachmentsize check_email
@@ -129,6 +129,15 @@ sub check_sslbase {
return "";
}
+sub check_ip {
+ my $inbound_proxies = shift;
+ my @proxies = split(/[\s,]+/, $inbound_proxies);
+ foreach my $proxy (@proxies) {
+ validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address";
+ }
+ return "";
+}
+
sub check_utf8 {
my $utf8 = shift;
# You cannot turn off the UTF-8 parameter if you've already converted
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index 3d3d4a41d..78336818f 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -202,7 +202,7 @@ use Memoize;
# CONSTANTS
#
# Bugzilla version
-use constant BUGZILLA_VERSION => "4.2+";
+use constant BUGZILLA_VERSION => "4.2.1";
# Location of the remote and local XML files to track new releases.
use constant REMOTE_FILE => 'http://updates.bugzilla.org/bugzilla-update.xml';
diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm
index 874a99ce0..00ff4acc9 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -2938,7 +2938,7 @@ unsigned)
=item C<SMALLSERIAL>
-An auto-increment L</INT1>
+An auto-increment L</INT2>
=item C<MEDIUMSERIAL>
diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm
index df03dfd6d..2114bba47 100644
--- a/Bugzilla/Error.pm
+++ b/Bugzilla/Error.pm
@@ -93,6 +93,8 @@ sub _throw_error {
}
my $template = Bugzilla->template;
+ my $message;
+
if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) {
if (arecibo_should_notify($vars->{error})) {
$vars->{maintainers_notified} = 1;
@@ -111,52 +113,62 @@ sub _throw_error {
$vars->{error}, $vars->{processed}->{error_message}, $vars->{uid});
}
}
+
# There are some tests that throw and catch a lot of errors,
# and calling $template->process over and over for those errors
# is too slow. So instead, we just "die" with a dump of the arguments.
+ if (Bugzilla->error_mode != ERROR_MODE_TEST) {
+ $template->process($name, $vars, \$message)
+ || ThrowTemplateError($template->error());
+ }
+
+ # Let's call the hook first, so that extensions can override
+ # or extend the default behavior, or add their own error codes.
+ require Bugzilla::Hook;
+ Bugzilla::Hook::process('error_catch', { error => $error, vars => $vars,
+ message => \$message });
+
+ if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) {
+ print Bugzilla->cgi->header();
+ print $message;
+ }
elsif (Bugzilla->error_mode == ERROR_MODE_TEST) {
die Dumper($vars);
}
- else {
- my $message;
- $template->process($name, $vars, \$message)
- || ThrowTemplateError($template->error());
- if (Bugzilla->error_mode == ERROR_MODE_DIE) {
- die("$message\n");
+ elsif (Bugzilla->error_mode == ERROR_MODE_DIE) {
+ die("$message\n");
+ }
+ elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT
+ || Bugzilla->error_mode == ERROR_MODE_JSON_RPC)
+ {
+ # Clone the hash so we aren't modifying the constant.
+ my %error_map = %{ WS_ERROR_CODE() };
+ Bugzilla::Hook::process('webservice_error_codes',
+ { error_map => \%error_map });
+ my $code = $error_map{$error};
+ if (!$code) {
+ $code = ERROR_UNKNOWN_FATAL if $name =~ /code/i;
+ $code = ERROR_UNKNOWN_TRANSIENT if $name =~ /user/i;
}
- elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT
- || Bugzilla->error_mode == ERROR_MODE_JSON_RPC)
- {
- # Clone the hash so we aren't modifying the constant.
- my %error_map = %{ WS_ERROR_CODE() };
- require Bugzilla::Hook;
- Bugzilla::Hook::process('webservice_error_codes',
- { error_map => \%error_map });
- my $code = $error_map{$error};
- if (!$code) {
- $code = ERROR_UNKNOWN_FATAL if $name =~ /code/i;
- $code = ERROR_UNKNOWN_TRANSIENT if $name =~ /user/i;
- }
-
- if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
- die SOAP::Fault->faultcode($code)->faultstring($message);
- }
- else {
- my $server = Bugzilla->_json_server;
- # Technically JSON-RPC isn't allowed to have error numbers
- # higher than 999, but we do this to avoid conflicts with
- # the internal JSON::RPC error codes.
- $server->raise_error(code => 100000 + $code,
- message => $message,
- id => $server->{_bz_request_id},
- version => $server->version);
- # Most JSON-RPC Throw*Error calls happen within an eval inside
- # of JSON::RPC. So, in that circumstance, instead of exiting,
- # we die with no message. JSON::RPC checks raise_error before
- # it checks $@, so it returns the proper error.
- die if _in_eval();
- $server->response($server->error_response_header);
- }
+
+ if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
+ die SOAP::Fault->faultcode($code)->faultstring($message);
+ }
+ else {
+ my $server = Bugzilla->_json_server;
+ # Technically JSON-RPC isn't allowed to have error numbers
+ # higher than 999, but we do this to avoid conflicts with
+ # the internal JSON::RPC error codes.
+ $server->raise_error(code => 100000 + $code,
+ message => $message,
+ id => $server->{_bz_request_id},
+ version => $server->version);
+ # Most JSON-RPC Throw*Error calls happen within an eval inside
+ # of JSON::RPC. So, in that circumstance, instead of exiting,
+ # we die with no message. JSON::RPC checks raise_error before
+ # it checks $@, so it returns the proper error.
+ die if _in_eval();
+ $server->response($server->error_response_header);
}
}
exit;
diff --git a/Bugzilla/Hook.pm b/Bugzilla/Hook.pm
index 564c5bc49..da17946c0 100644
--- a/Bugzilla/Hook.pm
+++ b/Bugzilla/Hook.pm
@@ -127,6 +127,30 @@ This describes what hooks exist in Bugzilla currently. They are mostly
in alphabetical order, but some related hooks are near each other instead
of being alphabetical.
+=head2 admin_editusers_action
+
+This hook allows you to add additional actions to the admin Users page.
+
+Params:
+
+=over
+
+=item C<vars>
+
+You can add as many new key/value pairs as you want to this hashref.
+It will be passed to the template.
+
+=item C<action>
+
+A text which indicates the different behaviors that editusers.cgi will have.
+With this hook you can change the behavior of an action or add new actions.
+
+=item C<user>
+
+This is a Bugzilla::User object of the user.
+
+=back
+
=head2 attachment_process_data
This happens at the very beginning process of the attachment creation.
@@ -432,6 +456,41 @@ The definition is structured as:
=back
+=head2 buglist_column_joins
+
+This allows you to join additional tables to display additional columns
+in buglists. This hook is generally used in combination with the
+C<buglist_columns> hook.
+
+Params:
+
+=over
+
+=item C<column_joins> - A hashref containing data to return back to
+L<Bugzilla::Search>. This hashref contains names of the columns as keys and
+a hashref about table to join as values. This hashref has the following keys:
+
+=over
+
+=item C<table> - The name of the additional table to join.
+
+=item C<as> - (optional) The alias used for the additional table. This alias
+must not conflict with an existing alias already used in the query.
+
+=item C<from> - (optional) The name of the column in the C<bugs> table which
+the additional table should be linked to. If omitted, C<bug_id> will be used.
+
+=item C<to> - (optional) The name of the column in the additional table which
+should be linked to the column in the C<bugs> table, see C<from> above.
+If omitted, C<bug_id> will be used.
+
+=item C<join> - (optional) Either INNER or LEFT. Determine how the additional
+table should be joined with the C<bugs> table. If omitted, LEFT is used.
+
+=back
+
+=back
+
=head2 search_operator_field_override
This allows you to modify L<Bugzilla::Search/OPERATOR_FIELD_OVERRIDE>,
@@ -628,6 +687,37 @@ Params:
=back
+=head2 error_catch
+
+This hook allows extensions to catch errors thrown by Bugzilla and
+take the appropriate actions.
+
+Params:
+
+=over
+
+=item C<error>
+
+A string representing the error code thrown by Bugzilla. This string
+matches the C<error> variable in C<global/user-error.html.tmpl> and
+C<global/code-error.html.tmpl>.
+
+=item C<message>
+
+If the error mode is set to C<ERROR_MODE_WEBPAGE>, you get a reference to
+the whole HTML page with the error message in it, including its header and
+footer. If you need to extract the error message itself, you can do it by
+looking at the content of the table cell whose ID is C<error_msg>.
+If the error mode is not set to C<ERROR_MODE_WEBPAGE>, you get a reference
+to the error message itself.
+
+=item C<vars>
+
+This hash contains all the data passed to the error template. Its content
+depends on the error thrown.
+
+=back
+
=head2 flag_end_of_update
This happens at the end of L<Bugzilla::Flag/update_flags>, after all other
@@ -1354,30 +1444,6 @@ name), you can get it from here.
=back
-=head2 admin_editusers_action
-
-This hook allows you to add additional actions to the admin Users page.
-
-Params:
-
-=over
-
-=item C<vars>
-
-You can add as many new key/value pairs as you want to this hashref.
-It will be passed to the template.
-
-=item C<action>
-
-A text which indicates the different behaviors that edit_users.cgi will have.
-With this hook you can change the behavior of an action or add new actions.
-
-=item C<user>
-
-This is a Bugzilla::User object of the user.
-
-=back
-
=head2 user_preferences
This hook allows you to add additional panels to the User Preferences page,
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm
index 7d2d63284..c0a58c3f0 100644
--- a/Bugzilla/Search.pm
+++ b/Bugzilla/Search.pm
@@ -792,8 +792,8 @@ sub _param_array {
}
sub _params { $_[0]->{params} }
-
sub _user { return $_[0]->{user} }
+sub _sharer_id { $_[0]->{sharer} }
##############################
# Internal Accessors: SELECT #
@@ -959,7 +959,8 @@ sub _column_join {
my ($self, $field) = @_;
# The _realname fields require the same join as the username fields.
$field =~ s/_realname$//;
- my $join_info = COLUMN_JOINS->{$field};
+ my $column_joins = $self->_get_column_joins();
+ my $join_info = $column_joins->{$field};
if ($join_info) {
# Don't allow callers to modify the constant.
$join_info = dclone($join_info);
@@ -1800,6 +1801,20 @@ sub _get_operator_field_override {
return $cache->{operator_field_override};
}
+sub _get_column_joins {
+ my $self = shift;
+ my $cache = Bugzilla->request_cache;
+
+ return $cache->{column_joins} if defined $cache->{column_joins};
+
+ my %column_joins = %{ COLUMN_JOINS() };
+ Bugzilla::Hook::process('buglist_column_joins',
+ { column_joins => \%column_joins });
+
+ $cache->{column_joins} = \%column_joins;
+ return $cache->{column_joins};
+}
+
###########################
# Search Function Helpers #
###########################
@@ -1912,16 +1927,22 @@ sub _timestamp_translate {
my $value = $args->{value};
my $dbh = Bugzilla->dbh;
- return if $value !~ /^[\+\-]?\d+[hdwmy]s?$/i;
-
- $args->{value} = SqlifyDate($value);
- $args->{quoted} = $dbh->quote($args->{value});
+ return if $value !~ /^(?:[\+\-]?\d+[hdwmy]s?|now)$/i;
+
+ # By default, the time is appended to the date, which we don't want
+ # for deadlines.
+ $value = SqlifyDate($value);
+ if ($args->{field} eq 'deadline') {
+ ($value) = split(/\s/, $value);
+ }
+ $args->{value} = $value;
+ $args->{quoted} = $dbh->quote($value);
}
sub SqlifyDate {
my ($str) = @_;
my $fmt = "%Y-%m-%d %H:%M:%S";
- $str = "" if !defined $str;
+ $str = "" if (!defined $str || lc($str) eq 'now');
if ($str eq "") {
my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime(time());
return sprintf("%4d-%02d-%02d 00:00:00", $year+1900, $month+1, $mday);
@@ -2549,8 +2570,8 @@ sub _multiselect_table {
}
elsif ($field eq 'tag') {
$args->{full_field} = 'tag.name';
- return "bug_tag INNER JOIN tag ON bug_tag.tag_id = tag.id"
- . " AND user_id = " . $self->_user->id;
+ return "bug_tag INNER JOIN tag ON bug_tag.tag_id = tag.id AND user_id = "
+ . ($self->_sharer_id || $self->_user->id);
}
elsif ($field eq 'bug_group') {
$args->{full_field} = 'groups.name';
diff --git a/Bugzilla/Search/Quicksearch.pm b/Bugzilla/Search/Quicksearch.pm
index 563d2387c..54592f07e 100644
--- a/Bugzilla/Search/Quicksearch.pm
+++ b/Bugzilla/Search/Quicksearch.pm
@@ -129,7 +129,7 @@ use constant COMPONENT_EXCEPTIONS => (
);
# Quicksearch-wide globals for boolean charts.
-our ($chart, $and, $or, $fulltext);
+our ($chart, $and, $or, $fulltext, $bug_status_set);
sub quicksearch {
my ($searchstring) = (@_);
@@ -199,7 +199,8 @@ sub quicksearch {
}
}
- _handle_status_and_resolution(\@qswords);
+ _handle_status_and_resolution($qswords[0]);
+ shift(@qswords) if $bug_status_set;
my (@unknownFields, %ambiguous_fields);
$fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0;
@@ -233,6 +234,12 @@ sub quicksearch {
$or = 0;
}
+ # If there is no mention of a bug status, we restrict the query
+ # to open bugs by default.
+ unless ($bug_status_set) {
+ $cgi->param('bug_status', BUG_STATE_OPEN);
+ }
+
# Inform user about any unknown fields
if (scalar(@unknownFields) || scalar(keys %ambiguous_fields)) {
ThrowUserError("quicksearch_unknown_field",
@@ -303,48 +310,26 @@ sub _handle_alias {
}
sub _handle_status_and_resolution {
- my ($words) = @_;
+ my $word = shift;
my $legal_statuses = get_legal_field_values('bug_status');
- my $legal_resolutions = get_legal_field_values('resolution');
-
- my @openStates = BUG_STATE_OPEN;
- my @closedStates;
my (%states, %resolutions);
+ $bug_status_set = 1;
- foreach (@$legal_statuses) {
- push(@closedStates, $_) unless is_open_state($_);
- }
- foreach (@openStates) { $states{$_} = 1 }
- if ($words->[0] eq 'ALL') {
- foreach (@$legal_statuses) { $states{$_} = 1 }
- shift @$words;
+ if ($word eq 'OPEN') {
+ $states{$_} = 1 foreach BUG_STATE_OPEN;
}
- elsif ($words->[0] eq 'OPEN') {
- shift @$words;
- }
- elsif ($words->[0] =~ /^[A-Z_]+(,[_A-Z]+)*$/) {
- # e.g. CON,IN_PR,FIX
- undef %states;
- if (matchPrefixes(\%states,
- \%resolutions,
- [split(/,/, $words->[0])],
- $legal_statuses,
- $legal_resolutions)) {
- shift @$words;
- }
- else {
- # Carry on if no match found
- foreach (@openStates) { $states{$_} = 1 }
- }
- }
- else {
- # Default: search for unresolved bugs only.
- # Put custom code here if you would like to change this behaviour.
+ # If we want all bugs, then there is nothing to do.
+ elsif ($word ne 'ALL'
+ && !matchPrefixes(\%states, \%resolutions, $word, $legal_statuses))
+ {
+ $bug_status_set = 0;
}
# If we have wanted resolutions, allow closed states
if (keys(%resolutions)) {
- foreach (@closedStates) { $states{$_} = 1 }
+ foreach my $status (@$legal_statuses) {
+ $states{$status} = 1 unless is_open_state($status);
+ }
}
Bugzilla->cgi->param('bug_status', keys(%states));
@@ -416,6 +401,9 @@ sub _handle_field_names {
$ambiguous_fields->{$field} = $translated;
}
else {
+ if ($translated eq 'bug_status' || $translated eq 'resolution') {
+ $bug_status_set = 1;
+ }
foreach my $value (@values) {
my $operator = FIELD_OPERATOR->{$translated} || 'substring';
# If the string was quoted to protect some special
@@ -575,14 +563,14 @@ sub _matches_phrase {
# Expand found prefixes to states or resolutions
sub matchPrefixes {
- my $hr_states = shift;
- my $hr_resolutions = shift;
- my $ar_prefixes = shift;
- my $ar_check_states = shift;
- my $ar_check_resolutions = shift;
+ my ($hr_states, $hr_resolutions, $word, $ar_check_states) = @_;
+ return unless $word =~ /^[A-Z_]+(,[A-Z_]+)*$/;
+
+ my @ar_prefixes = split(/,/, $word);
+ my $ar_check_resolutions = get_legal_field_values('resolution');
my $foundMatch = 0;
- foreach my $prefix (@$ar_prefixes) {
+ foreach my $prefix (@ar_prefixes) {
foreach (@$ar_check_states) {
if (/^$prefix/) {
$$hr_states{$_} = 1;
diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm
index 7ecaddc88..c2dbdc97d 100644
--- a/Bugzilla/Util.pm
+++ b/Bugzilla/Util.pm
@@ -35,7 +35,7 @@ use base qw(Exporter);
detaint_signed
html_quote url_quote xml_quote
css_class_quote html_light_quote
- i_am_cgi correct_urlbase remote_ip
+ i_am_cgi correct_urlbase remote_ip validate_ip
do_ssl_redirect_if_required use_attachbase
diff_arrays on_main_db
trim wrap_hard wrap_comment find_wrap_point
@@ -285,12 +285,103 @@ sub correct_urlbase {
sub remote_ip {
my $ip = $ENV{'REMOTE_ADDR'} || '127.0.0.1';
my @proxies = split(/[\s,]+/, Bugzilla->params->{'inbound_proxies'});
- if (first { $_ eq $ip } @proxies) {
- $ip = $ENV{'HTTP_X_FORWARDED_FOR'} if $ENV{'HTTP_X_FORWARDED_FOR'};
+
+ # If the IP address is one of our trusted proxies, then we look at
+ # the X-Forwarded-For header to determine the real remote IP address.
+ if ($ENV{'HTTP_X_FORWARDED_FOR'} && first { $_ eq $ip } @proxies) {
+ my @ips = split(/[\s,]+/, $ENV{'HTTP_X_FORWARDED_FOR'});
+ # This header can contain several IP addresses. We want the
+ # IP address of the machine which connected to our proxies as
+ # all other IP addresses may be fake or internal ones.
+ # Note that this may block a whole external proxy, but we have
+ # no way to determine if this proxy is malicious or trustable.
+ foreach my $remote_ip (reverse @ips) {
+ if (!first { $_ eq $remote_ip } @proxies) {
+ # Keep the original IP address if the remote IP is invalid.
+ $ip = validate_ip($remote_ip) || $ip;
+ last;
+ }
+ }
}
return $ip;
}
+sub validate_ip {
+ my $ip = shift;
+ return is_ipv4($ip) || is_ipv6($ip);
+}
+
+# Copied from Data::Validate::IP::is_ipv4().
+sub is_ipv4 {
+ my $ip = shift;
+ return unless defined $ip;
+
+ my @octets = $ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
+ return unless scalar(@octets) == 4;
+
+ foreach my $octet (@octets) {
+ return unless ($octet >= 0 && $octet <= 255 && $octet !~ /^0\d{1,2}$/);
+ }
+
+ # The IP address is valid and can now be detainted.
+ return join('.', @octets);
+}
+
+# Copied from Data::Validate::IP::is_ipv6().
+sub is_ipv6 {
+ my $ip = shift;
+ return unless defined $ip;
+
+ # If there is a :: then there must be only one :: and the length
+ # can be variable. Without it, the length must be 8 groups.
+ my @chunks = split(':', $ip);
+
+ # Need to check if the last chunk is an IPv4 address, if it is we
+ # pop it off and exempt it from the normal IPv6 checking and stick
+ # it back on at the end. If there is only one chunk and it's an IPv4
+ # address, then it isn't an IPv6 address.
+ my $ipv4;
+ my $expected_chunks = 8;
+ if (@chunks > 1 && is_ipv4($chunks[$#chunks])) {
+ $ipv4 = pop(@chunks);
+ $expected_chunks--;
+ }
+
+ my $empty = 0;
+ # Workaround to handle trailing :: being valid.
+ if ($ip =~ /[0-9a-f]{1,4}::$/) {
+ $empty++;
+ # Single trailing ':' is invalid.
+ } elsif ($ip =~ /:$/) {
+ return;
+ }
+
+ foreach my $chunk (@chunks) {
+ return unless $chunk =~ /^[0-9a-f]{0,4}$/i;
+ $empty++ if $chunk eq '';
+ }
+ # More than one :: block is bad, but if it starts with :: it will
+ # look like two, so we need an exception.
+ if ($empty == 2 && $ip =~ /^::/) {
+ # This is ok
+ } elsif ($empty > 1) {
+ return;
+ }
+
+ push(@chunks, $ipv4) if $ipv4;
+ # Need 8 chunks, or we need an empty section that could be filled
+ # to represent the missing '0' sections.
+ return unless (@chunks == $expected_chunks || @chunks < $expected_chunks && $empty);
+
+ my $ipv6 = join(':', @chunks);
+ # The IP address is valid and can now be detainted.
+ trick_taint($ipv6);
+
+ # Need to handle the exception of trailing :: being valid.
+ return "${ipv6}::" if $ip =~ /::$/;
+ return $ipv6;
+}
+
sub use_attachbase {
my $attachbase = Bugzilla->params->{'attachment_base'};
return ($attachbase ne ''
@@ -884,6 +975,17 @@ in a command-line script.
Returns either the C<sslbase> or C<urlbase> parameter, depending on the
current setting for the C<ssl_redirect> parameter.
+=item C<remote_ip()>
+
+Returns the IP address of the remote client. If Bugzilla is behind
+a trusted proxy, it will get the remote IP address by looking at the
+X-Forwarded-For header.
+
+=item C<validate_ip($ip)>
+
+Returns the sanitized IP address if it is a valid IPv4 or IPv6 address,
+else returns undef.
+
=item C<use_attachbase()>
Returns true if an alternate host is used to display attachments; false
diff --git a/buglist.cgi b/buglist.cgi
index d2f9a6904..6236a5669 100755
--- a/buglist.cgi
+++ b/buglist.cgi
@@ -113,16 +113,6 @@ if (defined $cgi->param('ctype') && $cgi->param('ctype') eq "rss") {
$cgi->param('ctype', "atom");
}
-# The js ctype presents a security risk; a malicious site could use it
-# to gather information about secure bugs. So, we only allow public bugs to be
-# retrieved with this format.
-#
-# Note that if and when this call clears cookies or has other persistent
-# effects, we'll need to do this another way instead.
-if ((defined $cgi->param('ctype')) && ($cgi->param('ctype') eq "js")) {
- Bugzilla->logout_request();
-}
-
# An agent is a program that automatically downloads and extracts data
# on its user's behalf. If this request comes from an agent, we turn off
# various aspects of bug list functionality so agent requests succeed
@@ -229,7 +219,9 @@ sub LookupNamedQuery {
$query->url
|| ThrowUserError("buglist_parameters_required");
- return wantarray ? ($query->url, $query->id) : $query->url;
+ # Detaint $sharer_id.
+ $sharer_id = $query->user->id if $sharer_id;
+ return wantarray ? ($query->url, $query->id, $sharer_id) : $query->url;
}
# Inserts a Named Query (a "Saved Search") into the database, or
@@ -348,6 +340,7 @@ sub _close_standby_message {
my $cmdtype = $cgi->param('cmdtype') || '';
my $remaction = $cgi->param('remaction') || '';
+my $sharer_id;
# Backwards-compatibility - the old interface had cmdtype="runnamed" to run
# a named command, and we can't break this because it's in bookmarks.
@@ -384,8 +377,9 @@ $filename =~ s/"/\\"/g; # escape quotes
if ($cmdtype eq "dorem") {
if ($remaction eq "run") {
my $query_id;
- ($buffer, $query_id) = LookupNamedQuery(scalar $cgi->param("namedcmd"),
- scalar $cgi->param('sharer_id'));
+ ($buffer, $query_id, $sharer_id) =
+ LookupNamedQuery(scalar $cgi->param("namedcmd"),
+ scalar $cgi->param('sharer_id'));
# If this is the user's own query, remember information about it
# so that it can be modified easily.
$vars->{'searchname'} = $cgi->param('namedcmd');
@@ -775,7 +769,8 @@ if ($format->{'extension'} eq 'html' && !defined $params->param('limit')) {
# Generate the basic SQL query that will be used to generate the bug list.
my $search = new Bugzilla::Search('fields' => \@selectcolumns,
'params' => scalar $params->Vars,
- 'order' => \@orderstrings);
+ 'order' => \@orderstrings,
+ 'sharer' => $sharer_id);
my $query = $search->sql;
$vars->{'search_description'} = $search->search_description;
diff --git a/docs/en/xml/Bugzilla-Guide.xml b/docs/en/xml/Bugzilla-Guide.xml
index db33e8d57..bfb1146d9 100644
--- a/docs/en/xml/Bugzilla-Guide.xml
+++ b/docs/en/xml/Bugzilla-Guide.xml
@@ -32,9 +32,9 @@
For a devel release, simple bump bz-ver and bz-date
-->
-<!ENTITY bz-ver "4.2">
+<!ENTITY bz-ver "4.2.1">
<!ENTITY bz-nextver "4.4">
-<!ENTITY bz-date "2012-02-22">
+<!ENTITY bz-date "2012-04-18">
<!ENTITY current-year "2012">
<!ENTITY landfillbase "http://landfill.bugzilla.org/bugzilla-4.2-branch/">
diff --git a/docs/en/xml/using.xml b/docs/en/xml/using.xml
index 6d575c0c1..3bf0558fc 100644
--- a/docs/en/xml/using.xml
+++ b/docs/en/xml/using.xml
@@ -659,16 +659,6 @@
</member>
</simplelist>
</para>
-
- <para>
- If you would like to access the bug list from another program
- it is often useful to have the list returned in something other
- than HTML. By adding the ctype=type parameter into the bug list URL
- you can specify several alternate formats. Besides the types described
- above, the following formats are also supported: ECMAScript, also known
- as JavaScript (ctype=js), and Resource Description Framework RDF/XML
- (ctype=rdf).
- </para>
</section>
<section id="individual-buglists">
diff --git a/extensions/Example/Extension.pm b/extensions/Example/Extension.pm
index af56b506b..885a8e8ff 100644
--- a/extensions/Example/Extension.pm
+++ b/extensions/Example/Extension.pm
@@ -196,6 +196,22 @@ sub buglist_columns {
my $columns = $args->{'columns'};
$columns->{'example'} = { 'name' => 'bugs.delta_ts' , 'title' => 'Example' };
+ $columns->{'product_desc'} = { 'name' => 'prod_desc.description',
+ 'title' => 'Product Description' };
+}
+
+sub buglist_column_joins {
+ my ($self, $args) = @_;
+ my $joins = $args->{'column_joins'};
+
+ # This column is added using the "buglist_columns" hook
+ $joins->{'product_desc'} = {
+ from => 'product_id',
+ to => 'id',
+ table => 'products',
+ as => 'prod_desc',
+ join => 'INNER',
+ };
}
sub search_operator_field_override {
@@ -339,6 +355,25 @@ sub enter_bug_entrydefaultvars {
$vars->{'example'} = 1;
}
+sub error_catch {
+ my ($self, $args) = @_;
+ # Customize the error message displayed when someone tries to access
+ # page.cgi with an invalid page ID, and keep track of this attempt
+ # in the web server log.
+ return unless Bugzilla->error_mode == ERROR_MODE_WEBPAGE;
+ return unless $args->{error} eq 'bad_page_cgi_id';
+
+ my $page_id = $args->{vars}->{page_id};
+ my $login = Bugzilla->user->identity || "Someone";
+ warn "$login attempted to access page.cgi with id = $page_id";
+
+ my $page = $args->{message};
+ my $new_error_msg = "Ah ah, you tried to access $page_id? Good try!";
+ $new_error_msg = html_quote($new_error_msg);
+ # There are better tools to parse an HTML page, but it's just an example.
+ $$page =~ s/(?<=<td id="error_msg" class="throw_error">).*(?=<\/td>)/$new_error_msg/si;
+}
+
sub flag_end_of_update {
my ($self, $args) = @_;
diff --git a/extensions/Voting/Extension.pm b/extensions/Voting/Extension.pm
index 26fb58fa3..3fb799bbf 100644
--- a/extensions/Voting/Extension.pm
+++ b/extensions/Voting/Extension.pm
@@ -499,7 +499,10 @@ sub _page_user {
}
}
- $dbh->do('DELETE FROM votes WHERE vote_count <= 0');
+ if ($canedit && $bug) {
+ $dbh->do('DELETE FROM votes WHERE vote_count = 0 AND who = ?',
+ undef, $who->id);
+ }
$dbh->bz_commit_transaction();
$vars->{'canedit'} = $canedit;
diff --git a/js/comments.js b/js/comments.js
index 28ef54397..e9a3e209f 100644
--- a/js/comments.js
+++ b/js/comments.js
@@ -67,13 +67,11 @@ function toggle_all_comments(action) {
function collapse_comment(link, comment) {
link.innerHTML = "[+]";
- link.title = "Expand the comment.";
YAHOO.util.Dom.addClass(comment, 'collapsed');
}
function expand_comment(link, comment) {
link.innerHTML = "[-]";
- link.title = "Collapse the comment";
YAHOO.util.Dom.removeClass(comment, 'collapsed');
}
@@ -127,11 +125,11 @@ function wrapReplyText(text) {
/* This way, we are sure that browsers which do not support JS
* won't display this link */
-function addCollapseLink(count) {
+function addCollapseLink(count, title) {
document.write(' <a href="#" class="bz_collapse_comment"' +
' id="comment_link_' + count +
'" onclick="toggle_comment_display(this, ' + count +
- '); return false;" title="Collapse the comment.">[-]<\/a> ');
+ '); return false;" title="' + title + '">[-]<\/a> ');
}
function goto_add_comments( anchor ){
diff --git a/js/field.js b/js/field.js
index 8353100f0..e3fe460cf 100644
--- a/js/field.js
+++ b/js/field.js
@@ -218,12 +218,12 @@ function setupEditLink(id) {
hideEditableField(link_container, input_container, link);
}
-/* Hide input fields and show the text with (edit) next to it */
+/* Hide input/select fields and show the text with (edit) next to it */
function hideEditableField( container, input, action, field_id, original_value, new_value ) {
YAHOO.util.Dom.removeClass(container, 'bz_default_hidden');
YAHOO.util.Dom.addClass(input, 'bz_default_hidden');
YAHOO.util.Event.addListener(action, 'click', showEditableField,
- new Array(container, input, new_value));
+ new Array(container, input, field_id, new_value));
if(field_id != ""){
YAHOO.util.Event.addListener(window, 'load', checkForChangedFieldValues,
new Array(container, input, field_id, original_value));
@@ -231,13 +231,14 @@ function hideEditableField( container, input, action, field_id, original_value,
}
/* showEditableField (e, ContainerInputArray)
- * Function hides the (edit) link and the text and displays the input
+ * Function hides the (edit) link and the text and displays the input/select field
*
* var e: the event
* var ContainerInputArray: An array containing the (edit) and text area and the input being displayed
* var ContainerInputArray[0]: the container that will be hidden usually shows the (edit) or (take) text
* var ContainerInputArray[1]: the input area and label that will be displayed
- * var ContainerInputArray[2]: the new value to set the input field to when (take) is clicked
+ * var ContainerInputArray[2]: the input/select field id for which the new value must be set
+ * var ContainerInputArray[3]: the new value to set the input/select field to when (take) is clicked
*/
function showEditableField (e, ContainerInputArray) {
var inputs = new Array();
@@ -250,6 +251,8 @@ function showEditableField (e, ContainerInputArray) {
YAHOO.util.Dom.removeClass(inputArea, 'bz_default_hidden');
if ( inputArea.tagName.toLowerCase() == "input" ) {
inputs.push(inputArea);
+ } else if (ContainerInputArray[2]) {
+ inputs.push(document.getElementById(ContainerInputArray[2]));
} else {
inputs = inputArea.getElementsByTagName('input');
if ( inputs.length == 0 )
@@ -258,12 +261,24 @@ function showEditableField (e, ContainerInputArray) {
if ( inputs.length > 0 ) {
// Change the first field's value to ContainerInputArray[2]
// if present before focusing.
- if (ContainerInputArray[2]) {
- inputs[0].value = ContainerInputArray[2];
+ var type = inputs[0].tagName.toLowerCase();
+ if (ContainerInputArray[3]) {
+ if ( type == "input" ) {
+ inputs[0].value = ContainerInputArray[3];
+ } else {
+ for (var i = 0; inputs[0].length; i++) {
+ if ( inputs[0].options[i].value == ContainerInputArray[3] ) {
+ inputs[0].options[i].selected = true;
+ break;
+ }
+ }
+ }
}
// focus on the first field, this makes it easier to edit
inputs[0].focus();
- inputs[0].select();
+ if ( type == "input" ) {
+ inputs[0].select();
+ }
}
YAHOO.util.Event.preventDefault(e);
}
diff --git a/query.cgi b/query.cgi
index 0502f6c27..b3b9aa443 100755
--- a/query.cgi
+++ b/query.cgi
@@ -188,6 +188,7 @@ foreach my $val (editable_bug_fields()) {
if (Bugzilla->user->is_timetracker) {
push @chfields, "work_time";
} else {
+ @chfields = grep($_ ne "deadline", @chfields);
@chfields = grep($_ ne "estimated_time", @chfields);
@chfields = grep($_ ne "remaining_time", @chfields);
}
diff --git a/skins/standard/global.css b/skins/standard/global.css
index 47cebdeac..f95d4e644 100644
--- a/skins/standard/global.css
+++ b/skins/standard/global.css
@@ -413,6 +413,12 @@ dl dl > dt {
white-space: normal !important;
}
+/* Arrow buttons are buttons with only &uarr;, &darr;, &larr; or &rarr; on
+ * them. We want these to look a little less spidery. */
+.arrow_button {
+ font-size: 150%;
+}
+
/* Style of the attachment table and time tracking table */
#attachment_table {
border-collapse: collapse;
@@ -564,30 +570,3 @@ input.required, select.required, span.required_explanation {
border: 2px solid #B70000;
background-color: #FFEBEB;
}
-
-/*****************/
-/* colchange.cgi */
-/*****************/
-
-.image_button {
- background-repeat: no-repeat;
- background-position: center center;
- width: 30px;
- height: 20px;
-}
-
-#select_button {
- background-image: url(global/right.png);
-}
-
-#deselect_button {
- background-image: url(global/left.png);
-}
-
-#up_button {
- background-image: url(global/up.png);
-}
-
-#down_button {
- background-image: url(global/down.png);
-}
diff --git a/skins/standard/global/down.png b/skins/standard/global/down.png
deleted file mode 100644
index 78a9e631a..000000000
--- a/skins/standard/global/down.png
+++ /dev/null
Binary files differ
diff --git a/skins/standard/global/left.png b/skins/standard/global/left.png
deleted file mode 100644
index f8cb2b2dd..000000000
--- a/skins/standard/global/left.png
+++ /dev/null
Binary files differ
diff --git a/skins/standard/global/right.png b/skins/standard/global/right.png
deleted file mode 100644
index d02b707a6..000000000
--- a/skins/standard/global/right.png
+++ /dev/null
Binary files differ
diff --git a/skins/standard/global/up.png b/skins/standard/global/up.png
deleted file mode 100644
index 240d483df..000000000
--- a/skins/standard/global/up.png
+++ /dev/null
Binary files differ
diff --git a/template/en/default/bug/comments.html.tmpl b/template/en/default/bug/comments.html.tmpl
index 7b305a248..f9d7f0cf0 100644
--- a/template/en/default/bug/comments.html.tmpl
+++ b/template/en/default/bug/comments.html.tmpl
@@ -176,7 +176,7 @@
[% END %]
>reply</a>]
<script type="text/javascript"><!--
- addCollapseLink([% count %]); // -->
+ addCollapseLink([% count %], 'Toggle comment display'); // -->
</script>
</span>
[% END %]
diff --git a/template/en/default/bug/field.html.tmpl b/template/en/default/bug/field.html.tmpl
index a7e318c4d..4ed686248 100644
--- a/template/en/default/bug/field.html.tmpl
+++ b/template/en/default/bug/field.html.tmpl
@@ -226,22 +226,31 @@
'keyword_autocomplete');
</script>
[% END %]
-[% ELSIF field.type == constants.FIELD_TYPE_TEXTAREA %]
- <div class="uneditable_textarea">[% value FILTER html %]</div>
-[% ELSIF field.type == constants.FIELD_TYPE_BUG_ID %]
- [% IF value %]
- [% value FILTER bug_link(value, use_alias => 1) FILTER none %]
- [% END %]
-[% ELSIF field.type == constants.FIELD_TYPE_BUG_URLS %]
- [% '<ul class="bug_urls">' IF value.size %]
- [% FOREACH bug_url = value %]
- <li>
- [% PROCESS bug_url_link bug_url = bug_url %]
- </li>
- [% END %]
- [% '</ul>' IF value.size %]
[% ELSE %]
- [% value.join(', ') FILTER html %]
+ [% SWITCH field.type %]
+ [% CASE constants.FIELD_TYPE_TEXTAREA %]
+ <div class="uneditable_textarea">[% value FILTER html %]</div>
+ [% CASE constants.FIELD_TYPE_BUG_ID %]
+ [% IF value %]
+ [% value FILTER bug_link(value, use_alias => 1) FILTER none %]
+ [% END %]
+ [% CASE [ constants.FIELD_TYPE_SINGLE_SELECT
+ constants.FIELD_TYPE_MULTI_SELECT ] %]
+ [% FOREACH val = value %]
+ [% display_value(field.name, val) FILTER html %]
+ [% ', ' UNLESS loop.last() %]
+ [% END %]
+ [% CASE constants.FIELD_TYPE_BUG_URLS %]
+ [% '<ul class="bug_urls">' IF value.size %]
+ [% FOREACH bug_url = value %]
+ <li>
+ [% PROCESS bug_url_link bug_url = bug_url %]
+ </li>
+ [% END %]
+ [% '</ul>' IF value.size %]
+ [% CASE %]
+ [% value.join(', ') FILTER html %]
+ [% END %]
[% END %]
[% Hook.process('end_field_column') %]
[% '</td>' IF NOT no_tds %]
diff --git a/template/en/default/list/change-columns.html.tmpl b/template/en/default/list/change-columns.html.tmpl
index b13055c38..ff7e5d371 100644
--- a/template/en/default/list/change-columns.html.tmpl
+++ b/template/en/default/list/change-columns.html.tmpl
@@ -57,8 +57,7 @@
<tr>
<th><div id="avail_header" class="bz_default_hidden">Available Columns</div></th>
<th></th>
- <th>Selected Columns</th>
- <th></th>
+ <th colspan="2">Selected Columns</th>
</tr>
<tr>
<td>
@@ -68,11 +67,13 @@
</select>
</td>
<td>
- <input class="image_button bz_default_hidden" type="button"
- id="select_button" name="select" onclick="move_select()">
+ <button type="button" id="select_button" name="select"
+ class="bz_default_hidden arrow_button"
+ onclick="move_select()">&rarr;</button>
<br><br>
- <input class="image_button bz_default_hidden" type="button"
- id="deselect_button" name="deselect" onclick="move_deselect()">
+ <button type="button" id="deselect_button" name="deselect"
+ class="bz_default_hidden arrow_button"
+ onclick="move_deselect()">&larr;</button>
</td>
<td>
<select name="selected_columns" id="selected_columns"
@@ -93,11 +94,13 @@
</select>
</td>
<td>
- <input class="image_button bz_default_hidden" type="button"
- id="up_button" name="up" onclick="move_up()">
+ <button type="button" id="up_button" name="up"
+ class="bz_default_hidden arrow_button"
+ onclick="move_up()">&uarr;</button>
<br><br>
- <input class="image_button bz_default_hidden" type="button"
- id="down_button" name="down" onclick="move_down()">
+ <button type="button" id="down_button" name="down"
+ class="bz_default_hidden arrow_button"
+ onclick="move_down()">&darr;</button>
</td>
</tr>
</table>
diff --git a/template/en/default/list/list.js.tmpl b/template/en/default/list/list.js.tmpl
deleted file mode 100644
index 7e9664c43..000000000
--- a/template/en/default/list/list.js.tmpl
+++ /dev/null
@@ -1,37 +0,0 @@
-[%# The contents of this file are subject to the Mozilla Public
- # License Version 1.1 (the "License"); you may not use this file
- # except in compliance with the License. You may obtain a copy of
- # the License at http://www.mozilla.org/MPL/
- #
- # Software distributed under the License is distributed on an "AS
- # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
- # implied. See the License for the specific language governing
- # rights and limitations under the License.
- #
- # The Original Code is the Bugzilla Bug Tracking System.
- #
- # The Initial Developer of the Original Code is Netscape Communications
- # Corporation. Portions created by Netscape are
- # Copyright (C) 1998 Netscape Communications Corporation. All
- # Rights Reserved.
- #
- # Contributor(s): Gervase Markham <gerv@gerv.net>
- #%]
-
-// Note: only publicly-accessible bugs (those not in any group) will be
-// listed when using this JavaScript format. This is to prevent malicious
-// sites stealing information about secure bugs.
-
-bugs = new Array;
-
-[% FOREACH bug = bugs %]
- bugs[[% bug.bug_id %]] = [
- [% FOREACH column = displaycolumns %]
- "[%- bug.$column FILTER js -%]"[% "," UNLESS loop.last %]
- [% END %]
- ];
-[% END %]
-
-if (window.buglistCallback) {
- buglistCallback(bugs);
-}
diff --git a/template/en/default/pages/release-notes.html.tmpl b/template/en/default/pages/release-notes.html.tmpl
index 6402814e1..3cba64406 100644
--- a/template/en/default/pages/release-notes.html.tmpl
+++ b/template/en/default/pages/release-notes.html.tmpl
@@ -29,7 +29,7 @@
<ul class="bz_toc">
<li><a href="#v42_introduction">Introduction</a></li>
- <!-- li><a href="#v42_point">Updates in this 4.2.x Release</a></li -->
+ <li><a href="#v42_point">Updates in this 4.2.x Release</a></li>
<li><a href="#v42_req">Minimum Requirements</a></li>
<li><a href="#v42_feat">New Features and Improvements</a></li>
<li><a href="#v42_issues">Outstanding Issues</a></li>
@@ -51,8 +51,56 @@
in between your version and this one, <strong>particularly the Upgrading
section of each version's release notes</strong>.</p>
-<!-- h2 id="v42_point">Updates in this 4.2.x Release</h2 -->
+<h2 id="v42_point">Updates in this 4.2.x Release</h2>
+<h3>4.2.1</h3>
+
+<p>This release fixes two security issues. See the
+ <a href="http://www.bugzilla.org/security/3.6.8/">Security Advisory</a>
+ for details.</p>
+
+<p>In addition, the following important fixes/changes have been made in this
+ release:</p>
+
+<ul>
+ <li>Due to a regression introduced when fixing CVE-2012-0453, if an XML-RPC
+ client sets the charset as part of its Content-Type header, we were
+ incorrectly rejecting the request. The header is now correctly parsed.
+ (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=731219">[% terms.Bug %] 731219</a>)</li>
+ <li>Email notifications about status changes in blockers were incorrectly
+ formatted. Several pieces of text were missing in the emails.
+ (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=731586">[% terms.Bug %] 731586</a>)</li>
+ <li>Many [% terms.bugs %] related to the searching system have been fixed.
+ (<a href="https://bugzilla.mozilla.org/buglist.cgi?bug_id=58179,715270,730984,731163,737436,745320">
+ [% terms.Bugs %] 58179, 715270, 730984, 731163, 737436 and 745320</a>)</li>
+ <li>When using the QuickSearch box, complex queries are now parsed correctly.
+ It also behaves correctly with non-ASCII characters (such as é, ä, ü, etc.).
+ (<a href="https://bugzilla.mozilla.org/buglist.cgi?bug_id=554819,663377,730207">
+ [% terms.Bugs %] 554819, 663377 and 730207</a>)</li>
+ <li>The 'take' link besides the assignee field now works correctly when
+ the <kbd>usemenuforusers</kbd> parameter is turned on.
+ (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=734997">[% terms.Bug %] 734997</a>)</li>
+ <li>URLs in the 'Total' row at the bottom of tabular reports were broken
+ when JavaScript was enabled and a user field was used for the vertical
+ axis.
+ (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=731323">[% terms.Bug %] 731323</a>)</li>
+ <li>Some performance problems have been fixed for installations with many
+ products, components or versions.
+ (<a href="https://bugzilla.mozilla.org/buglist.cgi?bug_id=695514,731055">
+ [% terms.Bugs %] 695514 and 731055</a>)</li>
+ <li>A new hook named <kbd>buglist_column_joins</kbd> has been added to let
+ extensions alter the <kbd>Bugzilla::Search::COLUMN_JOINS</kbd> hash.
+ Now more fields can be displayed as columns in buglists, in combination
+ with the already existing <kbd>buglist_columns</kbd> hook.
+ (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=743991">[% terms.Bug %] 743991</a>)</li>
+ <li>A new hook named <kbd>error_catch</kbd> has been added to let extensions
+ alter the way errors are thrown.
+ (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=745197">[% terms.Bug %] 745197</a>)</li>
+ <li>A new hook named <kbd>admin_editusers_action</kbd> has been added to let
+ extensions alter the behavior of <kbd>editusers.cgi</kbd>. This lets you add
+ new features to this script very easily.
+ (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=730794">[% terms.Bug %] 730794</a>)</li>
+</ul>
<h2 id="v42_req">Minimum Requirements</h2>