| # 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::Flag; |
| |
| use 5.10.1; |
| use strict; |
| use warnings; |
| |
| =head1 NAME |
| |
| Bugzilla::Flag - A module to deal with Bugzilla flag values. |
| |
| =head1 SYNOPSIS |
| |
| Flag.pm provides an interface to flags as stored in Bugzilla. |
| See below for more information. |
| |
| =head1 NOTES |
| |
| =over |
| |
| =item * |
| |
| Import relevant functions from that script. |
| |
| =item * |
| |
| Use of private functions / variables outside this module may lead to |
| unexpected results after an upgrade. Please avoid using private |
| functions in other files/modules. Private functions are functions |
| whose names start with _ or a re specifically noted as being private. |
| |
| =back |
| |
| =cut |
| |
| use Scalar::Util qw(blessed); |
| use Storable qw(dclone); |
| |
| use Bugzilla::FlagType; |
| use Bugzilla::Hook; |
| use Bugzilla::User; |
| use Bugzilla::Util; |
| use Bugzilla::Error; |
| use Bugzilla::Mailer; |
| use Bugzilla::Constants; |
| use Bugzilla::Field; |
| |
| use parent qw(Bugzilla::Object Exporter); |
| @Bugzilla::Flag::EXPORT = qw(SKIP_REQUESTEE_ON_ERROR); |
| |
| ############################### |
| #### Initialization #### |
| ############################### |
| |
| use constant DB_TABLE => 'flags'; |
| use constant LIST_ORDER => 'id'; |
| # Flags are tracked in bugs_activity. |
| use constant AUDIT_CREATES => 0; |
| use constant AUDIT_UPDATES => 0; |
| use constant AUDIT_REMOVES => 0; |
| |
| use constant SKIP_REQUESTEE_ON_ERROR => 1; |
| |
| 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 |
| setter_id |
| status |
| type_id |
| ); |
| |
| use constant VALIDATORS => { |
| }; |
| |
| use constant UPDATE_VALIDATORS => { |
| setter => \&_check_setter, |
| status => \&_check_status, |
| }; |
| |
| ############################### |
| #### Accessors ###### |
| ############################### |
| |
| =head2 METHODS |
| |
| =over |
| |
| =item C<id> |
| |
| Returns the ID of the flag. |
| |
| =item C<name> |
| |
| Returns the name of the flagtype the flag belongs to. |
| |
| =item C<bug_id> |
| |
| Returns the ID of the bug this flag belongs to. |
| |
| =item C<attach_id> |
| |
| Returns the ID of the attachment this flag belongs to, if any. |
| |
| =item C<status> |
| |
| 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 |
| |
| sub id { return $_[0]->{'id'}; } |
| sub name { return $_[0]->type->name; } |
| sub type_id { return $_[0]->{'type_id'}; } |
| sub bug_id { return $_[0]->{'bug_id'}; } |
| 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 #### |
| ############################### |
| |
| =pod |
| |
| =over |
| |
| =item C<type> |
| |
| Returns the type of the flag, as a Bugzilla::FlagType object. |
| |
| =item C<setter> |
| |
| Returns the user who set the flag, as a Bugzilla::User object. |
| |
| =item C<requestee> |
| |
| Returns the user who has been requested to set the flag, as a |
| Bugzilla::User object. |
| |
| =item C<attachment> |
| |
| Returns the attachment object the flag belongs to if the flag |
| is an attachment flag, else undefined. |
| |
| =back |
| |
| =cut |
| |
| sub type { |
| my $self = shift; |
| |
| return $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'}); |
| } |
| |
| sub setter { |
| my $self = shift; |
| |
| return $self->{'setter'} ||= new Bugzilla::User({ id => $self->{'setter_id'}, cache => 1 }); |
| } |
| |
| sub requestee { |
| my $self = shift; |
| |
| if (!defined $self->{'requestee'} && $self->{'requestee_id'}) { |
| $self->{'requestee'} = new Bugzilla::User({ id => $self->{'requestee_id'}, cache => 1 }); |
| } |
| return $self->{'requestee'}; |
| } |
| |
| sub attachment { |
| my $self = shift; |
| return undef unless $self->attach_id; |
| |
| require Bugzilla::Attachment; |
| return $self->{'attachment'} |
| ||= new Bugzilla::Attachment({ id => $self->attach_id, cache => 1 }); |
| } |
| |
| sub bug { |
| my $self = shift; |
| |
| require Bugzilla::Bug; |
| return $self->{'bug'} ||= new Bugzilla::Bug({ id => $self->bug_id, cache => 1 }); |
| } |
| |
| ################################ |
| ## Searching/Retrieving Flags ## |
| ################################ |
| |
| =pod |
| |
| =over |
| |
| =item C<match($criteria)> |
| |
| Queries the database for flags matching the given criteria |
| (specified as a hash of field names and their matching values) |
| and returns an array of matching records. |
| |
| =back |
| |
| =cut |
| |
| sub match { |
| my $class = shift; |
| my ($criteria) = @_; |
| |
| # If the caller specified only bug or attachment flags, |
| # limit the query to those kinds of flags. |
| if (my $type = delete $criteria->{'target_type'}) { |
| if ($type eq 'bug') { |
| $criteria->{'attach_id'} = IS_NULL; |
| } |
| elsif (!defined $criteria->{'attach_id'}) { |
| $criteria->{'attach_id'} = NOT_NULL; |
| } |
| } |
| # Flag->snapshot() calls Flag->match() with bug_id and attach_id |
| # as hash keys, even if attach_id is undefined. |
| if (exists $criteria->{'attach_id'} && !defined $criteria->{'attach_id'}) { |
| $criteria->{'attach_id'} = IS_NULL; |
| } |
| |
| return $class->SUPER::match(@_); |
| } |
| |
| =pod |
| |
| =over |
| |
| =item C<count($criteria)> |
| |
| Queries the database for flags matching the given criteria |
| (specified as a hash of field names and their matching values) |
| and returns an array of matching records. |
| |
| =back |
| |
| =cut |
| |
| sub count { |
| my $class = shift; |
| return scalar @{$class->match(@_)}; |
| } |
| |
| ###################################################################### |
| # Creating and Modifying |
| ###################################################################### |
| |
| sub set_flag { |
| my ($class, $obj, $params) = @_; |
| |
| my ($bug, $attachment, $obj_flag, $requestee_changed); |
| if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) { |
| $attachment = $obj; |
| $bug = $attachment->bug; |
| } |
| elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) { |
| $bug = $obj; |
| } |
| else { |
| ThrowCodeError('flag_unexpected_object', { 'caller' => ref $obj }); |
| } |
| |
| # Make sure the user can change flags |
| my $privs; |
| $bug->check_can_change_field('flagtypes.name', 0, 1, \$privs) |
| || ThrowUserError('illegal_change', |
| { field => 'flagtypes.name', privs => $privs }); |
| |
| # Update (or delete) an existing flag. |
| if ($params->{id}) { |
| my $flag = $class->check({ id => $params->{id} }); |
| |
| # Security check: make sure the flag belongs to the bug/attachment. |
| # We don't check that the user editing the flag can see |
| # the bug/attachment. That's the job of the caller. |
| ($attachment && $flag->attach_id && $attachment->id == $flag->attach_id) |
| || (!$attachment && !$flag->attach_id && $bug->id == $flag->bug_id) |
| || ThrowCodeError('invalid_flag_association', |
| { bug_id => $bug->id, |
| attach_id => $attachment ? $attachment->id : undef }); |
| |
| # Extract the current flag object from the object. |
| my ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types}; |
| # If no flagtype can be found for this flag, this means the bug is being |
| # moved into a product/component where the flag is no longer valid. |
| # So either we can attach the flag to another flagtype having the same |
| # name, or we remove the flag. |
| if (!$obj_flagtype) { |
| my $success = $flag->retarget($obj); |
| return unless $success; |
| |
| ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types}; |
| push(@{$obj_flagtype->{flags}}, $flag); |
| } |
| ($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; |
| |
| ($obj_flag, $requestee_changed) = |
| $class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment); |
| } |
| # Create a new flag. |
| elsif ($params->{type_id}) { |
| # Don't bother validating types the user didn't touch. |
| return if $params->{status} eq 'X'; |
| |
| my $flagtype = Bugzilla::FlagType->check({ id => $params->{type_id} }); |
| # Security check: make sure the flag type belongs to the bug/attachment. |
| ($attachment && $flagtype->target_type eq 'attachment' |
| && scalar(grep { $_->id == $flagtype->id } @{$attachment->flag_types})) |
| || (!$attachment && $flagtype->target_type eq 'bug' |
| && scalar(grep { $_->id == $flagtype->id } @{$bug->flag_types})) |
| || ThrowCodeError('invalid_flag_association', |
| { bug_id => $bug->id, |
| attach_id => $attachment ? $attachment->id : undef }); |
| |
| # Make sure the flag type is active. |
| $flagtype->is_active |
| || ThrowCodeError('flag_type_inactive', { type => $flagtype->name }); |
| |
| # Extract the current flagtype object from the object. |
| my ($obj_flagtype) = grep { $_->id == $flagtype->id } @{$obj->flag_types}; |
| |
| # We cannot create a new flag if there is already one and this |
| # flag type is not multiplicable. |
| if (!$flagtype->is_multiplicable) { |
| if (scalar @{$obj_flagtype->{flags}}) { |
| ThrowUserError('flag_type_not_multiplicable', { type => $flagtype }); |
| } |
| } |
| |
| ($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 { |
| my ($class, $flag, $flag_type, $params, $bug, $attachment) = @_; |
| |
| # If it's a new flag, let's create it now. |
| my $obj_flag = $flag || bless({ type_id => $flag_type->id, |
| status => '', |
| bug_id => $bug->id, |
| attach_id => $attachment ? |
| $attachment->id : undef}, |
| $class); |
| |
| my $old_status = $obj_flag->status; |
| my $old_requestee_id = $obj_flag->requestee_id; |
| |
| $obj_flag->_set_status($params->{status}); |
| $obj_flag->_set_requestee($params->{requestee}, $bug, $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) || $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 |
| |
| =over |
| |
| =item C<create($flag, $timestamp)> |
| |
| Creates a flag record in the database. |
| |
| =back |
| |
| =cut |
| |
| sub create { |
| my ($class, $flag, $timestamp) = @_; |
| $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; |
| |
| $flag = $class->SUPER::create($params); |
| return $flag; |
| } |
| |
| sub update { |
| my $self = shift; |
| my $dbh = Bugzilla->dbh; |
| my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); |
| |
| my $changes = $self->SUPER::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', Bugzilla->local_timezone); |
| Bugzilla->memcached->clear({ table => 'flags', id => $self->id }); |
| } |
| return $changes; |
| } |
| |
| sub snapshot { |
| my ($class, $flags) = @_; |
| |
| my @summaries; |
| foreach my $flag (@$flags) { |
| my $summary = $flag->setter->nick . ':' . $flag->type->name . $flag->status; |
| $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee; |
| push(@summaries, $summary); |
| } |
| return @summaries; |
| } |
| |
| sub update_activity { |
| my ($class, $old_summaries, $new_summaries) = @_; |
| |
| my ($removed, $added) = diff_arrays($old_summaries, $new_summaries); |
| if (scalar @$removed || scalar @$added) { |
| # Remove flag requester/setter information |
| foreach (@$removed, @$added) { s/^[^:]+:// } |
| |
| $removed = join(", ", @$removed); |
| $added = join(", ", @$added); |
| return ($removed, $added); |
| } |
| return (); |
| } |
| |
| sub update_flags { |
| my ($class, $self, $old_self, $timestamp) = @_; |
| |
| my @old_summaries = $class->snapshot($old_self->flags); |
| my %old_flags = map { $_->id => $_ } @{$old_self->flags}; |
| |
| foreach my $new_flag (@{$self->flags}) { |
| if (!$new_flag->id) { |
| # This is a new flag. |
| my $flag = $class->create($new_flag, $timestamp); |
| $new_flag->{id} = $flag->id; |
| $class->notify($new_flag, undef, $self, $timestamp); |
| } |
| else { |
| my $changes = $new_flag->update($timestamp); |
| if (scalar(keys %$changes)) { |
| $class->notify($new_flag, $old_flags{$new_flag->id}, $self, $timestamp); |
| } |
| delete $old_flags{$new_flag->id}; |
| } |
| } |
| # These flags have been deleted. |
| foreach my $old_flag (values %old_flags) { |
| $class->notify(undef, $old_flag, $self, $timestamp); |
| $old_flag->remove_from_db(); |
| } |
| |
| # If the bug has been moved into another product or component, |
| # we must also take care of attachment flags which are no longer valid, |
| # as well as all bug flags which haven't been forgotten above. |
| if ($self->isa('Bugzilla::Bug') |
| && ($self->{_old_product_name} || $self->{_old_component_name})) |
| { |
| my @removed = $class->force_cleanup($self); |
| push(@old_summaries, @removed); |
| } |
| |
| my @new_summaries = $class->snapshot($self->flags); |
| my @changes = $class->update_activity(\@old_summaries, \@new_summaries); |
| |
| Bugzilla::Hook::process('flag_end_of_update', { object => $self, |
| timestamp => $timestamp, |
| old_flags => \@old_summaries, |
| new_flags => \@new_summaries, |
| }); |
| return @changes; |
| } |
| |
| sub retarget { |
| my ($self, $obj) = @_; |
| |
| my @flagtypes = grep { $_->name eq $self->type->name } @{$obj->flag_types}; |
| |
| my $success = 0; |
| foreach my $flagtype (@flagtypes) { |
| next if !$flagtype->is_active; |
| next if (!$flagtype->is_multiplicable && scalar @{$flagtype->{flags}}); |
| next unless (($self->status eq '?' && $self->setter->can_request_flag($flagtype)) |
| || $self->setter->can_set_flag($flagtype)); |
| |
| $self->{type_id} = $flagtype->id; |
| delete $self->{type}; |
| $success = 1; |
| last; |
| } |
| return $success; |
| } |
| |
| # In case the bug's product/component has changed, clear flags that are |
| # no longer valid. |
| sub force_cleanup { |
| my ($class, $bug) = @_; |
| my $dbh = Bugzilla->dbh; |
| |
| my $flag_ids = $dbh->selectcol_arrayref( |
| 'SELECT DISTINCT flags.id |
| FROM flags |
| INNER JOIN bugs |
| ON flags.bug_id = bugs.bug_id |
| LEFT JOIN flaginclusions AS i |
| ON flags.type_id = i.type_id |
| AND (bugs.product_id = i.product_id OR i.product_id IS NULL) |
| AND (bugs.component_id = i.component_id OR i.component_id IS NULL) |
| WHERE bugs.bug_id = ? AND i.type_id IS NULL', |
| undef, $bug->id); |
| |
| my @removed = $class->force_retarget($flag_ids, $bug); |
| |
| $flag_ids = $dbh->selectcol_arrayref( |
| 'SELECT DISTINCT flags.id |
| FROM flags, bugs, flagexclusions e |
| WHERE bugs.bug_id = ? |
| AND flags.bug_id = bugs.bug_id |
| AND flags.type_id = e.type_id |
| AND (bugs.product_id = e.product_id OR e.product_id IS NULL) |
| AND (bugs.component_id = e.component_id OR e.component_id IS NULL)', |
| undef, $bug->id); |
| |
| push(@removed , $class->force_retarget($flag_ids, $bug)); |
| return @removed; |
| } |
| |
| sub force_retarget { |
| my ($class, $flag_ids, $bug) = @_; |
| my $dbh = Bugzilla->dbh; |
| |
| my $flags = $class->new_from_list($flag_ids); |
| my @removed; |
| foreach my $flag (@$flags) { |
| # $bug is undefined when e.g. editing inclusion and exclusion lists. |
| my $obj = $flag->attachment || $bug || $flag->bug; |
| my $is_retargetted = $flag->retarget($obj); |
| if ($is_retargetted) { |
| $dbh->do('UPDATE flags SET type_id = ? WHERE id = ?', |
| undef, ($flag->type_id, $flag->id)); |
| Bugzilla->memcached->clear({ table => 'flags', id => $flag->id }); |
| } |
| else { |
| # Track deleted attachment flags. |
| push(@removed, $class->snapshot([$flag])) if $flag->attach_id; |
| $class->notify(undef, $flag, $bug || $flag->bug); |
| $flag->remove_from_db(); |
| } |
| } |
| return @removed; |
| } |
| |
| ############################### |
| #### Validators ###### |
| ############################### |
| |
| sub _set_requestee { |
| my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_; |
| |
| $self->{requestee} = |
| $self->_check_requestee($requestee, $bug, $attachment, $skip_requestee_on_error); |
| |
| $self->{requestee_id} = |
| $self->{requestee} ? $self->{requestee}->id : undef; |
| } |
| |
| sub _set_setter { |
| my ($self, $setter) = @_; |
| |
| $self->set('setter', $setter); |
| $self->{setter_id} = $self->setter->id; |
| } |
| |
| sub _set_status { |
| my ($self, $status) = @_; |
| |
| # Store the old flag status. It's needed by _check_setter(). |
| $self->{_old_status} = $self->status; |
| $self->set('status', $status); |
| } |
| |
| sub _check_requestee { |
| my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_; |
| |
| # If the flag status is not "?", then no requestee can be defined. |
| return undef if ($self->status ne '?'); |
| |
| # Store this value before updating the flag object. |
| my $old_requestee = $self->requestee ? $self->requestee->login : ''; |
| |
| if ($self->status eq '?' && $requestee) { |
| $requestee = Bugzilla::User->check($requestee); |
| } |
| else { |
| undef $requestee; |
| } |
| |
| if ($requestee && $requestee->login ne $old_requestee) { |
| # Make sure the user didn't specify a requestee unless the flag |
| # is specifically requestable. For existing flags, if the requestee |
| # was set before the flag became specifically unrequestable, the |
| # user can either remove them or leave them alone. |
| ThrowUserError('flag_type_requestee_disabled', { type => $self->type }) |
| if !$self->type->is_requesteeble; |
| |
| # You can't ask a disabled account, as they don't have the ability to |
| # set the flag. |
| ThrowUserError('flag_requestee_disabled', { requestee => $requestee }) |
| if !$requestee->is_enabled; |
| |
| # Make sure the requestee can see the bug. |
| # Note that can_see_bug() will query the DB, so if the bug |
| # is being added/removed from some groups and these changes |
| # haven't been committed to the DB yet, they won't be taken |
| # into account here. In this case, old group restrictions matter. |
| # However, if the user has just been changed to the assignee, |
| # qa_contact, or added to the cc list of the bug and the bug |
| # is cclist_accessible, the requestee is allowed. |
| if (!$requestee->can_see_bug($self->bug_id) |
| && (!$bug->cclist_accessible |
| || !grep($_->id == $requestee->id, @{ $bug->cc_users }) |
| && $requestee->id != $bug->assigned_to->id |
| && (!$bug->qa_contact || $requestee->id != $bug->qa_contact->id))) |
| { |
| if ($skip_requestee_on_error) { |
| undef $requestee; |
| } |
| else { |
| ThrowUserError('flag_requestee_unauthorized', |
| { flag_type => $self->type, |
| requestee => $requestee, |
| bug_id => $self->bug_id, |
| attach_id => $self->attach_id }); |
| } |
| } |
| # Make sure the requestee can see the private attachment. |
| elsif ($self->attach_id && $attachment->isprivate && !$requestee->is_insider) { |
| if ($skip_requestee_on_error) { |
| undef $requestee; |
| } |
| else { |
| ThrowUserError('flag_requestee_unauthorized_attachment', |
| { flag_type => $self->type, |
| requestee => $requestee, |
| bug_id => $self->bug_id, |
| attach_id => $self->attach_id }); |
| } |
| } |
| # Make sure the user is allowed to set the flag. |
| elsif (!$requestee->can_set_flag($self->type)) { |
| if ($skip_requestee_on_error) { |
| undef $requestee; |
| } |
| else { |
| ThrowUserError('flag_requestee_needs_privs', |
| {'requestee' => $requestee, |
| 'flagtype' => $self->type}); |
| } |
| } |
| } |
| return $requestee; |
| } |
| |
| sub _check_setter { |
| my ($self, $setter) = @_; |
| |
| # By default, the currently logged in user is the setter. |
| $setter ||= Bugzilla->user; |
| (blessed($setter) && $setter->isa('Bugzilla::User') && $setter->id) |
| || ThrowUserError('invalid_user'); |
| |
| # set_status() has already been called. So this refers |
| # to the new flag status. |
| my $status = $self->status; |
| |
| # Make sure the user is authorized to modify flags, see bug 180879: |
| # - The flag exists and is unchanged. |
| # - The flag setter can unset flag. |
| # - Users in the request_group can clear pending requests and set flags |
| # and can rerequest set flags. |
| # - Users in the grant_group can set/clear flags, including "+" and "-". |
| unless (($status eq $self->{_old_status}) |
| || ($status eq 'X' && $setter->id == Bugzilla->user->id) |
| || (($status eq 'X' || $status eq '?') |
| && $setter->can_request_flag($self->type)) |
| || $setter->can_set_flag($self->type)) |
| { |
| ThrowUserError('flag_update_denied', |
| { name => $self->type->name, |
| status => $status, |
| old_status => $self->{_old_status} }); |
| } |
| |
| # If the request is being retargetted, we don't update |
| # the setter, so that the setter gets the notification. |
| if ($status eq '?' && $self->{_old_status} eq '?') { |
| return $self->setter; |
| } |
| return $setter; |
| } |
| |
| sub _check_status { |
| my ($self, $status) = @_; |
| |
| # - Make sure the status is valid. |
| # - Make sure the user didn't request the flag unless it's requestable. |
| # If the flag existed and was requested before it became unrequestable, |
| # leave it as is. |
| if (!grep($status eq $_ , qw(X + - ?)) |
| || ($status eq '?' && $self->status ne '?' && !$self->type->is_requestable)) |
| { |
| ThrowUserError('flag_status_invalid', { id => $self->id, |
| status => $status }); |
| } |
| return $status; |
| } |
| |
| ###################################################################### |
| # Utility Functions |
| ###################################################################### |
| |
| =pod |
| |
| =over |
| |
| =item C<extract_flags_from_cgi($bug, $attachment, $hr_vars)> |
| |
| Checks whether or not there are new flags to create and returns an |
| array of hashes. This array is then passed to Flag::create(). |
| |
| =back |
| |
| =cut |
| |
| sub extract_flags_from_cgi { |
| my ($class, $bug, $attachment, $vars, $skip) = @_; |
| my $cgi = Bugzilla->cgi; |
| |
| my $match_status = Bugzilla::User::match_field({ |
| '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' }, |
| }, undef, $skip); |
| |
| $vars->{'match_field'} = 'requestee'; |
| if ($match_status == USER_MATCH_FAILED) { |
| $vars->{'message'} = 'user_match_failed'; |
| } |
| elsif ($match_status == USER_MATCH_MULTIPLE) { |
| $vars->{'message'} = 'user_match_multiple'; |
| } |
| |
| # Extract a list of flag type IDs from field names. |
| my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param()); |
| @flagtype_ids = grep($cgi->param("flag_type-$_") ne 'X', @flagtype_ids); |
| |
| # Extract a list of existing flag IDs. |
| my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param()); |
| |
| return ([], []) unless (scalar(@flagtype_ids) || scalar(@flag_ids)); |
| |
| my (@new_flags, @flags); |
| foreach my $flag_id (@flag_ids) { |
| my $flag = $class->new($flag_id); |
| # If the flag no longer exists, ignore it. |
| next unless $flag; |
| |
| my $status = $cgi->param("flag-$flag_id"); |
| |
| # If the user entered more than one name into the requestee field |
| # (i.e. they want more than one person to set the flag) we can reuse |
| # the existing flag for the first person (who may well be the existing |
| # requestee), but we have to create new flags for each additional requestee. |
| my @requestees = $cgi->param("requestee-$flag_id"); |
| my $requestee_email; |
| if ($status eq "?" |
| && scalar(@requestees) > 1 |
| && $flag->type->is_multiplicable) |
| { |
| # The first person, for which we'll reuse the existing flag. |
| $requestee_email = shift(@requestees); |
| |
| # Create new flags like the existing one for each additional person. |
| foreach my $login (@requestees) { |
| push(@new_flags, { type_id => $flag->type_id, |
| status => "?", |
| requestee => $login, |
| skip_roe => $skip }); |
| } |
| } |
| elsif ($status eq "?" && scalar(@requestees)) { |
| # If there are several requestees and the flag type is not multiplicable, |
| # this will fail. But that's the job of the validator to complain. All |
| # we do here is to extract and convert data from the CGI. |
| $requestee_email = trim($cgi->param("requestee-$flag_id") || ''); |
| } |
| |
| push(@flags, { id => $flag_id, |
| status => $status, |
| requestee => $requestee_email, |
| skip_roe => $skip }); |
| } |
| |
| # Get a list of active flag types available for this product/component. |
| my $flag_types = Bugzilla::FlagType::match( |
| { 'product_id' => $bug->{'product_id'}, |
| 'component_id' => $bug->{'component_id'}, |
| 'is_active' => 1 }); |
| |
| foreach my $flagtype_id (@flagtype_ids) { |
| # Checks if there are unexpected flags for the product/component. |
| if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) { |
| $vars->{'message'} = 'unexpected_flag_types'; |
| last; |
| } |
| } |
| |
| foreach my $flag_type (@$flag_types) { |
| my $type_id = $flag_type->id; |
| |
| # Bug flags are only valid for bugs, and attachment flags are |
| # only valid for attachments. So don't mix both. |
| next unless ($flag_type->target_type eq 'bug' xor $attachment); |
| |
| # We are only interested in flags the user tries to create. |
| next unless scalar(grep { $_ == $type_id } @flagtype_ids); |
| |
| # Get the number of flags of this type already set for this target. |
| my $has_flags = $class->count( |
| { 'type_id' => $type_id, |
| 'target_type' => $attachment ? 'attachment' : 'bug', |
| 'bug_id' => $bug->bug_id, |
| 'attach_id' => $attachment ? $attachment->id : undef }); |
| |
| # Do not create a new flag of this type if this flag type is |
| # not multiplicable and already has a flag set. |
| next if (!$flag_type->is_multiplicable && $has_flags); |
| |
| my $status = $cgi->param("flag_type-$type_id"); |
| trick_taint($status); |
| |
| my @logins = $cgi->param("requestee_type-$type_id"); |
| if ($status eq "?" && scalar(@logins)) { |
| foreach my $login (@logins) { |
| push (@new_flags, { type_id => $type_id, |
| status => $status, |
| requestee => $login, |
| skip_roe => $skip }); |
| last unless $flag_type->is_multiplicable; |
| } |
| } |
| else { |
| push (@new_flags, { type_id => $type_id, |
| status => $status }); |
| } |
| } |
| |
| # Return the list of flags to update and/or to create. |
| return (\@flags, \@new_flags); |
| } |
| |
| =pod |
| |
| =over |
| |
| =item C<multi_extract_flags_from_cgi($bug, $hr_vars)> |
| |
| Checks whether or not there are new flags to create and returns an |
| array of hashes. This array is then passed to Flag::create(). This differs |
| from the previous sub-routine as it is called for changing multiple bugs |
| |
| =back |
| |
| =cut |
| |
| sub multi_extract_flags_from_cgi { |
| my ($class, $bug, $vars, $skip) = @_; |
| my $cgi = Bugzilla->cgi; |
| |
| my $match_status = Bugzilla::User::match_field({ |
| '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' }, |
| }, undef, $skip); |
| |
| $vars->{'match_field'} = 'requestee'; |
| if ($match_status == USER_MATCH_FAILED) { |
| $vars->{'message'} = 'user_match_failed'; |
| } |
| elsif ($match_status == USER_MATCH_MULTIPLE) { |
| $vars->{'message'} = 'user_match_multiple'; |
| } |
| |
| # Extract a list of flag type IDs from field names. |
| my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param()); |
| |
| my (@new_flags, @flags); |
| |
| # Get a list of active flag types available for this product/component. |
| my $flag_types = Bugzilla::FlagType::match( |
| { 'product_id' => $bug->{'product_id'}, |
| 'component_id' => $bug->{'component_id'}, |
| 'is_active' => 1 }); |
| |
| foreach my $flagtype_id (@flagtype_ids) { |
| # Checks if there are unexpected flags for the product/component. |
| if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) { |
| $vars->{'message'} = 'unexpected_flag_types'; |
| last; |
| } |
| } |
| |
| foreach my $flag_type (@$flag_types) { |
| my $type_id = $flag_type->id; |
| |
| # Bug flags are only valid for bugs |
| next unless ($flag_type->target_type eq 'bug'); |
| |
| # We are only interested in flags the user tries to create. |
| next unless scalar(grep { $_ == $type_id } @flagtype_ids); |
| |
| # Get the flags of this type already set for this bug. |
| my $current_flags = $class->match( |
| { 'type_id' => $type_id, |
| 'target_type' => 'bug', |
| 'bug_id' => $bug->bug_id }); |
| |
| # We will update existing flags (instead of creating new ones) |
| # if the flag exists and the user has not chosen the 'always add' |
| # option |
| my $update = scalar(@$current_flags) && ! $cgi->param("flags_add-$type_id"); |
| |
| my $status = $cgi->param("flag_type-$type_id"); |
| trick_taint($status); |
| |
| my @logins = $cgi->param("requestee_type-$type_id"); |
| if ($status eq "?" && scalar(@logins)) { |
| foreach my $login (@logins) { |
| if ($update) { |
| foreach my $current_flag (@$current_flags) { |
| push (@flags, { id => $current_flag->id, |
| status => $status, |
| requestee => $login, |
| skip_roe => $skip }); |
| } |
| } |
| else { |
| push (@new_flags, { type_id => $type_id, |
| status => $status, |
| requestee => $login, |
| skip_roe => $skip }); |
| } |
| |
| last unless $flag_type->is_multiplicable; |
| } |
| } |
| else { |
| if ($update) { |
| foreach my $current_flag (@$current_flags) { |
| push (@flags, { id => $current_flag->id, |
| status => $status }); |
| } |
| } |
| else { |
| push (@new_flags, { type_id => $type_id, |
| status => $status }); |
| } |
| } |
| } |
| |
| # Return the list of flags to update and/or to create. |
| return (\@flags, \@new_flags); |
| } |
| |
| =pod |
| |
| =over |
| |
| =item C<notify($flag, $old_flag, $object, $timestamp)> |
| |
| Sends an email notification about a flag being created, fulfilled |
| or deleted. |
| |
| =back |
| |
| =cut |
| |
| sub notify { |
| my ($class, $flag, $old_flag, $obj, $timestamp) = @_; |
| |
| my ($bug, $attachment); |
| if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) { |
| $attachment = $obj; |
| $bug = $attachment->bug; |
| } |
| elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) { |
| $bug = $obj; |
| } |
| else { |
| # Not a good time to throw an error. |
| return; |
| } |
| |
| my $addressee; |
| # If the flag is set to '?', maybe the requestee wants a notification. |
| if ($flag && $flag->requestee_id |
| && (!$old_flag || ($old_flag->requestee_id || 0) != $flag->requestee_id)) |
| { |
| if ($flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) { |
| $addressee = $flag->requestee; |
| } |
| } |
| elsif ($old_flag && $old_flag->status eq '?' |
| && (!$flag || $flag->status ne '?')) |
| { |
| if ($old_flag->setter->wants_mail([EVT_REQUESTED_FLAG])) { |
| $addressee = $old_flag->setter; |
| } |
| } |
| |
| my $cc_list = $flag ? $flag->type->cc_list : $old_flag->type->cc_list; |
| # Is there someone to notify? |
| return unless ($addressee || $cc_list); |
| |
| # The email client will display the Date: header in the desired timezone, |
| # so we can always use UTC here. |
| $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); |
| $timestamp = format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC'); |
| |
| # If the target bug is restricted to one or more groups, then we need |
| # to make sure we don't send email about it to unauthorized users |
| # on the request type's CC: list, so we have to trawl the list for users |
| # not in those groups or email addresses that don't have an account. |
| my @bug_in_groups = grep {$_->{'ison'} || $_->{'mandatory'}} @{$bug->groups}; |
| my $attachment_is_private = $attachment ? $attachment->isprivate : undef; |
| |
| my %recipients; |
| foreach my $cc (split(/[, ]+/, $cc_list)) { |
| my $ccuser = new Bugzilla::User({ name => $cc }); |
| next if (scalar(@bug_in_groups) && (!$ccuser || !$ccuser->can_see_bug($bug->bug_id))); |
| next if $attachment_is_private && (!$ccuser || !$ccuser->is_insider); |
| # Prevent duplicated entries due to case sensitivity. |
| $cc = $ccuser ? $ccuser->email : $cc; |
| $recipients{$cc} = $ccuser; |
| } |
| |
| # Only notify if the addressee is allowed to receive the email. |
| if ($addressee && $addressee->email_enabled) { |
| $recipients{$addressee->email} = $addressee; |
| } |
| # Process and send notification for each recipient. |
| # If there are users in the CC list who don't have an account, |
| # use the default language for email notifications. |
| my $default_lang; |
| if (grep { !$_ } values %recipients) { |
| $default_lang = Bugzilla::User->new()->setting('lang'); |
| } |
| |
| # Get comments on the bug |
| my $all_comments = $bug->comments({ after => $bug->lastdiffed }); |
| @$all_comments = grep { $_->type || $_->body =~ /\S/ } @$all_comments; |
| |
| # Get public only comments |
| my $public_comments = [ grep { !$_->is_private } @$all_comments ]; |
| |
| foreach my $to (keys %recipients) { |
| # Add threadingmarker to allow flag notification emails to be the |
| # threaded similar to normal bug change emails. |
| my $thread_user_id = $recipients{$to} ? $recipients{$to}->id : 0; |
| |
| # We only want to show private comments to users in the is_insider group |
| my $comments = $recipients{$to} && $recipients{$to}->is_insider |
| ? $all_comments : $public_comments; |
| |
| my $vars = { |
| flag => $flag, |
| old_flag => $old_flag, |
| to => $to, |
| date => $timestamp, |
| bug => $bug, |
| attachment => $attachment, |
| threadingmarker => build_thread_marker($bug->id, $thread_user_id), |
| new_comments => $comments, |
| }; |
| |
| my $lang = $recipients{$to} ? |
| $recipients{$to}->setting('lang') : $default_lang; |
| |
| my $template = Bugzilla->template_inner($lang); |
| my $message; |
| $template->process("email/flagmail.txt.tmpl", $vars, \$message) |
| || ThrowTemplateError($template->error()); |
| |
| MessageToMTA($message); |
| } |
| } |
| |
| # This is an internal function used by $bug->flag_types |
| # and $attachment->flag_types to collect data about available |
| # flag types and existing flags set on them. You should never |
| # call this function directly. |
| sub _flag_types { |
| my ($class, $vars) = @_; |
| |
| my $target_type = $vars->{target_type}; |
| my $flags; |
| |
| # Retrieve all existing flags for this bug/attachment. |
| if ($target_type eq 'bug') { |
| my $bug_id = delete $vars->{bug_id}; |
| $flags = $class->match({target_type => 'bug', bug_id => $bug_id}); |
| } |
| elsif ($target_type eq 'attachment') { |
| my $attach_id = delete $vars->{attach_id}; |
| $flags = $class->match({attach_id => $attach_id}); |
| } |
| else { |
| ThrowCodeError('bad_arg', {argument => 'target_type', |
| function => $class . '->_flag_types'}); |
| } |
| |
| # Get all available flag types for the given product and component. |
| my $cache = Bugzilla->request_cache->{flag_types_per_component}->{$vars->{target_type}} ||= {}; |
| my $flag_data = $cache->{$vars->{component_id}} ||= Bugzilla::FlagType::match($vars); |
| my $flag_types = dclone($flag_data); |
| |
| $_->{flags} = [] foreach @$flag_types; |
| my %flagtypes = map { $_->id => $_ } @$flag_types; |
| |
| # Group existing flags per type, and skip those becoming invalid |
| # (which can happen when a bug is being moved into a new product |
| # or component). |
| @$flags = grep { exists $flagtypes{$_->type_id} } @$flags; |
| push(@{$flagtypes{$_->type_id}->{flags}}, $_) foreach @$flags; |
| return $flag_types; |
| } |
| |
| 1; |
| |
| =head1 B<Methods in need of POD> |
| |
| =over |
| |
| =item update_activity |
| |
| =item setter_id |
| |
| =item bug |
| |
| =item requestee_id |
| |
| =item DB_COLUMNS |
| |
| =item set_flag |
| |
| =item type_id |
| |
| =item snapshot |
| |
| =item update_flags |
| |
| =item update |
| |
| =back |