diff options
Diffstat (limited to 'Bugzilla')
32 files changed, 1691 insertions, 215 deletions
diff --git a/Bugzilla/Arecibo.pm b/Bugzilla/Arecibo.pm new file mode 100644 index 000000000..974594b72 --- /dev/null +++ b/Bugzilla/Arecibo.pm @@ -0,0 +1,335 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Arecibo; + +use strict; +use warnings; + +use base qw(Exporter); +our @EXPORT = qw( + arecibo_handle_error + arecibo_generate_id + arecibo_should_notify +); + +use Apache2::Log; +use Apache2::SubProcess; +use Carp; +use Email::Date::Format qw(email_gmdate); +use LWP::UserAgent; +use POSIX qw(setsid nice); +use Sys::Hostname; + +use Bugzilla::Constants; +use Bugzilla::Util; +use Bugzilla::WebService::Constants; + +use constant CONFIG => { + # 'types' maps from the error message to types and priorities + types => [ + { + type => 'the_schwartz', + boost => -10, + match => [ + qr/TheSchwartz\.pm/, + ], + }, + { + type => 'database_error', + boost => -10, + match => [ + qr/DBD::mysql/, + qr/Can't connect to the database/, + ], + }, + { + type => 'patch_reader', + boost => +5, + match => [ + qr#/PatchReader/#, + ], + }, + { + type => 'uninitialized_warning', + boost => 0, + match => [ + qr/Use of uninitialized value/, + ], + }, + ], + + # 'codes' lists the code-errors which are sent to arecibo + codes => [qw( + bug_error + chart_datafile_corrupt + chart_dir_nonexistent + chart_file_open_fail + illegal_content_type_method + jobqueue_insert_failed + ldap_bind_failed + mail_send_error + template_error + token_generation_error + )], + + # any error messages matching these regex's will not be sent to arecibo + ignore => [ + qr/Software caused connection abort/, + qr/Could not check out .*\/cvsroot/, + ], +}; + +sub arecibo_generate_id { + return sprintf("%s.%s", (time), $$); +} + +sub arecibo_should_notify { + my $code_error = shift; + return grep { $_ eq $code_error } @{CONFIG->{codes}}; +} + +sub arecibo_handle_error { + my $class = shift; + my @message = split(/\n/, shift); + my $id = shift || arecibo_generate_id(); + + my $is_error = $class eq 'error'; + if ($class ne 'error' && $class ne 'warning') { + # it's a code-error + return 0 unless arecibo_should_notify($class); + $is_error = 1; + } + + # build traceback + my $traceback; + { + # for now don't show function arguments, in case they contain + # confidential data. waiting on bug 700683 + #local $Carp::MaxArgLen = 256; + #local $Carp::MaxArgNums = 0; + local $Carp::MaxArgNums = -1; + local $Carp::CarpInternal{'CGI::Carp'} = 1; + local $Carp::CarpInternal{'Bugzilla::Error'} = 1; + local $Carp::CarpInternal{'Bugzilla::Arecibo'} = 1; + $traceback = Carp::longmess(); + } + + # strip timestamp + foreach my $line (@message) { + $line =~ s/^\[[^\]]+\] //; + } + my $message = join(" ", map { trim($_) } grep { $_ ne '' } @message); + + # don't send to arecibo unless configured + my $arecibo_server = Bugzilla->params->{arecibo_server} || ''; + my $send_to_arecibo = $arecibo_server ne ''; + + # web service filtering + if ($send_to_arecibo + && (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT || Bugzilla->error_mode == ERROR_MODE_JSON_RPC)) + { + my ($code) = $message =~ /^(-?\d+): /; + if ($code + && !($code == ERROR_UNKNOWN_FATAL || $code == ERROR_UNKNOWN_TRANSIENT)) + { + $send_to_arecibo = 0; + } + } + + # message content filtering + if ($send_to_arecibo) { + foreach my $re (@{CONFIG->{ignore}}) { + if ($message =~ $re) { + $send_to_arecibo = 0; + last; + } + } + } + + # log to apache's error_log + if ($send_to_arecibo) { + $message .= " [#$id]"; + } else { + $traceback =~ s/\n/ /g; + $message .= " $traceback"; + } + _write_to_error_log($message, $is_error); + + return 0 unless $send_to_arecibo; + + # set the error type and priority from the message content + $message = join("\n", grep { $_ ne '' } @message); + my $type = ''; + my $priority = $class eq 'error' ? 3 : 10; + foreach my $rh_type (@{CONFIG->{types}}) { + foreach my $re (@{$rh_type->{match}}) { + if ($message =~ $re) { + $type = $rh_type->{type}; + $priority += $rh_type->{boost}; + last; + } + } + last if $type ne ''; + } + $type ||= $class; + $priority = 1 if $priority < 1; + $priority = 10 if $priority > 10; + + my $username = ''; + eval { $username = Bugzilla->user->login }; + + my $request = ''; + foreach my $name (sort { lc($a) cmp lc($b) } keys %ENV) { + $request .= "$name=$ENV{$name}\n"; + } + chomp($request); + + my $data = [ + ip => remote_ip(), + msg => $message, + priority => $priority, + server => hostname(), + request => $request, + status => '500', + timestamp => email_gmdate(), + traceback => $traceback, + type => $type, + uid => $id, + url => Bugzilla->cgi->self_url, + user_agent => $ENV{HTTP_USER_AGENT}, + username => $username, + ]; + + # fork then post + $SIG{CHLD} = 'IGNORE'; + my $pid = fork(); + if (defined($pid) && $pid == 0) { + # detach + chdir('/'); + open(STDIN, '</dev/null'); + open(STDOUT, '>/dev/null'); + open(STDERR, '>/dev/null'); + setsid(); + nice(19); + + # post to arecibo (ignore any errors) + my $agent = LWP::UserAgent->new( + agent => 'bugzilla.mozilla.org', + timeout => 10, # seconds + ); + $agent->post($arecibo_server, $data); + + CORE::exit(0); + } + return 1; +} + +sub _write_to_error_log { + my ($message, $is_error) = @_; + if ($ENV{MOD_PERL}) { + if ($is_error) { + Apache2::ServerRec::log_error($message); + } else { + Apache2::ServerRec::warn($message); + } + } else { + print STDERR "$message\n"; + } +} + +# lifted from Bugzilla::Error +sub _in_eval { + my $in_eval = 0; + for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) { + last if $sub =~ /^ModPerl/; + last if $sub =~ /^Bugzilla::Template/; + $in_eval = 1 if $sub =~ /^\(eval\)/; + } + return $in_eval; +} + +sub _arecibo_die_handler { + my $message = shift; + $message =~ s/^undef error - //; + + # avoid recursion, and check for CGI::Carp::die failures + my $in_cgi_carp_die = 0; + for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) { + return if $sub =~ /:_arecibo_die_handler$/; + $in_cgi_carp_die = 1 if $sub =~ /CGI::Carp::die$/; + } + + return if _in_eval(); + + # mod_perl overrides exit to call die with this string + exit if $message =~ /\bModPerl::Util::exit\b/; + + my $nested_error = ''; + my $is_compilation_failure = $message =~ /\bcompilation (aborted|failed)\b/i; + + # if we are called via CGI::Carp::die chances are something is seriously + # wrong, so skip trying to use ThrowTemplateError + if (!$in_cgi_carp_die && !$is_compilation_failure) { + eval { Bugzilla::Error::ThrowTemplateError($message) }; + $nested_error = $@ if $@; + } + + if ($is_compilation_failure || + $in_cgi_carp_die || + ($nested_error && $nested_error !~ /\bModPerl::Util::exit\b/) + ) { + my $uid = arecibo_generate_id(); + my $notified = arecibo_handle_error('error', $message, $uid); + + # if we aren't dying from a web page, let perl deal with it. this + # does the right thing with respect to returning web service errors + if (Bugzilla->error_mode != ERROR_MODE_WEBPAGE) { + CORE::die($message); + } + + # right now it's hard to determine if we've already returned a + # content-type header, it's better to return two than none + print "Content-type: text/html\n\n"; + + my $maintainer = html_quote(Bugzilla->params->{'maintainer'}); + $message =~ s/ at \S+ line \d+\.\s*$//; + $message = html_quote($message); + $uid = html_quote($uid); + $nested_error = html_quote($nested_error); + print qq( + <h1>Bugzilla has suffered an internal error</h1> + <pre>$message</pre> + <hr> + <pre>$nested_error</pre> + ); + if ($notified) { + print qq( + The <a href="mailto:$maintainer">Bugzilla maintainers</a> have + been notified of this error [#$uid]. + ); + }; + } + exit; +} + +sub install_arecibo_handler { + require CGI::Carp; + CGI::Carp::set_die_handler(\&_arecibo_die_handler); + $main::SIG{__WARN__} = sub { + return if _in_eval(); + arecibo_handle_error('warning', shift); + }; +} + +BEGIN { + if ($ENV{SCRIPT_NAME} || $ENV{MOD_PERL}) { + install_arecibo_handler(); + } +} + +1; diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm index b1f47d0cd..b80228e78 100644 --- a/Bugzilla/Attachment.pm +++ b/Bugzilla/Attachment.pm @@ -415,6 +415,53 @@ sub datasize { return $self->{datasize}; } +=over + +=item C<linecount> + +the number of lines of the attachment content + +=back + +=cut + +# linecount allows for getting the number of lines of an attachment +# from the database directly if the data has not yet been loaded for +# performance reasons. + +sub linecount { + my ($self) = @_; + + return $self->{linecount} if exists $self->{linecount}; + + # Limit this to just text/* attachments as this could + # cause strange results for binary attachments. + return if $self->contenttype !~ /^text\//; + + # If the data has already been loaded, we can just determine + # line count from the data directly. + if ($self->{data}) { + $self->{linecount} = $self->{data} =~ tr/\n/\n/; + } + else { + $self->{linecount} = + int(Bugzilla->dbh->selectrow_array(" + SELECT LENGTH(attach_data.thedata)-LENGTH(REPLACE(attach_data.thedata,'\n',''))/LENGTH('\n') + FROM attach_data WHERE id = ?", undef, $self->id)); + + } + + # If we still do not have a linecount either the attachment + # is stored in a local file or has been deleted. If the former, + # we call self->data to force a load from the filesystem and + # then do a split on newlines and count again. + unless ($self->{linecount}) { + $self->{linecount} = $self->data =~ tr/\n/\n/; + } + + return $self->{linecount}; +} + sub _get_local_filename { my $self = shift; my $hash = ($self->id % 100) + 100; @@ -458,7 +505,8 @@ sub flag_types { my $vars = { target_type => 'attachment', product_id => $self->bug->product_id, component_id => $self->bug->component_id, - attach_id => $self->id }; + attach_id => $self->id, + active_or_has_flags => $self->bug_id }; $self->{flag_types} = Bugzilla::Flag->_flag_types($vars); return $self->{flag_types}; diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm index 45034e166..ab741965a 100644 --- a/Bugzilla/Auth.pm +++ b/Bugzilla/Auth.pm @@ -38,6 +38,7 @@ use Bugzilla::User::Setting (); use Bugzilla::Auth::Login::Stack; use Bugzilla::Auth::Verify::Stack; use Bugzilla::Auth::Persist::Cookie; +use Socket; sub new { my ($class, $params) = @_; @@ -215,10 +216,18 @@ sub _handle_login_result { my $default_settings = Bugzilla::User::Setting::get_defaults(); my $template = Bugzilla->template_inner( $default_settings->{lang}->{default_value}); + my $address = $attempts->[0]->{ip_addr}; + # Note: inet_aton will only resolve IPv4 addresses. + # For IPv6 we'll need to use inet_pton which requires Perl 5.12. + my $n = inet_aton($address); + if ($n) { + $address = gethostbyaddr($n, AF_INET) . " ($address)" + } my $vars = { locked_user => $user, attempts => $attempts, unlock_at => $unlock_at, + address => $address, }; my $message; $template->process('email/lockout.txt.tmpl', $vars, \$message) diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm index 741475a74..dcf85d206 100644 --- a/Bugzilla/Bug.pm +++ b/Bugzilla/Bug.pm @@ -1632,6 +1632,14 @@ sub _check_groups { : $params->{product}; my %add_groups; + # BMO: Allow extension to add groups before the + # real checks are done. + Bugzilla::Hook::process('bug_check_groups', { + product => $product, + group_names => $group_names, + add_groups => \%add_groups + }); + # In email or WebServices, when the "groups" item actually # isn't specified, then just add the default groups. if (!defined $group_names) { @@ -1650,7 +1658,12 @@ sub _check_groups { foreach my $name (@$group_names) { my $group = Bugzilla::Group->check_no_disclose({ %args, name => $name }); - if (!$product->group_is_settable($group)) { + # BMO: Do not check group_is_settable if the group is + # already added, such as from the extension hook. group_is_settable + # will reject any group the user is not currently in. + if (!$add_groups{$group->id} + && !$product->group_is_settable($group)) + { ThrowUserError('group_restriction_not_allowed', { %args, name => $name }); } $add_groups{$group->id} = $group; @@ -1659,7 +1672,7 @@ sub _check_groups { # Now enforce mandatory groups. $add_groups{$_->id} = $_ foreach @{ $product->groups_mandatory }; - + my @add_groups = values %add_groups; return \@add_groups; } @@ -3262,6 +3275,26 @@ sub depends_on_obj { return $self->{depends_on_obj}; } +sub duplicates { + my $self = shift; + return $self->{duplicates} if exists $self->{duplicates}; + return [] if $self->{error}; + $self->{duplicates} = Bugzilla::Bug->new_from_list($self->duplicate_ids); + return $self->{duplicates}; +} + +sub duplicate_ids { + my $self = shift; + return $self->{duplicate_ids} if exists $self->{duplicate_ids}; + return [] if $self->{error}; + + my $dbh = Bugzilla->dbh; + $self->{duplicate_ids} = + $dbh->selectcol_arrayref('SELECT dupe FROM duplicates WHERE dupe_of = ?', + undef, $self->id); + return $self->{duplicate_ids}; +} + sub flag_types { my ($self) = @_; return $self->{'flag_types'} if exists $self->{'flag_types'}; @@ -3270,7 +3303,8 @@ sub flag_types { my $vars = { target_type => 'bug', product_id => $self->{product_id}, component_id => $self->{component_id}, - bug_id => $self->bug_id }; + bug_id => $self->bug_id, + active_or_has_flags => $self->bug_id }; $self->{'flag_types'} = Bugzilla::Flag->_flag_types($vars); return $self->{'flag_types'}; @@ -3785,14 +3819,26 @@ sub GetBugActivity { $changes = []; } + # If this is the same field as the previoius item, then concatenate + # the data into the same change. + if ($operation->{'who'} && $who eq $operation->{'who'} + && $when eq $operation->{'when'} + && $fieldname eq $operation->{'fieldname'} + && ($attachid || 0) == ($operation->{'attachid'} || 0)) + { + my $old_change = pop @$changes; + $removed = _join_activity_entries($fieldname, $old_change->{'removed'}, $removed); + $added = _join_activity_entries($fieldname, $old_change->{'added'}, $added); + } + $operation->{'who'} = $who; $operation->{'when'} = $when; + $operation->{'fieldname'} = $change{'fieldname'} = $fieldname; + $operation->{'attachid'} = $change{'attachid'} = $attachid; - $change{'fieldname'} = $fieldname; - $change{'attachid'} = $attachid; $change{'removed'} = $removed; $change{'added'} = $added; - + if ($comment_id) { $change{'comment'} = Bugzilla::Comment->new($comment_id); } @@ -3809,6 +3855,37 @@ sub GetBugActivity { return(\@operations, $incomplete_data); } +sub _join_activity_entries { + my ($field, $current_change, $new_change) = @_; + # We need to insert characters as these were removed by old + # LogActivityEntry code. + + if ($current_change eq '') { + return $new_change; + } + + # Buglists and see_also need the comma restored + if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') { + if (substr($new_change, 0, 1) eq ',') { + return $current_change . $new_change; + } else { + return $current_change . ', ' . $new_change; + } + } + + # Assume bug_file_loc contain a single url, don't insert a delimiter + if ($field eq 'bug_file_loc') { + return $current_change . $new_change; + } + + # All other fields get a space + if (substr($new_change, 0, 1) eq ' ') { + return $current_change . $new_change; + } else { + return $current_change . ' ' . $new_change; + } +} + # Update the bugs_activity table to reflect changes made in bugs. sub LogActivityEntry { my ($i, $col, $removed, $added, $whoid, $timestamp, $comment_id) = @_; @@ -3823,7 +3900,6 @@ sub LogActivityEntry { my $commaposition = find_wrap_point($removed, MAX_LINE_LENGTH); $removestr = substr($removed, 0, $commaposition); $removed = substr($removed, $commaposition); - $removed =~ s/^[,\s]+//; # remove any comma or space } else { $removed = ""; # no more entries } @@ -3831,7 +3907,6 @@ sub LogActivityEntry { my $commaposition = find_wrap_point($added, MAX_LINE_LENGTH); $addstr = substr($added, 0, $commaposition); $added = substr($added, $commaposition); - $added =~ s/^[,\s]+//; # remove any comma or space } else { $added = ""; # no more entries } diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm index 55eeeab25..31a0a1af2 100644 --- a/Bugzilla/BugMail.pm +++ b/Bugzilla/BugMail.pm @@ -47,7 +47,7 @@ use Bugzilla::Hook; use Date::Parse; use Date::Format; use Scalar::Util qw(blessed); -use List::MoreUtils qw(uniq); +use List::MoreUtils qw(uniq firstidx); use constant BIT_DIRECT => 1; use constant BIT_WATCHING => 2; @@ -107,6 +107,7 @@ sub Send { my %user_cache = map { $_->id => $_ } (@assignees, @qa_contacts, @ccs); my @diffs; + my @referenced_bugs; if (!$start) { @diffs = _get_new_bugmail_fields($bug); } @@ -122,15 +123,31 @@ sub Send { new => $params->{changes}->{resolution}->[1], login_name => $changer->login, blocker => $params->{blocker} }); + push(@referenced_bugs, $params->{blocker}->id); } else { - push(@diffs, _get_diffs($bug, $end, \%user_cache)); + my ($diffs, $referenced) = _get_diffs($bug, $end, \%user_cache); + push(@diffs, @$diffs); + push(@referenced_bugs, @$referenced); } my $comments = $bug->comments({ after => $start, to => $end }); # Skip empty comments. @$comments = grep { $_->type || $_->body =~ /\S/ } @$comments; + # Add duplicate bug to referenced bug list + foreach my $comment (@$comments) { + if ($comment->type == CMT_DUPE_OF || $comment->type == CMT_HAS_DUPE) { + push(@referenced_bugs, $comment->extra_data); + } + } + + # Add dependencies to referenced bug list on new bugs + if (!$start) { + push @referenced_bugs, @{ $bug->dependson }; + push @referenced_bugs, @{ $bug->blocked }; + } + ########################################################################### # Start of email filtering code ########################################################################### @@ -193,21 +210,23 @@ sub Send { { bug => $bug, recipients => \%recipients, users => \%user_cache, diffs => \@diffs }); - # Find all those user-watching anyone on the current list, who is not - # on it already themselves. - my $involved = join(",", keys %recipients); + if (scalar keys %recipients) { + # Find all those user-watching anyone on the current list, who is not + # on it already themselves. + my $involved = join(",", keys %recipients); - my $userwatchers = - $dbh->selectall_arrayref("SELECT watcher, watched FROM watch - WHERE watched IN ($involved)"); + my $userwatchers = + $dbh->selectall_arrayref("SELECT watcher, watched FROM watch + WHERE watched IN ($involved)"); - # Mark these people as having the role of the person they are watching - foreach my $watch (@$userwatchers) { - while (my ($role, $bits) = each %{$recipients{$watch->[1]}}) { - $recipients{$watch->[0]}->{$role} |= BIT_WATCHING - if $bits & BIT_DIRECT; + # Mark these people as having the role of the person they are watching + foreach my $watch (@$userwatchers) { + while (my ($role, $bits) = each %{$recipients{$watch->[1]}}) { + $recipients{$watch->[0]}->{$role} |= BIT_WATCHING + if $bits & BIT_DIRECT; + } + push(@{$watching{$watch->[0]}}, $watch->[1]); } - push(@{$watching{$watch->[0]}}, $watch->[1]); } # Global watcher @@ -229,6 +248,9 @@ sub Send { my $date = $params->{dep_only} ? $end : $bug->delta_ts; $date = format_time($date, '%a, %d %b %Y %T %z', 'UTC'); + # Remove duplicate references, and convert to bug objects + @referenced_bugs = @{ Bugzilla::Bug->new_from_list([uniq @referenced_bugs]) }; + foreach my $user_id (keys %recipients) { my %rels_which_want; my $sent_mail = 0; @@ -267,8 +289,33 @@ sub Send { } # Make sure the user isn't in the nomail list, and the dep check passed. - if ($user->email_enabled && $dep_ok) { + # BMO: never send emails to bugs or .tld addresses. this check needs to + # happen after the bugmail_recipients hook. + if ($user->email_enabled && $dep_ok && + ($user->login !~ /bugs$/) && + ($user->login !~ /\.tld$/)) + { # OK, OK, if we must. Email the user. + + # Don't show summaries for bugs the user can't access, and + # provide a hook for extensions such as SecureMail to filter + # this list. + # + # We build an array with the short_desc as a separate item to + # allow extensions to modify the summary without touching the + # bug object. + my $referenced_bugs = []; + foreach my $ref (@{ $user->visible_bugs(\@referenced_bugs) }) { + push @$referenced_bugs, { + bug => $ref, + id => $ref->id, + short_desc => $ref->short_desc, + }; + } + Bugzilla::Hook::process('bugmail_referenced_bugs', + { updated_bug => $bug, + referenced_bugs => $referenced_bugs }); + $sent_mail = sendMail( { to => $user, bug => $bug, @@ -279,6 +326,7 @@ sub Send { $watching{$user_id} : undef, diffs => \@diffs, rels_which_want => \%rels_which_want, + referenced_bugs => $referenced_bugs, }); } } @@ -314,6 +362,7 @@ sub sendMail { my $watchingRef = $params->{watchers}; my @diffs = @{ $params->{diffs} }; my $relRef = $params->{rels_which_want}; + my $referenced_bugs = $params->{referenced_bugs}; # Only display changes the user is allowed see. my @display_diffs; @@ -352,6 +401,14 @@ sub sendMail { push(@watchingrel, 'None') unless @watchingrel; push @watchingrel, map { user_id_to_login($_) } @$watchingRef; + my @changedfields = uniq map { $_->{field_name} } @display_diffs; + + # Add attachments.created to changedfields if one or more + # comments contain information about a new attachment + if (grep($_->type == CMT_ATTACHMENT_CREATED, @send_comments)) { + push(@changedfields, 'attachments.created'); + } + my $vars = { date => $date, to_user => $user, @@ -362,9 +419,10 @@ sub sendMail { reasonswatchheader => join(" ", @watchingrel), changer => $changer, diffs => \@display_diffs, - changedfields => [uniq map { $_->{field_name} } @display_diffs], + changedfields => \@changedfields, new_comments => \@send_comments, threadingmarker => build_thread_marker($bug->id, $user->id, !$bug->lastdiffed), + referenced_bugs => $referenced_bugs, }; my $msg = _generate_bugmail($user, $vars); MessageToMTA($msg); @@ -395,7 +453,7 @@ sub _generate_bugmail { || ThrowTemplateError($template->error()); push @parts, Email::MIME->create( attributes => { - content_type => "text/html", + content_type => "text/html", }, body => $msg_html, ); @@ -434,11 +492,12 @@ sub _get_diffs { ON fielddefs.id = bugs_activity.fieldid WHERE bugs_activity.bug_id = ? $when_restriction - ORDER BY bugs_activity.bug_when", {Slice=>{}}, @args); + ORDER BY bugs_activity.bug_when, fielddefs.description", {Slice=>{}}, @args); + my $referenced_bugs = []; foreach my $diff (@$diffs) { - $user_cache->{$diff->{who}} ||= new Bugzilla::User($diff->{who}); - $diff->{who} = $user_cache->{$diff->{who}}; + $user_cache->{$diff->{who}} ||= new Bugzilla::User($diff->{who}); + $diff->{who} = $user_cache->{$diff->{who}}; if ($diff->{attach_id}) { $diff->{isprivate} = $dbh->selectrow_array( 'SELECT isprivate FROM attachments WHERE attach_id = ?', @@ -449,9 +508,13 @@ sub _get_diffs { $diff->{num} = $comment->count; $diff->{isprivate} = $diff->{new}; } + elsif ($diff->{field_name} eq 'dependson' || $diff->{field_name} eq 'blocked') { + push @$referenced_bugs, grep { /^\d+$/ } split(/[\s,]+/, $diff->{old}); + push @$referenced_bugs, grep { /^\d+$/ } split(/[\s,]+/, $diff->{new}); + } } - return @$diffs; + return ($diffs, $referenced_bugs); } sub _get_new_bugmail_fields { @@ -459,6 +522,20 @@ sub _get_new_bugmail_fields { my @fields = @{ Bugzilla->fields({obsolete => 0, in_new_bugmail => 1}) }; my @diffs; + # Show fields in the same order as the DEFAULT_FIELDS list, which mirrors + # 4.0's behavour and provides sane grouping of similar fields. + # Any additional fields are sorted by descrsiption + my @prepend; + foreach my $name (map { $_->{name} } Bugzilla::Field::DEFAULT_FIELDS) { + my $idx = firstidx { $_->name eq $name } @fields; + if ($idx != -1) { + push(@prepend, $fields[$idx]); + splice(@fields, $idx, 1); + } + } + @fields = sort { $a->description cmp $b->description } @fields; + @fields = (@prepend, @fields); + foreach my $field (@fields) { my $name = $field->name; my $value = $bug->$name; diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm index e0e1c40ba..9d8a1c48f 100644 --- a/Bugzilla/CGI.pm +++ b/Bugzilla/CGI.pm @@ -306,6 +306,10 @@ sub header { unshift(@_, '-x_frame_options' => 'SAMEORIGIN'); } + # Add X-XSS-Protection header to prevent simple XSS attacks + # and enforce the blocking (rather than the rewriting) mode. + unshift(@_, '-x_xss_protection' => '1; mode=block'); + return $self->SUPER::header(@_) || ""; } diff --git a/Bugzilla/Component.pm b/Bugzilla/Component.pm index dc3cc1b9e..ad5166a0f 100644 --- a/Bugzilla/Component.pm +++ b/Bugzilla/Component.pm @@ -371,11 +371,13 @@ sub default_qa_contact { } sub flag_types { - my $self = shift; + my ($self, $params) = @_; + $params ||= {}; if (!defined $self->{'flag_types'}) { my $flagtypes = Bugzilla::FlagType::match({ product_id => $self->product_id, - component_id => $self->id }); + component_id => $self->id, + %$params }); $self->{'flag_types'} = {}; $self->{'flag_types'}->{'bug'} = diff --git a/Bugzilla/Config/Advanced.pm b/Bugzilla/Config/Advanced.pm index 941cefc4f..a5ae3048a 100644 --- a/Bugzilla/Config/Advanced.pm +++ b/Bugzilla/Config/Advanced.pm @@ -63,6 +63,18 @@ use constant get_param_list => ( default => 'off', checker => \&check_multi }, + + { + name => 'disable_bug_updates', + type => 'b', + default => 0 + }, + + { + name => 'arecibo_server', + type => 't', + default => '', + }, ); 1; diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index 01d555b7a..d0770cf73 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -105,7 +105,7 @@ use Memoize; POS_EVENTS EVT_OTHER EVT_ADDED_REMOVED EVT_COMMENT EVT_ATTACHMENT EVT_ATTACHMENT_DATA EVT_PROJ_MANAGEMENT EVT_OPENED_CLOSED EVT_KEYWORD EVT_CC EVT_DEPEND_BLOCK - EVT_BUG_CREATED + EVT_BUG_CREATED EVT_COMPONENT NEG_EVENTS EVT_UNCONFIRMED EVT_CHANGED_BY_ME @@ -262,7 +262,8 @@ use constant AUTH_NO_SUCH_USER => 5; use constant AUTH_LOCKOUT => 6; # The minimum length a password must have. -use constant USER_PASSWORD_MIN_LENGTH => 6; +# BMO uses 8 characters. +use constant USER_PASSWORD_MIN_LENGTH => 8; use constant LOGIN_OPTIONAL => 0; use constant LOGIN_NORMAL => 1; @@ -355,11 +356,13 @@ use constant EVT_KEYWORD => 7; use constant EVT_CC => 8; use constant EVT_DEPEND_BLOCK => 9; use constant EVT_BUG_CREATED => 10; +use constant EVT_COMPONENT => 11; use constant POS_EVENTS => EVT_OTHER, EVT_ADDED_REMOVED, EVT_COMMENT, EVT_ATTACHMENT, EVT_ATTACHMENT_DATA, EVT_PROJ_MANAGEMENT, EVT_OPENED_CLOSED, EVT_KEYWORD, - EVT_CC, EVT_DEPEND_BLOCK, EVT_BUG_CREATED; + EVT_CC, EVT_DEPEND_BLOCK, EVT_BUG_CREATED, + EVT_COMPONENT; use constant EVT_UNCONFIRMED => 50; use constant EVT_CHANGED_BY_ME => 51; @@ -431,8 +434,8 @@ use constant MAX_LOGIN_ATTEMPTS => 5; use constant LOGIN_LOCKOUT_INTERVAL => 30; # The maximum number of seconds the Strict-Transport-Security header -# will remain valid. Default is one week. -use constant MAX_STS_AGE => 604800; +# will remain valid. BMO uses one month. +use constant MAX_STS_AGE => 2629744; # Protocols which are considered as safe. use constant SAFE_PROTOCOLS => ('afs', 'cid', 'ftp', 'gopher', 'http', 'https', @@ -445,15 +448,16 @@ use constant LEGAL_CONTENT_TYPES => ('application', 'audio', 'image', 'message', use constant contenttypes => { - "html"=> "text/html" , - "rdf" => "application/rdf+xml" , - "atom"=> "application/atom+xml" , - "xml" => "application/xml" , - "js" => "application/x-javascript" , - "json"=> "application/json" , - "csv" => "text/csv" , - "png" => "image/png" , - "ics" => "text/calendar" , + "html" => "text/html" , + "rdf" => "application/rdf+xml" , + "atom" => "application/atom+xml" , + "xml" => "application/xml" , + "dtd" => "application/xml-dtd" , + "js" => "application/x-javascript" , + "json" => "application/json" , + "csv" => "text/csv" , + "png" => "image/png" , + "ics" => "text/calendar" , }; # Usage modes. Default USAGE_MODE_BROWSER. Use with Bugzilla->usage_mode. diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm index 0c841632f..2f708d065 100644 --- a/Bugzilla/DB.pm +++ b/Bugzilla/DB.pm @@ -159,7 +159,7 @@ sub _handle_error { # Cut down the error string to a reasonable size $_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000) if length($_[0]) > 4000; - $_[0] = Carp::longmess($_[0]); + # BMO: stracktrace disabled: $_[0] = Carp::longmess($_[0]); return 0; # Now let DBI handle raising the error } diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm index 00ff4acc9..5a88540a9 100644 --- a/Bugzilla/DB/Schema.pm +++ b/Bugzilla/DB/Schema.pm @@ -342,6 +342,8 @@ use constant ABSTRACT_SCHEMA => { bugs_activity => { FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, + PRIMARYKEY => 1}, bug_id => {TYPE => 'INT3', NOTNULL => 1, REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', @@ -358,8 +360,8 @@ use constant ABSTRACT_SCHEMA => { REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'}}, added => {TYPE => 'varchar(255)'}, - removed => {TYPE => 'TINYTEXT'}, - comment_id => {TYPE => 'INT3', + removed => {TYPE => 'varchar(255)'}, + comment_id => {TYPE => 'INT4', REFERENCES => { TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'}}, @@ -370,6 +372,7 @@ use constant ABSTRACT_SCHEMA => { bugs_activity_bug_when_idx => ['bug_when'], bugs_activity_fieldid_idx => ['fieldid'], bugs_activity_added_idx => ['added'], + bugs_activity_removed_idx => ['removed'], ], }, @@ -393,7 +396,7 @@ use constant ABSTRACT_SCHEMA => { longdescs => { FIELDS => [ - comment_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, + comment_id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, bug_id => {TYPE => 'INT3', NOTNULL => 1, REFERENCES => {TABLE => 'bugs', @@ -915,6 +918,8 @@ use constant ABSTRACT_SCHEMA => { profiles_activity => { FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, + PRIMARYKEY => 1}, userid => {TYPE => 'INT3', NOTNULL => 1, REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm index 178f6f90c..e49f466d6 100644 --- a/Bugzilla/Error.pm +++ b/Bugzilla/Error.pm @@ -26,8 +26,9 @@ package Bugzilla::Error; use strict; use base qw(Exporter); -@Bugzilla::Error::EXPORT = qw(ThrowCodeError ThrowTemplateError ThrowUserError); +@Bugzilla::Error::EXPORT = qw(ThrowCodeError ThrowTemplateError ThrowUserError ThrowErrorPage); +use Bugzilla::Arecibo; use Bugzilla::Constants; use Bugzilla::WebService::Constants; use Bugzilla::Util; @@ -93,6 +94,7 @@ sub _throw_error { my $template = Bugzilla->template; my $message; + # 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. @@ -108,8 +110,22 @@ sub _throw_error { message => \$message }); if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) { + if (arecibo_should_notify($vars->{error})) { + $vars->{maintainers_notified} = 1; + $vars->{uid} = arecibo_generate_id(); + $vars->{processed} = {}; + } else { + $vars->{maintainers_notified} = 0; + } + print Bugzilla->cgi->header(); - print $message; + $template->process($name, $vars) + || ThrowTemplateError($template->error()); + + if ($vars->{maintainers_notified}) { + arecibo_handle_error( + $vars->{error}, $vars->{processed}->{error_message}, $vars->{uid}); + } } elsif (Bugzilla->error_mode == ERROR_MODE_TEST) { die Dumper($vars); @@ -183,40 +199,85 @@ sub ThrowTemplateError { die("error: template error: $template_err"); } + # mod_perl overrides exit to call die with this string + # we never want to display this to the user + exit if $template_err =~ /\bModPerl::Util::exit\b/; + $vars->{'template_error_msg'} = $template_err; $vars->{'error'} = "template_error"; + $vars->{'uid'} = arecibo_generate_id(); + arecibo_handle_error('error', $template_err, $vars->{'uid'}); + $vars->{'template_error_msg'} =~ s/ at \S+ line \d+\.\s*$//; + my $template = Bugzilla->template; # Try a template first; but if this one fails too, fall back # on plain old print statements. if (!$template->process("global/code-error.html.tmpl", $vars)) { - my $maintainer = Bugzilla->params->{'maintainer'}; + my $maintainer = html_quote(Bugzilla->params->{'maintainer'}); my $error = html_quote($vars->{'template_error_msg'}); my $error2 = html_quote($template->error()); + my $uid = html_quote($vars->{'uid'}); print <<END; <tt> <p> - Bugzilla has suffered an internal error. Please save this page and - send it to $maintainer with details of what you were doing at the - time this message appeared. + Bugzilla has suffered an internal error: + </p> + <p> + $error + </p> + <!-- template error, no real need to show this to the user + $error2 + --> + <p> + The <a href="mailto:$maintainer">Bugzilla maintainers</a> have + been notified of this error [#$uid]. </p> - <script type="text/javascript"> <!-- - document.write("<p>URL: " + - document.location.href.replace(/&/g,"&") - .replace(/</g,"<") - .replace(/>/g,">") + "</p>"); - // --> - </script> - <p>Template->process() failed twice.<br> - First error: $error<br> - Second error: $error2</p> </tt> END } exit; } +sub ThrowErrorPage { + # BMO customisation for bug 659231 + my ($template_name, $message) = @_; + + my $dbh = Bugzilla->dbh; + $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction(); + + if (Bugzilla->error_mode == ERROR_MODE_DIE) { + die("error: $message"); + } + + if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT + || Bugzilla->error_mode == ERROR_MODE_JSON_RPC) + { + my $code = ERROR_UNKNOWN_TRANSIENT; + if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) { + die SOAP::Fault->faultcode($code)->faultstring($message); + } else { + my $server = Bugzilla->_json_server; + $server->raise_error(code => 100000 + $code, + message => $message, + id => $server->{_bz_request_id}, + version => $server->version); + die if _in_eval(); + $server->response($server->error_response_header); + } + } else { + my $cgi = Bugzilla->cgi; + my $template = Bugzilla->template; + my $vars = {}; + $vars->{message} = $message; + print $cgi->header(); + $template->process($template_name, $vars) + || ThrowTemplateError($template->error()); + exit; + } +} + 1; __END__ diff --git a/Bugzilla/Field.pm b/Bugzilla/Field.pm index dbee5df3d..5442c6401 100644 --- a/Bugzilla/Field.pm +++ b/Bugzilla/Field.pm @@ -78,6 +78,8 @@ use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Util; use List::MoreUtils qw(any); +use Bugzilla::Config qw(SetParam write_params); +use Bugzilla::Hook; use Scalar::Util qw(blessed); @@ -918,53 +920,64 @@ sub remove_from_db { ThrowUserError('customfield_not_obsolete', {'name' => $self->name }); } - $dbh->bz_start_transaction(); + # BMO: disable bug updates during field creation + # using an eval as try/finally + eval { + SetParam('disable_bug_updates', 1); + write_params(); - # Check to see if bug activity table has records (should be fast with index) - my $has_activity = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs_activity - WHERE fieldid = ?", undef, $self->id); - if ($has_activity) { - ThrowUserError('customfield_has_activity', {'name' => $name }); - } + $dbh->bz_start_transaction(); + + # Check to see if bug activity table has records (should be fast with index) + my $has_activity = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs_activity + WHERE fieldid = ?", undef, $self->id); + if ($has_activity) { + ThrowUserError('customfield_has_activity', {'name' => $name }); + } - # Check to see if bugs table has records (slow) - my $bugs_query = ""; + # Check to see if bugs table has records (slow) + my $bugs_query = ""; - if ($self->type == FIELD_TYPE_MULTI_SELECT) { - $bugs_query = "SELECT COUNT(*) FROM bug_$name"; - } - else { - $bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL"; - if ($self->type != FIELD_TYPE_BUG_ID && $self->type != FIELD_TYPE_DATETIME) { - $bugs_query .= " AND $name != ''"; + if ($self->type == FIELD_TYPE_MULTI_SELECT) { + $bugs_query = "SELECT COUNT(*) FROM bug_$name"; } - # Ignore the default single select value - if ($self->type == FIELD_TYPE_SINGLE_SELECT) { - $bugs_query .= " AND $name != '---'"; + else { + $bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL"; + if ($self->type != FIELD_TYPE_BUG_ID && $self->type != FIELD_TYPE_DATETIME) { + $bugs_query .= " AND $name != ''"; + } + # Ignore the default single select value + if ($self->type == FIELD_TYPE_SINGLE_SELECT) { + $bugs_query .= " AND $name != '---'"; + } } - } - my $has_bugs = $dbh->selectrow_array($bugs_query); - if ($has_bugs) { - ThrowUserError('customfield_has_contents', {'name' => $name }); - } + my $has_bugs = $dbh->selectrow_array($bugs_query); + if ($has_bugs) { + ThrowUserError('customfield_has_contents', {'name' => $name }); + } - # Once we reach here, we should be OK to delete. - $dbh->do('DELETE FROM fielddefs WHERE id = ?', undef, $self->id); + # Once we reach here, we should be OK to delete. + $dbh->do('DELETE FROM fielddefs WHERE id = ?', undef, $self->id); - my $type = $self->type; + my $type = $self->type; - # the values for multi-select are stored in a seperate table - if ($type != FIELD_TYPE_MULTI_SELECT) { - $dbh->bz_drop_column('bugs', $name); - } + # the values for multi-select are stored in a seperate table + if ($type != FIELD_TYPE_MULTI_SELECT) { + $dbh->bz_drop_column('bugs', $name); + } - if ($self->is_select) { - # Delete the table that holds the legal values for this field. - $dbh->bz_drop_field_tables($self); - } + if ($self->is_select) { + # Delete the table that holds the legal values for this field. + $dbh->bz_drop_field_tables($self); + } - $dbh->bz_commit_transaction() + $dbh->bz_commit_transaction(); + }; + my $error = "$@"; + SetParam('disable_bug_updates', 0); + write_params(); + die $error if $error; } =pod @@ -1016,36 +1029,72 @@ sub create { # the parameter isn't sent to create(). $params->{sortkey} = undef if !exists $params->{sortkey}; $params->{type} ||= 0; - - $dbh->bz_start_transaction(); - $class->check_required_create_fields(@_); - my $field_values = $class->run_create_validators($params); - my $visibility_values = delete $field_values->{visibility_values}; - my $field = $class->insert_create_data($field_values); - - $field->set_visibility_values($visibility_values); - $field->_update_visibility_values(); - - $dbh->bz_commit_transaction(); - if ($field->custom) { - my $name = $field->name; - my $type = $field->type; - if (SQL_DEFINITIONS->{$type}) { - # Create the database column that stores the data for this field. - $dbh->bz_add_column('bugs', $name, SQL_DEFINITIONS->{$type}); + # BMO: disable bug updates during field creation + # using an eval as try/finally + my $field; + eval { + if ($params->{'custom'}) { + SetParam('disable_bug_updates', 1); + write_params(); } - if ($field->is_select) { - # Create the table that holds the legal values for this field. - $dbh->bz_add_field_tables($field); + # Purpose: if the field is active in the fields list before all of the + # data structures are created, anything accessing Bug.pm will crash. So + # stash a copy of the intended obsolete value for later and force it + # to be obsolete on initial creation. + # Upstreaming: https://bugzilla.mozilla.org/show_bug.cgi?id=531243 + my $original_obsolete; + if ($params->{'custom'}) { + $original_obsolete = $params->{'obsolete'}; + $params->{'obsolete'} = 1; } - if ($type == FIELD_TYPE_SINGLE_SELECT) { - # Insert a default value of "---" into the legal values table. - $dbh->do("INSERT INTO $name (value) VALUES ('---')"); + $dbh->bz_start_transaction(); + $class->check_required_create_fields(@_); + my $field_values = $class->run_create_validators($params); + my $visibility_values = delete $field_values->{visibility_values}; + my $field = $class->insert_create_data($field_values); + + $field->set_visibility_values($visibility_values); + $field->_update_visibility_values(); + + $dbh->bz_commit_transaction(); + + if ($field->custom) { + # Restore the obsolete value that got stashed earlier (in memory) + $field->set_obsolete($original_obsolete); + + my $name = $field->name; + my $type = $field->type; + if (SQL_DEFINITIONS->{$type}) { + # Create the database column that stores the data for this field. + $dbh->bz_add_column('bugs', $name, SQL_DEFINITIONS->{$type}); + } + + if ($field->is_select) { + # Create the table that holds the legal values for this field. + $dbh->bz_add_field_tables($field); + } + + if ($type == FIELD_TYPE_SINGLE_SELECT) { + # Insert a default value of "---" into the legal values table. + $dbh->do("INSERT INTO $name (value) VALUES ('---')"); + } + + # Safe to write the original 'obsolete' value to the database now + $field->update; } + }; + + my $error = "$@"; + if ($params->{'custom'}) { + SetParam('disable_bug_updates', 0); + write_params(); } + die $error if $error; + + Bugzilla::Hook::process("field_end_of_create", { field => $field }); return $field; } diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm index a727532a6..0828ddc7c 100644 --- a/Bugzilla/Flag.pm +++ b/Bugzilla/Flag.pm @@ -81,15 +81,21 @@ use constant AUDIT_REMOVES => 0; use constant SKIP_REQUESTEE_ON_ERROR => 1; -use constant DB_COLUMNS => qw( - id - type_id - bug_id - attach_id - requestee_id - setter_id - status -); +sub DB_COLUMNS { + my $dbh = Bugzilla->dbh; + return qw( + id + type_id + bug_id + attach_id + requestee_id + setter_id + status), + $dbh->sql_date_format('creation_date', '%Y.%m.%d %H:%i:%s') . + ' AS creation_date', + $dbh->sql_date_format('modification_date', '%Y.%m.%d %H:%i:%s') . + ' AS modification_date'; +} use constant UPDATE_COLUMNS => qw( requestee_id @@ -134,6 +140,14 @@ Returns the ID of the attachment this flag belongs to, if any. Returns the status '+', '-', '?' of the flag. +=item C<creation_date> + +Returns the timestamp when the flag was created. + +=item C<modification_date> + +Returns the timestamp when the flag was last modified. + =back =cut @@ -146,6 +160,8 @@ sub attach_id { return $_[0]->{'attach_id'}; } sub status { return $_[0]->{'status'}; } sub setter_id { return $_[0]->{'setter_id'}; } sub requestee_id { return $_[0]->{'requestee_id'}; } +sub creation_date { return $_[0]->{'creation_date'}; } +sub modification_date { return $_[0]->{'modification_date'}; } ############################### #### Methods #### @@ -284,7 +300,7 @@ sub count { sub set_flag { my ($class, $obj, $params) = @_; - my ($bug, $attachment); + my ($bug, $attachment, $obj_flag, $requestee_changed); if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) { $attachment = $obj; $bug = $attachment->bug; @@ -322,13 +338,14 @@ sub set_flag { ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types}; push(@{$obj_flagtype->{flags}}, $flag); } - my ($obj_flag) = grep { $_->id == $flag->id } @{$obj_flagtype->{flags}}; + ($obj_flag) = grep { $_->id == $flag->id } @{$obj_flagtype->{flags}}; # If the flag has the correct type but cannot be found above, this means # the flag is going to be removed (e.g. because this is a pending request # and the attachment is being marked as obsolete). return unless $obj_flag; - $class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment); + ($obj_flag, $requestee_changed) = + $class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment); } # Create a new flag. elsif ($params->{type_id}) { @@ -360,12 +377,21 @@ sub set_flag { } } - $class->_validate(undef, $obj_flagtype, $params, $bug, $attachment); + ($obj_flag, $requestee_changed) = + $class->_validate(undef, $obj_flagtype, $params, $bug, $attachment); } else { ThrowCodeError('param_required', { function => $class . '->set_flag', param => 'id/type_id' }); } + + if ($obj_flag + && $requestee_changed + && $obj_flag->requestee_id + && $obj_flag->requestee->setting('requestee_cc') eq 'on') + { + $bug->add_cc($obj_flag->requestee); + } } sub _validate { @@ -385,23 +411,25 @@ sub _validate { $obj_flag->_set_status($params->{status}); $obj_flag->_set_requestee($params->{requestee}, $attachment, $params->{skip_roe}); + # The requestee ID can be undefined. + my $requestee_changed = ($obj_flag->requestee_id || 0) != ($old_requestee_id || 0); + # The setter field MUST NOT be updated if neither the status # nor the requestee fields changed. - if (($obj_flag->status ne $old_status) - # The requestee ID can be undefined. - || (($obj_flag->requestee_id || 0) != ($old_requestee_id || 0))) - { + if (($obj_flag->status ne $old_status) || $requestee_changed) { $obj_flag->_set_setter($params->{setter}); } # If the flag is deleted, remove it from the list. if ($obj_flag->status eq 'X') { @{$flag_type->{flags}} = grep { $_->id != $obj_flag->id } @{$flag_type->{flags}}; + return; } # Add the newly created flag to the list. elsif (!$obj_flag->id) { push(@{$flag_type->{flags}}, $obj_flag); } + return wantarray ? ($obj_flag, $requestee_changed) : $obj_flag; } =pod @@ -418,10 +446,14 @@ Creates a flag record in the database. sub create { my ($class, $flag, $timestamp) = @_; - $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT NOW()'); + $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); my $params = {}; my @columns = grep { $_ ne 'id' } $class->_get_db_columns; + + # Some columns use date formatting so use alias instead + @columns = map { /\s+AS\s+(.*)$/ ? $1 : $_ } @columns; + $params->{$_} = $flag->{$_} foreach @columns; $params->{creation_date} = $params->{modification_date} = $timestamp; @@ -440,6 +472,7 @@ sub update { if (scalar(keys %$changes)) { $dbh->do('UPDATE flags SET modification_date = ? WHERE id = ?', undef, ($timestamp, $self->id)); + $self->{'modification_date'} = format_time($timestamp, '%Y.%m.%d %T'); } return $changes; } diff --git a/Bugzilla/FlagType.pm b/Bugzilla/FlagType.pm index 15d982744..5fc00e137 100644 --- a/Bugzilla/FlagType.pm +++ b/Bugzilla/FlagType.pm @@ -601,7 +601,7 @@ sub match { $tables = join(' ', @$tables); $criteria = join(' AND ', @criteria); - my $flagtype_ids = $dbh->selectcol_arrayref("SELECT id FROM $tables WHERE $criteria"); + my $flagtype_ids = $dbh->selectcol_arrayref("SELECT flagtypes.id FROM $tables WHERE $criteria"); return Bugzilla::FlagType->new_from_list($flagtype_ids); } @@ -679,6 +679,11 @@ sub sqlify_criteria { my $is_active = $criteria->{is_active} ? "1" : "0"; push(@criteria, "flagtypes.is_active = $is_active"); } + if (exists($criteria->{active_or_has_flags}) && $criteria->{active_or_has_flags} =~ /^\d+$/) { + push(@$tables, "LEFT JOIN flags AS f ON flagtypes.id = f.type_id " . + "AND f.bug_id = " . $criteria->{active_or_has_flags}); + push(@criteria, "(flagtypes.is_active = 1 OR f.id IS NOT NULL)"); + } if ($criteria->{product_id}) { my $product_id = $criteria->{product_id}; detaint_natural($product_id) diff --git a/Bugzilla/Group.pm b/Bugzilla/Group.pm index b7532fe09..3f521d0f2 100644 --- a/Bugzilla/Group.pm +++ b/Bugzilla/Group.pm @@ -119,9 +119,10 @@ sub _get_members { } sub flag_types { - my $self = shift; + my ($self, $params) = @_; + $params ||= {}; require Bugzilla::FlagType; - $self->{flag_types} ||= Bugzilla::FlagType::match({ group => $self->id }); + $self->{flag_types} ||= Bugzilla::FlagType::match({ group => $self->id, %$params }); return $self->{flag_types}; } diff --git a/Bugzilla/Install.pm b/Bugzilla/Install.pm index ce8fe6bad..6019c9d18 100644 --- a/Bugzilla/Install.pm +++ b/Bugzilla/Install.pm @@ -93,6 +93,10 @@ sub SETTINGS { # 2011-06-21 glob@mozilla.com -- Bug 589128 email_format => { options => ['html', 'text_only'], default => 'html' }, + # 2011-06-16 glob@mozilla.com -- Bug 663747 + bugmail_new_prefix => { options => ['on', 'off'], default => 'on' }, + # 2011-10-11 glob@mozilla.com -- Bug 301656 + requestee_cc => { options => ['on', 'off'], default => 'on' }, } }; diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm index 6b9dd65cd..6a8dbc64a 100644 --- a/Bugzilla/Install/DB.pm +++ b/Bugzilla/Install/DB.pm @@ -398,7 +398,7 @@ sub update_table_definitions { "WHERE initialqacontact = 0"); _migrate_email_prefs_to_new_table(); - _initialize_dependency_tree_changes_email_pref(); + _initialize_new_email_prefs(); _change_all_mysql_booleans_to_tinyint(); # make classification_id field type be consistent with DB:Schema @@ -454,8 +454,8 @@ sub update_table_definitions { _clean_control_characters_from_short_desc(); # 2005-12-07 altlst@sonic.net -- Bug 225221 - $dbh->bz_add_column('longdescs', 'comment_id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2012-07-24 dkl@mozilla.com - Bug 776982 + _fix_longdescs_primary_key(); _stop_storing_inactive_flags(); _change_short_desc_from_mediumtext_to_varchar(); @@ -669,6 +669,24 @@ sub update_table_definitions { $dbh->bz_add_index('profile_search', 'profile_search_user_id_idx', [qw(user_id)]); } + # 2012-06-06 dkl@mozilla.com - Bug 762288 + $dbh->bz_alter_column('bugs_activity', 'removed', + { TYPE => 'varchar(255)' }); + $dbh->bz_add_index('bugs_activity', 'bugs_activity_removed_idx', ['removed']); + + # 2012-06-13 dkl@mozilla.com - Bug 764457 + $dbh->bz_add_column('bugs_activity', 'id', + {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + + # 2012-06-13 dkl@mozilla.com - Bug 764466 + $dbh->bz_add_column('profiles_activity', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + + # 2012-07-24 dkl@mozilla.com - Bug 776972 + $dbh->bz_alter_column('bugs_activity', 'id', + {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + + ################################################################ # New --TABLE-- changes should go *** A B O V E *** this point # ################################################################ @@ -2396,13 +2414,16 @@ sub _migrate_email_prefs_to_new_table { } } -sub _initialize_dependency_tree_changes_email_pref { +sub _initialize_new_email_prefs { my $dbh = Bugzilla->dbh; # Check for any "new" email settings that wouldn't have been ported over # during the block above. Since these settings would have otherwise # fallen under EVT_OTHER, we'll just clone those settings. That way if # folks have already disabled all of that mail, there won't be any change. - my %events = ("Dependency Tree Changes" => EVT_DEPEND_BLOCK); + my %events = ( + "Dependency Tree Changes" => EVT_DEPEND_BLOCK, + "Product/Component Changes" => EVT_COMPONENT, + ); foreach my $desc (keys %events) { my $event = $events{$desc}; @@ -3220,6 +3241,11 @@ sub _populate_bugs_fulltext { print "Populating bugs_fulltext with $num_bugs entries..."; print " (this can take a long time.)\n"; } + + # As recommended by Monty Widenius for GNOME's upgrade. + # mkanat and justdave concur it'll be helpful for bmo, too. + $dbh->do('SET SESSION myisam_sort_buffer_size = 3221225472'); + my $newline = $dbh->quote("\n"); $dbh->do( qq{$command INTO bugs_fulltext (bug_id, short_desc, comments, @@ -3687,6 +3713,21 @@ sub _fix_notnull_defaults { } } +sub _fix_longdescs_primary_key { + my $dbh = Bugzilla->dbh; + my $column_def = {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}; + if (!$dbh->bz_column_info('longdescs', 'comment_id')) { + $dbh->bz_add_column('longdescs', 'comment_id', $column_def); + } + elsif ($dbh->bz_column_info('longdescs', 'comment_id')->{TYPE} ne 'INTSERIAL') { + $dbh->bz_drop_fk('bugs_activity', 'comment_id'); + $dbh->bz_alter_column('bugs_activity', 'comment_id', {TYPE => 'INT4'}); + $dbh->bz_alter_column('longdescs', 'comment_id', $column_def); + $dbh->bz_add_fk('bugs_activity', 'comment_id', + {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'}); + } +} + 1; __END__ diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm index c5215ecfa..c3f103aaa 100644 --- a/Bugzilla/Install/Filesystem.pm +++ b/Bugzilla/Install/Filesystem.pm @@ -170,6 +170,7 @@ sub FILESYSTEM { 'contrib/README' => { perms => OWNER_WRITE }, 'contrib/*/README' => { perms => OWNER_WRITE }, + 'contrib/sendunsentbugmail.pl' => { perms => WS_EXECUTE }, 'docs/bugzilla.ent' => { perms => OWNER_WRITE }, 'docs/makedocs.pl' => { perms => OWNER_EXECUTE }, 'docs/style.css' => { perms => WS_SERVE }, @@ -184,8 +185,10 @@ sub FILESYSTEM { # Directories that we want to set the perms on, but not # recurse through. These are directories we didn't create # in checkesetup.pl. + # + # Purpose of BMO change: unknown. my %non_recurse_dirs = ( - '.' => DIR_WS_SERVE, + '.' => 0755, docs => DIR_WS_SERVE, ); @@ -243,10 +246,13 @@ sub FILESYSTEM { dirs => DIR_WS_SERVE }, "$extensionsdir/*/web" => { files => WS_SERVE, dirs => DIR_WS_SERVE }, - + + # Purpose: allow webserver to read .bzr so we execute bzr commands + # in backticks and look at the result over the web. Used to show + # bzr history. + '.bzr' => { files => WS_SERVE, + dirs => DIR_WS_SERVE }, # Directories only for the owner, not for the webserver. - '.bzr' => { files => OWNER_WRITE, - dirs => DIR_OWNER_WRITE }, t => { files => OWNER_WRITE, dirs => DIR_OWNER_WRITE }, xt => { files => OWNER_WRITE, diff --git a/Bugzilla/Mailer.pm b/Bugzilla/Mailer.pm index 7e42cb609..74c9010c2 100644 --- a/Bugzilla/Mailer.pm +++ b/Bugzilla/Mailer.pm @@ -87,29 +87,6 @@ sub MessageToMTA { # thus to hopefully avoid auto replies. $email->header_set('Auto-Submitted', 'auto-generated'); - $email->walk_parts(sub { - my ($part) = @_; - return if $part->parts > 1; # Top-level - my $content_type = $part->content_type || ''; - $content_type =~ /charset=['"](.+)['"]/; - # If no charset is defined or is the default us-ascii, - # then we encode the email to UTF-8 if Bugzilla has utf8 enabled. - # XXX - This is a hack to workaround bug 723944. - if (!$1 || $1 eq 'us-ascii') { - my $body = $part->body; - if (Bugzilla->params->{'utf8'}) { - $part->charset_set('UTF-8'); - # encoding_set works only with bytes, not with utf8 strings. - my $raw = $part->body_raw; - if (utf8::is_utf8($raw)) { - utf8::encode($raw); - $part->body_set($raw); - } - } - $part->encoding_set('quoted-printable') if !is_7bit_clean($body); - } - }); - # MIME-Version must be set otherwise some mailsystems ignore the charset $email->header_set('MIME-Version', '1.0') if !$email->header('MIME-Version'); @@ -134,7 +111,9 @@ sub MessageToMTA { my $from = $email->header('From'); my ($hostname, @args); + my $mailer_class = $method; if ($method eq "Sendmail") { + $mailer_class = 'Bugzilla::Send::Sendmail'; if (ON_WINDOWS) { $Email::Send::Sendmail::SENDMAIL = SENDMAIL_EXE; } @@ -174,6 +153,29 @@ sub MessageToMTA { Bugzilla::Hook::process('mailer_before_send', { email => $email, mailer_args => \@args }); + $email->walk_parts(sub { + my ($part) = @_; + return if $part->parts > 1; # Top-level + my $content_type = $part->content_type || ''; + $content_type =~ /charset=['"](.+)['"]/; + # If no charset is defined or is the default us-ascii, + # then we encode the email to UTF-8 if Bugzilla has utf8 enabled. + # XXX - This is a hack to workaround bug 723944. + if (!$1 || $1 eq 'us-ascii') { + my $body = $part->body; + if (Bugzilla->params->{'utf8'}) { + $part->charset_set('UTF-8'); + # encoding_set works only with bytes, not with utf8 strings. + my $raw = $part->body_raw; + if (utf8::is_utf8($raw)) { + utf8::encode($raw); + $part->body_set($raw); + } + } + $part->encoding_set('quoted-printable') if !is_7bit_clean($body); + } + }); + if ($method eq "Test") { my $filename = bz_locations()->{'datadir'} . '/mailer.testfile'; open TESTFILE, '>>', $filename; @@ -184,7 +186,7 @@ sub MessageToMTA { else { # This is useful for both Sendmail and Qmail, so we put it out here. local $ENV{PATH} = SENDMAIL_PATH; - my $mailer = Email::Send->new({ mailer => $method, + my $mailer = Email::Send->new({ mailer => $mailer_class, mailer_args => \@args }); my $retval = $mailer->send($email); ThrowCodeError('mail_send_error', { msg => $retval, mail => $email }) diff --git a/Bugzilla/Object.pm b/Bugzilla/Object.pm index 422a2ffa5..c20cef450 100644 --- a/Bugzilla/Object.pm +++ b/Bugzilla/Object.pm @@ -228,8 +228,11 @@ sub match { } next; } - - $class->_check_field($field, 'match'); + + # It's always safe to use the field defined by classes as being + # their ID field. In particular, this means that new_from_list() + # is exempted from this check. + $class->_check_field($field, 'match') unless $field eq $class->ID_FIELD; if (ref $value eq 'ARRAY') { # IN () is invalid SQL, and if we have an empty list diff --git a/Bugzilla/Product.pm b/Bugzilla/Product.pm index a0079a033..79af9cbf5 100644 --- a/Bugzilla/Product.pm +++ b/Bugzilla/Product.pm @@ -114,7 +114,7 @@ sub create { # for each product in the list, particularly with hundreds or thousands # of products. sub preload { - my ($products, $preload_flagtypes) = @_; + my ($products, $preload_flagtypes, $flagtypes_params) = @_; my %prods = map { $_->id => $_ } @$products; my @prod_ids = keys %prods; return unless @prod_ids; @@ -132,7 +132,7 @@ sub preload { } } if ($preload_flagtypes) { - $_->flag_types foreach @$products; + $_->flag_types($flagtypes_params) foreach @$products; } } @@ -779,7 +779,8 @@ sub user_has_access { } sub flag_types { - my $self = shift; + my ($self, $params) = @_; + $params ||= {}; return $self->{'flag_types'} if defined $self->{'flag_types'}; @@ -787,7 +788,7 @@ sub flag_types { my $cache = Bugzilla->request_cache->{flag_types_per_product} ||= {}; $self->{flag_types} = {}; my $prod_id = $self->id; - my $flagtypes = Bugzilla::FlagType::match({ product_id => $prod_id }); + my $flagtypes = Bugzilla::FlagType::match({ product_id => $prod_id, %$params }); foreach my $type ('bug', 'attachment') { my @flags = grep { $_->target_type eq $type } @$flagtypes; diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm index da37a9638..8e70a9721 100644 --- a/Bugzilla/Search.pm +++ b/Bugzilla/Search.pm @@ -2839,7 +2839,8 @@ sub _changedby { sub IsValidQueryType { my ($queryType) = @_; - if (grep { $_ eq $queryType } qw(specific advanced)) { + # BMO: Added google and instant + if (grep { $_ eq $queryType } qw(specific advanced google instant)) { return 1; } return 0; diff --git a/Bugzilla/Search/Quicksearch.pm b/Bugzilla/Search/Quicksearch.pm index 7424f831f..54592f07e 100644 --- a/Bugzilla/Search/Quicksearch.pm +++ b/Bugzilla/Search/Quicksearch.pm @@ -530,6 +530,9 @@ sub _default_quicksearch_word { addChart('short_desc', 'substring', $word, $negate); addChart('status_whiteboard', 'substring', $word, $negate); addChart('content', 'matches', _matches_phrase($word), $negate) if $fulltext; + + # BMO Bug 664124 - Include the crash signature (sig:) field in default quicksearches + addChart('cf_crash_signature', 'substring', $word, $negate); } sub _handle_urls { diff --git a/Bugzilla/Send/Sendmail.pm b/Bugzilla/Send/Sendmail.pm new file mode 100644 index 000000000..9513134f4 --- /dev/null +++ b/Bugzilla/Send/Sendmail.pm @@ -0,0 +1,95 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Send::Sendmail; + +use strict; + +use base qw(Email::Send::Sendmail); + +use Return::Value; +use Symbol qw(gensym); + +sub send { + my ($class, $message, @args) = @_; + my $mailer = $class->_find_sendmail; + + return failure "Couldn't find 'sendmail' executable in your PATH" + ." and Email::Send::Sendmail::SENDMAIL is not set" + unless $mailer; + + return failure "Found $mailer but cannot execute it" + unless -x $mailer; + + local $SIG{'CHLD'} = 'DEFAULT'; + + my $pipe = gensym; + + open($pipe, "| $mailer -t -oi @args") + || return failure "Error executing $mailer: $!"; + print($pipe $message->as_string) + || return failure "Error printing via pipe to $mailer: $!"; + unless (close $pipe) { + return failure "error when closing pipe to $mailer: $!" if $!; + my ($error_message, $is_transient) = _map_exitcode($? >> 8); + if (Bugzilla->params->{'use_mailer_queue'}) { + # Return success for errors which are fatal so Bugzilla knows to + # remove them from the queue + if ($is_transient) { + return failure "error when closing pipe to $mailer: $error_message"; + } else { + warn "error when closing pipe to $mailer: $error_message\n"; + return success; + } + } else { + return failure "error when closing pipe to $mailer: $error_message"; + } + } + return success; +} + +sub _map_exitcode { + # Returns (error message, is_transient) + # from the sendmail source (sendmail/sysexit.h) + my $code = shift; + if ($code == 64) { + return ("Command line usage error (EX_USAGE)", 1); + } elsif ($code == 65) { + return ("Data format error (EX_DATAERR)", 1); + } elsif ($code == 66) { + return ("Cannot open input (EX_NOINPUT)", 1); + } elsif ($code == 67) { + return ("Addressee unknown (EX_NOUSER)", 0); + } elsif ($code == 68) { + return ("Host name unknown (EX_NOHOST)", 0); + } elsif ($code == 69) { + return ("Service unavailable (EX_UNAVAILABLE)", 1); + } elsif ($code == 70) { + return ("Internal software error (EX_SOFTWARE)", 1); + } elsif ($code == 71) { + return ("System error (EX_OSERR)", 1); + } elsif ($code == 72) { + return ("Critical OS file missing (EX_OSFILE)", 1); + } elsif ($code == 73) { + return ("Can't create output file (EX_CANTCREAT)", 1); + } elsif ($code == 74) { + return ("Input/output error (EX_IOERR)", 1); + } elsif ($code == 75) { + return ("Temp failure (EX_TEMPFAIL)", 1); + } elsif ($code == 76) { + return ("Remote error in protocol (EX_PROTOCOL)", 1); + } elsif ($code == 77) { + return ("Permission denied (EX_NOPERM)", 1); + } elsif ($code == 78) { + return ("Configuration error (EX_CONFIG)", 1); + } else { + return ("Unknown Error ($code)", 1); + } +} + +1; + diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm index c907f9267..870053b46 100644 --- a/Bugzilla/Template.pm +++ b/Bugzilla/Template.pm @@ -234,7 +234,8 @@ sub quoteUrls { ~<a href=\"mailto:$2\">$1$2</a>~igx; # attachment links - $text =~ s~\b(attachment\s*\#?\s*(\d+)(?:\s+\[details\])?) + # BMO: Bug 652332 dkl@mozilla.com 2011-07-20 + $text =~ s~\b(attachment\s*\#?\s*(\d+)(?:\s+\[diff\])?(?:\s+\[details\])?) ~($things[$count++] = get_attachment_link($2, $1)) && ("\0\0" . ($count-1) . "\0\0") ~egmxi; @@ -293,19 +294,21 @@ sub get_attachment_link { $title = html_quote(clean_text($title)); $link_text =~ s/ \[details\]$//; + $link_text =~ s/ \[diff\]$//; my $linkval = "attachment.cgi?id=$attachid"; - # If the attachment is a patch, try to link to the diff rather - # than the text, by default. + # If the attachment is a patch and patch_viewer feature is + # enabled, add link to the diff. my $patchlink = ""; if ($attachment->ispatch and Bugzilla->feature('patch_viewer')) { - $patchlink = '&action=diff'; + $patchlink = qq| <a href="${linkval}&action=diff" title="$title">[diff]</a>|; } # Whitespace matters here because these links are in <pre> tags. return qq|<span class="$className">| - . qq|<a href="${linkval}${patchlink}" name="attach_${attachid}" title="$title">$link_text</a>| + . qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>| . qq| <a href="${linkval}&action=edit" title="$title">[details]</a>| + . qq|${patchlink}| . qq|</span>|; } else { @@ -660,6 +663,18 @@ sub create { $var =~ s/>/\\x3e/g; return $var; }, + + # Sadly, different to the above. See http://www.json.org/ + # for details. + json => sub { + my ($var) = @_; + $var =~ s/([\\\"\/])/\\$1/g; + $var =~ s/\n/\\n/g; + $var =~ s/\r/\\r/g; + $var =~ s/\f/\\f/g; + $var =~ s/\t/\\t/g; + return $var; + }, # Converts data to base64 base64 => sub { @@ -923,7 +938,15 @@ sub create { Bugzilla->fields({ by_name => 1 }); return $cache->{template_bug_fields}; }, - + + # A general purpose cache to store rendered templates for reuse. + # Make sure to not mix language-specific data. + 'template_cache' => sub { + my $cache = Bugzilla->request_cache->{template_cache} ||= {}; + $cache->{users} ||= {}; + return $cache; + }, + 'css_files' => \&css_files, yui_resolve_deps => \&yui_resolve_deps, diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm index 2bb68e721..4804851bb 100644 --- a/Bugzilla/Token.pm +++ b/Bugzilla/Token.pm @@ -109,6 +109,8 @@ sub IssueEmailChangeToken { $vars->{'newemailaddress'} = $new_email . $email_suffix; $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); $vars->{'token'} = $token; + # For SecureMail extension + $vars->{'to_user'} = $user; $vars->{'emailaddress'} = $old_email . $email_suffix; my $message; diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm index 391e416af..60dbb5f83 100644 --- a/Bugzilla/User.pm +++ b/Bugzilla/User.pm @@ -50,6 +50,7 @@ use Bugzilla::Product; use Bugzilla::Classification; use Bugzilla::Field; use Bugzilla::Group; +use Bugzilla::Hook; use DateTime::TimeZone; use List::Util qw(max); @@ -707,8 +708,8 @@ sub bless_groups { return $self->{'bless_groups'} if defined $self->{'bless_groups'}; return [] unless $self->id; - if ($self->in_group('editusers')) { - # Users having editusers permissions may bless all groups. + if ($self->in_group('admin')) { + # Users having admin permissions may bless all groups. $self->{'bless_groups'} = [Bugzilla::Group->get_all]; return $self->{'bless_groups'}; } @@ -778,6 +779,15 @@ sub in_group_id { return grep($_->id == $id, @{ $self->groups }) ? 1 : 0; } +# This is a helper to get all groups which have an icon to be displayed +# besides the name of the commenter. +sub groups_with_icon { + my $self = shift; + + my @groups = grep { $_->icon_url } @{ $self->direct_group_membership }; + return \@groups; +} + sub get_products_by_permission { my ($self, $group) = @_; # Make sure $group exists on a per-product basis. @@ -1633,7 +1643,9 @@ our %names_to_events = ( 'attachments.mimetype' => EVT_ATTACHMENT_DATA, 'attachments.ispatch' => EVT_ATTACHMENT_DATA, 'dependson' => EVT_DEPEND_BLOCK, - 'blocked' => EVT_DEPEND_BLOCK); + 'blocked' => EVT_DEPEND_BLOCK, + 'product' => EVT_COMPONENT, + 'component' => EVT_COMPONENT); # Returns true if the user wants mail for a given bug change. # Note: the "+" signs before the constants suppress bareword quoting. @@ -1652,7 +1664,7 @@ sub wants_bug_mail { } else { # Catch-all for any change not caught by a more specific event - $events{+EVT_OTHER} = 1; + $events{+EVT_OTHER} = 1; } # If the user is in a particular role and the value of that role @@ -2332,7 +2344,7 @@ Determines whether or not a user is in the given group by id. Returns an arrayref of L<Bugzilla::Group> objects. The arrayref consists of the groups the user can bless, taking into account -that having editusers permissions means that you can bless all groups, and +that having admin permissions means that you can bless all groups, and that you need to be able to see a group in order to bless it. =item C<get_products_by_permission($group)> diff --git a/Bugzilla/UserAgent.pm b/Bugzilla/UserAgent.pm new file mode 100644 index 000000000..6589930ce --- /dev/null +++ b/Bugzilla/UserAgent.pm @@ -0,0 +1,249 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 the Mozilla Foundation +# Portions created by the Initial Developer are Copyright (C) 2011 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Terry Weissman <terry@mozilla.org> +# Dave Miller <justdave@syndicomm.com> +# Joe Robins <jmrobins@tgix.com> +# Gervase Markham <gerv@gerv.net> +# Shane H. W. Travis <travis@sedsystems.ca> +# Nitish Bezzala <nbezzala@yahoo.com> +# Byron Jones <glob@mozilla.com> + +package Bugzilla::UserAgent; + +use strict; +use base qw(Exporter); +our @EXPORT = qw(detect_platform detect_op_sys); + +use Bugzilla::Field; +use List::MoreUtils qw(natatime); + +use constant DEFAULT_VALUE => 'Other'; + +use constant PLATFORMS_MAP => ( + # PowerPC + qr/\(.*PowerPC.*\)/i => ["PowerPC", "Macintosh"], + # AMD64, Intel x86_64 + qr/\(.*[ix0-9]86 (?:on |\()x86_64.*\)/ => ["IA32", "x86", "PC"], + qr/\(.*amd64.*\)/ => ["AMD64", "x86_64", "PC"], + qr/\(.*x86_64.*\)/ => ["AMD64", "x86_64", "PC"], + # Intel IA64 + qr/\(.*IA64.*\)/ => ["IA64", "PC"], + # Intel x86 + qr/\(.*Intel.*\)/ => ["IA32", "x86", "PC"], + qr/\(.*[ix0-9]86.*\)/ => ["IA32", "x86", "PC"], + # Versions of Windows that only run on Intel x86 + qr/\(.*Win(?:dows |)[39M].*\)/ => ["IA32", "x86", "PC"], + qr/\(.*Win(?:dows |)16.*\)/ => ["IA32", "x86", "PC"], + # Sparc + qr/\(.*sparc.*\)/ => ["Sparc", "Sun"], + qr/\(.*sun4.*\)/ => ["Sparc", "Sun"], + # Alpha + qr/\(.*AXP.*\)/i => ["Alpha", "DEC"], + qr/\(.*[ _]Alpha.\D/i => ["Alpha", "DEC"], + qr/\(.*[ _]Alpha\)/i => ["Alpha", "DEC"], + # MIPS + qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"], + qr/\(.*MIPS.*\)/i => ["MIPS", "SGI"], + # 68k + qr/\(.*68K.*\)/ => ["68k", "Macintosh"], + qr/\(.*680[x0]0.*\)/ => ["68k", "Macintosh"], + # HP + qr/\(.*9000.*\)/ => ["PA-RISC", "HP"], + # ARM + qr/\(.*ARM.*\)/ => ["ARM", "PocketPC"], + # PocketPC intentionally before PowerPC + qr/\(.*Windows CE.*PPC.*\)/ => ["ARM", "PocketPC"], + # PowerPC + qr/\(.*PPC.*\)/ => ["PowerPC", "Macintosh"], + qr/\(.*AIX.*\)/ => ["PowerPC", "Macintosh"], + # Stereotypical and broken + qr/\(.*Windows CE.*\)/ => ["ARM", "PocketPC"], + qr/\(.*Macintosh.*\)/ => ["68k", "Macintosh"], + qr/\(.*Mac OS [89].*\)/ => ["68k", "Macintosh"], + qr/\(.*WOW64.*\)/ => ["x86_64"], + qr/\(.*Win64.*\)/ => ["IA64"], + qr/\(Win.*\)/ => ["IA32", "x86", "PC"], + qr/\(.*Win(?:dows[ -])NT.*\)/ => ["IA32", "x86", "PC"], + qr/\(.*OSF.*\)/ => ["Alpha", "DEC"], + qr/\(.*HP-?UX.*\)/i => ["PA-RISC", "HP"], + qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"], + qr/\(.*(SunOS|Solaris).*\)/ => ["Sparc", "Sun"], + # Braindead old browsers who didn't follow convention: + qr/Amiga/ => ["68k", "Macintosh"], + qr/WinMosaic/ => ["IA32", "x86", "PC"], +); + +use constant OS_MAP => ( + # Sun + qr/\(.*Solaris.*\)/ => ["Solaris"], + qr/\(.*SunOS 5.11.*\)/ => [("OpenSolaris", "Opensolaris", "Solaris 11")], + qr/\(.*SunOS 5.10.*\)/ => ["Solaris 10"], + qr/\(.*SunOS 5.9.*\)/ => ["Solaris 9"], + qr/\(.*SunOS 5.8.*\)/ => ["Solaris 8"], + qr/\(.*SunOS 5.7.*\)/ => ["Solaris 7"], + qr/\(.*SunOS 5.6.*\)/ => ["Solaris 6"], + qr/\(.*SunOS 5.5.*\)/ => ["Solaris 5"], + qr/\(.*SunOS 5.*\)/ => ["Solaris"], + qr/\(.*SunOS.*sun4u.*\)/ => ["Solaris"], + qr/\(.*SunOS.*i86pc.*\)/ => ["Solaris"], + qr/\(.*SunOS.*\)/ => ["SunOS"], + # BSD + qr/\(.*BSD\/(?:OS|386).*\)/ => ["BSDI"], + qr/\(.*FreeBSD.*\)/ => ["FreeBSD"], + qr/\(.*OpenBSD.*\)/ => ["OpenBSD"], + qr/\(.*NetBSD.*\)/ => ["NetBSD"], + # Misc POSIX + qr/\(.*IRIX.*\)/ => ["IRIX"], + qr/\(.*OSF.*\)/ => ["OSF/1"], + qr/\(.*Linux.*\)/ => ["Linux"], + qr/\(.*BeOS.*\)/ => ["BeOS"], + qr/\(.*AIX.*\)/ => ["AIX"], + qr/\(.*OS\/2.*\)/ => ["OS/2"], + qr/\(.*QNX.*\)/ => ["Neutrino"], + qr/\(.*VMS.*\)/ => ["OpenVMS"], + qr/\(.*HP-?UX.*\)/ => ["HP-UX"], + qr/\(.*Android.*\)/ => ["Android"], + # Windows + qr/\(.*Windows XP.*\)/ => ["Windows XP"], + qr/\(.*Windows NT 6\.2.*\)/ => ["Windows 8"], + qr/\(.*Windows NT 6\.1.*\)/ => ["Windows 7"], + qr/\(.*Windows NT 6\.0.*\)/ => ["Windows Vista"], + qr/\(.*Windows NT 5\.2.*\)/ => ["Windows Server 2003"], + qr/\(.*Windows NT 5\.1.*\)/ => ["Windows XP"], + qr/\(.*Windows 2000.*\)/ => ["Windows 2000"], + qr/\(.*Windows NT 5.*\)/ => ["Windows 2000"], + qr/\(.*Win.*9[8x].*4\.9.*\)/ => ["Windows ME"], + qr/\(.*Win(?:dows |)M[Ee].*\)/ => ["Windows ME"], + qr/\(.*Win(?:dows |)98.*\)/ => ["Windows 98"], + qr/\(.*Win(?:dows |)95.*\)/ => ["Windows 95"], + qr/\(.*Win(?:dows |)16.*\)/ => ["Windows 3.1"], + qr/\(.*Win(?:dows[ -]|)NT.*\)/ => ["Windows NT"], + qr/\(.*Windows.*NT.*\)/ => ["Windows NT"], + # OS X + qr/\(.*Mac OS X (?:|Mach-O |\()10.6.*\)/ => ["Mac OS X 10.6"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.5.*\)/ => ["Mac OS X 10.5"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.4.*\)/ => ["Mac OS X 10.4"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.3.*\)/ => ["Mac OS X 10.3"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.2.*\)/ => ["Mac OS X 10.2"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.1.*\)/ => ["Mac OS X 10.1"], + # Unfortunately, OS X 10.4 was the first to support Intel. This is fallback + # support because some browsers refused to include the OS Version. + qr/\(.*Intel.*Mac OS X.*\)/ => ["Mac OS X 10.4"], + # OS X 10.3 is the most likely default version of PowerPC Macs + # OS X 10.0 is more for configurations which didn't setup 10.x versions + qr/\(.*Mac OS X.*\)/ => [("Mac OS X 10.3", "Mac OS X 10.0", "Mac OS X")], + qr/\(.*Mac OS 9.*\)/ => [("Mac System 9.x", "Mac System 9.0")], + qr/\(.*Mac OS 8\.6.*\)/ => [("Mac System 8.6", "Mac System 8.5")], + qr/\(.*Mac OS 8\.5.*\)/ => ["Mac System 8.5"], + qr/\(.*Mac OS 8\.1.*\)/ => [("Mac System 8.1", "Mac System 8.0")], + qr/\(.*Mac OS 8\.0.*\)/ => ["Mac System 8.0"], + qr/\(.*Mac OS 8[^.].*\)/ => ["Mac System 8.0"], + qr/\(.*Mac OS 8.*\)/ => ["Mac System 8.6"], + qr/\(.*Darwin.*\)/ => [("Mac OS X 10.0", "Mac OS X")], + # Silly + qr/\(.*Mac.*PowerPC.*\)/ => ["Mac System 9.x"], + qr/\(.*Mac.*PPC.*\)/ => ["Mac System 9.x"], + qr/\(.*Mac.*68k.*\)/ => ["Mac System 8.0"], + # Evil + qr/Amiga/i => ["Other"], + qr/WinMosaic/ => ["Windows 95"], + qr/\(.*32bit.*\)/ => ["Windows 95"], + qr/\(.*16bit.*\)/ => ["Windows 3.1"], + qr/\(.*PowerPC.*\)/ => ["Mac System 9.x"], + qr/\(.*PPC.*\)/ => ["Mac System 9.x"], + qr/\(.*68K.*\)/ => ["Mac System 8.0"], +); + +sub detect_platform { + my $userAgent = $ENV{'HTTP_USER_AGENT'}; + my @detected; + my $iterator = natatime(2, PLATFORMS_MAP); + while (my($re, $ra) = $iterator->()) { + if ($userAgent =~ $re) { + push @detected, @$ra; + } + } + return _pick_valid_field_value('rep_platform', @detected); +} + +sub detect_op_sys { + my $userAgent = $ENV{'HTTP_USER_AGENT'} || ''; + my @detected; + my $iterator = natatime(2, OS_MAP); + while (my($re, $ra) = $iterator->()) { + if ($userAgent =~ $re) { + push @detected, @$ra; + } + } + push(@detected, "Windows") if grep(/^Windows /, @detected); + push(@detected, "Mac OS") if grep(/^Mac /, @detected); + return _pick_valid_field_value('op_sys', @detected); +} + +# Takes the name of a field and a list of possible values for that field. +# Returns the first value in the list that is actually a valid value for that +# field. +# Returns 'Other' if none of the values match. +sub _pick_valid_field_value { + my ($field, @values) = @_; + foreach my $value (@values) { + return $value if check_field($field, $value, undef, 1); + } + return DEFAULT_VALUE; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::UserAgent - UserAgent utilities for Bugzilla + +=head1 SYNOPSIS + + use Bugzilla::UserAgent; + printf "platform: %s op-sys: %s\n", detect_platform(), detect_op_sys(); + +=head1 DESCRIPTION + +The functions exported by this module all return information derived from the +remote client's user agent. + +=head1 FUNCTIONS + +=over 4 + +=item C<detect_platform> + +This function attempts to detect the remote client's platform from the +presented user-agent. If a suitable value on the I<platform> field is found, +that field value will be returned. If no suitable value is detected, +C<detect_platform> returns I<Other>. + +=item C<detect_op_sys> + +This function attempts to detect the remote client's operating system from the +presented user-agent. If a suitable value on the I<op_sys> field is found, that +field value will be returned. If no suitable value is detected, +C<detect_op_sys> returns I<Other>. + +=back + diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm index 781e8b944..6b5d8e3ef 100644 --- a/Bugzilla/WebService/Bug.pm +++ b/Bugzilla/WebService/Bug.pm @@ -82,6 +82,8 @@ BEGIN { sub fields { my ($self, $params) = validate(@_, 'ids', 'names'); + Bugzilla->switch_to_shadow_db(); + my @fields; if (defined $params->{ids}) { my $ids = $params->{ids}; @@ -117,11 +119,12 @@ sub fields { my (@values, $has_values); if ( ($field->is_select and $field->name ne 'product') - or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) + or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS) + or $field->name eq 'keywords') { $has_values = 1; @values = @{ $self->_legal_field_values({ field => $field }) }; - } + } if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) { $value_field = 'product'; @@ -211,6 +214,15 @@ sub _legal_field_values { } } + elsif ($field_name eq 'keywords') { + my @legal_keywords = Bugzilla::Keyword->get_all; + foreach my $value (@legal_keywords) { + push (@result, { + name => $self->type('string', $value->name), + description => $self->type('string', $value->description), + }); + } + } else { my @values = Bugzilla::Field::Choice->type($field)->get_all(); foreach my $value (@values) { @@ -242,7 +254,7 @@ sub comments { my $bug_ids = $params->{ids} || []; my $comment_ids = $params->{comment_ids} || []; - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->switch_to_shadow_db(); my $user = Bugzilla->user; my %bugs; @@ -300,6 +312,7 @@ sub _translate_comment { creator => $self->type('string', $comment->author->login), author => $self->type('string', $comment->author->login), time => $self->type('dateTime', $comment->creation_ts), + creation_time => $self->type('dateTime', $comment->creation_ts), is_private => $self->type('boolean', $comment->is_private), text => $self->type('string', $comment->body_full), attachment_id => $self->type('int', $attach_id), @@ -309,6 +322,8 @@ sub _translate_comment { sub get { my ($self, $params) = validate(@_, 'ids'); + Bugzilla->switch_to_shadow_db(); + my $ids = $params->{ids}; defined $ids || ThrowCodeError('param_required', { param => 'ids' }); @@ -343,11 +358,15 @@ sub get { sub history { my ($self, $params) = validate(@_, 'ids'); + Bugzilla->switch_to_shadow_db(); + my $ids = $params->{ids}; defined $ids || ThrowCodeError('param_required', { param => 'ids' }); - my @return; + my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() }; + $api_name{'bug_group'} = 'groups'; + my @return; foreach my $bug_id (@$ids) { my %item; my $bug = Bugzilla::Bug->check($bug_id); @@ -363,14 +382,15 @@ sub history { $bug_history{who} = $self->type('string', $changeset->{who}); $bug_history{changes} = []; foreach my $change (@{ $changeset->{changes} }) { + my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname}; my $attach_id = delete $change->{attachid}; if ($attach_id) { $change->{attachment_id} = $self->type('int', $attach_id); } $change->{removed} = $self->type('string', $change->{removed}); $change->{added} = $self->type('string', $change->{added}); - $change->{field_name} = $self->type('string', - delete $change->{fieldname}); + $change->{field_name} = $self->type('string', $api_field); + delete $change->{fieldname}; push (@{$bug_history{changes}}, $change); } @@ -399,7 +419,9 @@ sub history { sub search { my ($self, $params) = @_; - + + Bugzilla->switch_to_shadow_db(); + if ( defined($params->{offset}) and !defined($params->{limit}) ) { ThrowCodeError('param_required', { param => 'limit', function => 'Bug.search()' }); @@ -439,16 +461,25 @@ sub search { delete $match_params{'include_fields'}; delete $match_params{'exclude_fields'}; + my $count_only = delete $match_params{count_only}; + my $bugs = Bugzilla::Bug->match(\%match_params); my $visible = Bugzilla->user->visible_bugs($bugs); - my @hashes = map { $self->_bug_to_hash($_, $params) } @$visible; - return { bugs => \@hashes }; + if ($count_only) { + return { bug_count => scalar @$visible }; + } + else { + my @hashes = map { $self->_bug_to_hash($_, $params) } @$visible; + return { bugs => \@hashes }; + } } sub possible_duplicates { my ($self, $params) = validate(@_, 'product'); my $user = Bugzilla->user; + Bugzilla->switch_to_shadow_db(); + # Undo the array-ification that validate() does, for "summary". $params->{summary} || ThrowCodeError('param_required', { function => 'Bug.possible_duplicates', param => 'summary' }); @@ -469,6 +500,12 @@ sub possible_duplicates { sub update { my ($self, $params) = validate(@_, 'ids'); + # BMO: Don't allow updating of bugs if disabled + if (Bugzilla->params->{disable_bug_updates}) { + ThrowErrorPage('bug/process/updates-disabled.html.tmpl', + 'Bug updates are currently disabled.'); + } + my $user = Bugzilla->login(LOGIN_REQUIRED); my $dbh = Bugzilla->dbh; @@ -563,6 +600,13 @@ sub update { sub create { my ($self, $params) = @_; + + # BMO: Don't allow updating of bugs if disabled + if (Bugzilla->params->{disable_bug_updates}) { + ThrowErrorPage('bug/process/updates-disabled.html.tmpl', + 'Bug updates are currently disabled.'); + } + Bugzilla->login(LOGIN_REQUIRED); $params = Bugzilla::Bug::map_fields($params); my $bug = Bugzilla::Bug->create($params); @@ -573,6 +617,8 @@ sub create { sub legal_values { my ($self, $params) = @_; + Bugzilla->switch_to_shadow_db(); + defined $params->{field} or ThrowCodeError('param_required', { param => 'field' }); @@ -625,6 +671,12 @@ sub add_attachment { my ($self, $params) = validate(@_, 'ids'); my $dbh = Bugzilla->dbh; + # BMO: Don't allow updating of bugs if disabled + if (Bugzilla->params->{disable_bug_updates}) { + ThrowErrorPage('bug/process/updates-disabled.html.tmpl', + 'Bug updates are currently disabled.'); + } + Bugzilla->login(LOGIN_REQUIRED); defined $params->{ids} || ThrowCodeError('param_required', { param => 'ids' }); @@ -673,6 +725,12 @@ sub add_attachment { sub add_comment { my ($self, $params) = @_; + # BMO: Don't allow updating of bugs if disabled + if (Bugzilla->params->{disable_bug_updates}) { + ThrowErrorPage('bug/process/updates-disabled.html.tmpl', + 'Bug updates are currently disabled.'); + } + #The user must login in order add a comment Bugzilla->login(LOGIN_REQUIRED); @@ -717,6 +775,12 @@ sub add_comment { sub update_see_also { my ($self, $params) = @_; + # BMO: Don't allow updating of bugs if disabled + if (Bugzilla->params->{disable_bug_updates}) { + ThrowErrorPage('bug/process/updates-disabled.html.tmpl', + 'Bug updates are currently disabled.'); + } + my $user = Bugzilla->login(LOGIN_REQUIRED); # Check parameters @@ -764,6 +828,8 @@ sub update_see_also { sub attachments { my ($self, $params) = validate(@_, 'ids', 'attachment_ids'); + Bugzilla->switch_to_shadow_db(); + if (!(defined $params->{ids} or defined $params->{attachment_ids})) { @@ -884,6 +950,9 @@ sub _bug_to_hash { @{ $bug->see_also }; $item{'see_also'} = \@see_also; } + if (filter_wants $params, 'flags') { + $item{'flags'} = [ map { $self->_flag_to_hash($_) } @{$bug->flags} ]; + } # And now custom fields my @custom_fields = Bugzilla->active_custom_fields; @@ -912,6 +981,7 @@ sub _bug_to_hash { # No need to format $bug->deadline specially, because Bugzilla::Bug # already does it for us. $item{'deadline'} = $self->type('string', $bug->deadline); + $item{'actual_time'} = $self->type('double', $bug->actual_time); } if (Bugzilla->user->id) { @@ -932,9 +1002,6 @@ sub _bug_to_hash { sub _attachment_to_hash { my ($self, $attach, $filters) = @_; - # Skipping attachment flags for now. - delete $attach->{flags}; - my $item = filter $filters, { creation_time => $self->type('dateTime', $attach->attached), last_change_time => $self->type('dateTime', $attach->modification_time), @@ -961,6 +1028,31 @@ sub _attachment_to_hash { $item->{'data'} = $self->type('base64', $attach->data); } + if (filter_wants $filters, 'flags') { + $item->{'flags'} = [ map { $self->_flag_to_hash($_) } @{$attach->flags} ]; + } + + return $item; +} + +sub _flag_to_hash { + my ($self, $flag) = @_; + + my $item = { + id => $self->type('int', $flag->id), + name => $self->type('string', $flag->name), + type_id => $self->type('int', $flag->type_id), + creation_date => $self->type('dateTime', $flag->creation_date), + modification_date => $self->type('dateTime', $flag->modification_date), + status => $self->type('string', $flag->status) + }; + + foreach my $field (qw(setter requestee)) { + my $field_id = $field . "_id"; + $item->{$field} = $self->type('string', $flag->$field->login) + if $flag->$field_id; + } + return $item; } @@ -1099,7 +1191,7 @@ values of the field are shown in the user interface. Can be null. This is an array of hashes, representing the legal values for select-type (drop-down and multiple-selection) fields. This is also -populated for the C<component>, C<version>, and C<target_milestone> +populated for the C<component>, C<version>, C<target_milestone>, and C<keywords> fields, but not for the C<product> field (you must use L<Product.get_accessible_products|Bugzilla::WebService::Product/get_accessible_products> for that. @@ -1132,6 +1224,11 @@ if the C<value_field> is set to one of the values listed in this array. Note that for per-product fields, C<value_field> is set to C<'product'> and C<visibility_values> will reflect which product(s) this value appears in. +=item C<description> + +C<string> The description of the value. This item is only included for the +C<keywords> field. + =item C<is_open> C<boolean> For C<bug_status> values, determines whether this status @@ -1361,6 +1458,48 @@ Also returned as C<attacher>, for backwards-compatibility with older Bugzillas. (However, this backwards-compatibility will go away in Bugzilla 5.0.) +=item C<flags> + +An array of hashes containing the information about flags currently set +for each attachment. Each flag hash contains the following items: + +=over + +=item C<id> + +C<int> The id of the flag. + +=item C<name> + +C<string> The name of the flag. + +=item C<type_id> + +C<int> The type id of the flag. + +=item C<creation_date> + +C<dateTime> The timestamp when this flag was originally created. + +=item C<modification_date> + +C<dateTime> The timestamp when the flag was last modified. + +=item C<status> + +C<string> The current status of the flag. + +=item C<setter> + +C<string> The login name of the user who created or last modified the flag. + +=item C<requestee> + +C<string> The login name of the user this flag has been requested to be granted or denied. +Note, this field is only returned if a requestee is set. + +=back + =back =item B<Errors> @@ -1397,6 +1536,8 @@ C<summary>. =back +=item The C<flags> array was added in Bugzilla B<4.4>. + =back @@ -1501,6 +1642,13 @@ Bugzillas. (However, this backwards-compatibility will go away in Bugzilla C<dateTime> The time (in Bugzilla's timezone) that the comment was added. +=item creation_time + +C<dateTime> This is exactly same as the C<time> key. Use this field instead of +C<time> for consistency with other methods including L</get> and L</attachments>. +For compatibility, C<time> is still usable. However, please note that C<time> +may be deprecated and removed in a future release. + =item is_private C<boolean> True if this comment is private (only visible to a certain @@ -1542,6 +1690,8 @@ C<creator>. =back +=item C<creation_time> was added in Bugzilla B<4.4>. + =back @@ -1601,6 +1751,13 @@ the valid ids. Each hash contains the following items: =over +=item C<actual_time> + +C<double> The total number of hours that this bug has taken (so far). + +If you are not in the time-tracking group, this field will not be included +in the return value. + =item C<alias> C<string> The unique alias of this bug. @@ -1659,6 +1816,48 @@ take. If you are not in the time-tracking group, this field will not be included in the return value. +=item C<flags> + +An array of hashes containing the information about flags currently set +for the bug. Each flag hash contains the following items: + +=over + +=item C<id> + +C<int> The id of the flag. + +=item C<name> + +C<string> The name of the flag. + +=item C<type_id> + +C<int> The type id of the flag. + +=item C<creation_date> + +C<dateTime> The timestamp when this flag was originally created. + +=item C<modification_date> + +C<dateTime> The timestamp when the flag was last modified. + +=item C<status> + +C<string> The current status of the flag. + +=item C<setter> + +C<string> The login name of the user who created or last modified the flag. + +=item C<requestee> + +C<string> The login name of the user this flag has been requested to be granted or denied. +Note, this field is only returned if a requestee is set. + +=back + =item C<groups> C<array> of C<string>s. The names of all the groups that this bug is in. @@ -1886,8 +2085,12 @@ C<op_sys>, C<platform>, C<qa_contact>, C<remaining_time>, C<see_also>, C<target_milestone>, C<update_token>, C<url>, C<version>, C<whiteboard>, and all custom fields. -=back +=item The C<flags> array was added in Bugzilla B<4.4>. + +=item The C<actual_time> item was added to the C<bugs> return value +in Bugzilla B<4.4>. +=back =back @@ -1993,6 +2196,10 @@ The same as L</get>. =item Added in Bugzilla B<3.4>. +=item Field names changed to be more consistent with other methods in Bugzilla B<4.4>. + +=item As of Bugzilla B<4.4>, field names now match names used by L<Bug.update|/"update"> for consistency. + =back =back @@ -2153,6 +2360,11 @@ C<string> Search the "Status Whiteboard" field on bugs for a substring. Works the same as the C<summary> field described above, but searches the Status Whiteboard field. +=item C<count_only> + +C<boolean> If count_only set to true, only a single hash key called C<bug_count> +will be returned which is the number of bugs that matched the search. + =back =item B<Returns> diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm index 3cd0d0a6c..c705ece28 100644 --- a/Bugzilla/WebService/Product.pm +++ b/Bugzilla/WebService/Product.pm @@ -47,23 +47,28 @@ BEGIN { *get_products = \&get } # Get the ids of the products the user can search sub get_selectable_products { + Bugzilla->switch_to_shadow_db(); return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]}; } # Get the ids of the products the user can enter bugs against sub get_enterable_products { + Bugzilla->switch_to_shadow_db(); return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]}; } # Get the union of the products the user can search and enter bugs against. sub get_accessible_products { + Bugzilla->switch_to_shadow_db(); return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]}; } # Get a list of actual products, based on list of ids or names sub get { my ($self, $params) = validate(@_, 'ids', 'names'); - + + Bugzilla->switch_to_shadow_db(); + # Only products that are in the users accessible products, # can be allowed to be returned my $accessible_products = Bugzilla->user->get_accessible_products; diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm index f8704a947..93c0881cb 100644 --- a/Bugzilla/WebService/User.pm +++ b/Bugzilla/WebService/User.pm @@ -29,6 +29,7 @@ use Bugzilla::Group; use Bugzilla::User; use Bugzilla::Util qw(trim); use Bugzilla::WebService::Util qw(filter validate); +use Bugzilla::Hook; # Don't need auth to login use constant LOGIN_EXEMPT => { @@ -126,6 +127,8 @@ sub create { sub get { my ($self, $params) = validate(@_, 'names', 'ids'); + Bugzilla->switch_to_shadow_db(); + defined($params->{names}) || defined($params->{ids}) || defined($params->{match}) || ThrowCodeError('params_required', @@ -200,17 +203,18 @@ sub get { my $in_group = $self->_filter_users_by_group( \@user_objects, $params); if (Bugzilla->user->in_group('editusers')) { - @users = + @users = map {filter $params, { id => $self->type('int', $_->id), real_name => $self->type('string', $_->name), name => $self->type('string', $_->login), email => $self->type('string', $_->email), can_login => $self->type('boolean', $_->is_enabled ? 1 : 0), + groups => $self->_filter_bless_groups($_->groups), email_enabled => $self->type('boolean', $_->email_enabled), login_denied_text => $self->type('string', $_->disabledtext), + saved_searches => [map { $self->_query_to_hash($_) } @{ $_->queries }], }} @$in_group; - } else { @users = @@ -220,9 +224,14 @@ sub get { name => $self->type('string', $_->login), email => $self->type('string', $_->email), can_login => $self->type('boolean', $_->is_enabled ? 1 : 0), + groups => $self->_filter_bless_groups($_->groups), + saved_searches => [map { $self->_query_to_hash($_) } @{ $_->queries }], }} @$in_group; } + Bugzilla::Hook::process('webservice_user_get', + { webservice => $self, params => $params, users => \@users }); + return { users => \@users }; } @@ -253,6 +262,40 @@ sub _user_in_any_group { return 0; } +sub _filter_bless_groups { + my ($self, $groups) = @_; + my $user = Bugzilla->user; + + my @filtered_groups; + foreach my $group (@$groups) { + next unless ($user->in_group('editusers') || $user->can_bless($group->id)); + push(@filtered_groups, $self->_group_to_hash($group)); + } + + return \@filtered_groups; +} + +sub _group_to_hash { + my ($self, $group) = @_; + my $item = { + id => $self->type('int', $group->id), + name => $self->type('string', $group->name), + description => $self->type('string', $group->description), + }; + return $item; +} + +sub _query_to_hash { + my ($self, $query) = @_; + my $item = { + id => $self->type('int', $query->id), + name => $self->type('string', $query->name), + url => $self->type('string', $query->url), + }; + + return $item; +} + 1; __END__ @@ -575,10 +618,60 @@ C<string> A text field that holds the reason for disabling a user from logging into bugzilla, if empty then the user account is enabled. Otherwise it is disabled/closed. +=item groups + +C<array> An array of group hashes the user is a member of. Each hash describes +the group and contains the following items: + +=over + +=item id + +C<int> The group id + +=item name + +C<string> The name of the group + +=item description + +C<string> The description for the group + +=back + +=over + +=item saved_searches + +C<array> An array of hashes, each of which represents a user's saved search and has +the following keys: + +=over + +=item id + +C<int> An integer id uniquely identifying the saved search. + +=item name + +C<string> The name of the saved search. + +=item url + +C<string> The CGI parameters for the saved search. + +=back + +B<Note>: The elements of the returned array (i.e. hashes) are ordered by the +name of each saved search. + +=back + B<Note>: If you are not logged in to Bugzilla when you call this function, you will only be returned the C<id>, C<name>, and C<real_name> items. If you are logged in and not in editusers group, you will only be returned the C<id>, C<name>, -C<real_name>, C<email>, and C<can_login> items. +C<real_name>, C<email>, and C<can_login> items. The groups returned are filtered +based on your permission to bless each group. =back @@ -614,6 +707,10 @@ function. =item C<include_disabled> added in Bugzilla B<4.0>. Default behavior for C<match> has changed to only returning enabled accounts. +=item C<groups> Added in Bugzilla B<4.4>. + +=item C<saved_searches> Added in Bugzilla B<4.4>. + =back =back |