diff options
Diffstat (limited to 'Bugzilla')
-rw-r--r-- | Bugzilla/Config/Advanced.pm | 3 | ||||
-rw-r--r-- | Bugzilla/Config/Common.pm | 11 | ||||
-rw-r--r-- | Bugzilla/Constants.pm | 2 | ||||
-rw-r--r-- | Bugzilla/DB/Schema.pm | 2 | ||||
-rw-r--r-- | Bugzilla/Error.pm | 90 | ||||
-rw-r--r-- | Bugzilla/Hook.pm | 114 | ||||
-rw-r--r-- | Bugzilla/Search.pm | 39 | ||||
-rw-r--r-- | Bugzilla/Search/Quicksearch.pm | 72 | ||||
-rw-r--r-- | Bugzilla/Util.pm | 108 |
9 files changed, 320 insertions, 121 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 |