123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194 |
- # -*- 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 Netscape Communications
- # Corporation. Portions created by Netscape are
- # Copyright (C) 1998 Netscape Communications Corporation. All
- # Rights Reserved.
- #
- # Contributor(s): Myk Melez <myk@mozilla.org>
- # Jouni Heikniemi <jouni@heikniemi.net>
- # Frédéric Buclin <LpSolit@gmail.com>
- use strict;
- package Bugzilla::Flag;
- =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 Bugzilla::FlagType;
- use Bugzilla::Hook;
- use Bugzilla::User;
- use Bugzilla::Util;
- use Bugzilla::Error;
- use Bugzilla::Mailer;
- use Bugzilla::Constants;
- use Bugzilla::Field;
- use base qw(Bugzilla::Object Exporter);
- @Bugzilla::Flag::EXPORT = qw(SKIP_REQUESTEE_ON_ERROR);
- ###############################
- #### Initialization ####
- ###############################
- use constant DB_COLUMNS => qw(
- flags.id
- flags.type_id
- flags.bug_id
- flags.attach_id
- flags.requestee_id
- flags.setter_id
- flags.status
- );
- use constant DB_TABLE => 'flags';
- use constant LIST_ORDER => 'id';
- use constant SKIP_REQUESTEE_ON_ERROR => 1;
- ###############################
- #### 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.
- =back
- =cut
- sub id { return $_[0]->{'id'}; }
- sub name { return $_[0]->type->name; }
- sub bug_id { return $_[0]->{'bug_id'}; }
- sub attach_id { return $_[0]->{'attach_id'}; }
- sub status { return $_[0]->{'status'}; }
- ###############################
- #### 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;
- $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'});
- return $self->{'type'};
- }
- sub setter {
- my $self = shift;
- $self->{'setter'} ||= new Bugzilla::User($self->{'setter_id'});
- return $self->{'setter'};
- }
- sub requestee {
- my $self = shift;
- if (!defined $self->{'requestee'} && $self->{'requestee_id'}) {
- $self->{'requestee'} = new Bugzilla::User($self->{'requestee_id'});
- }
- return $self->{'requestee'};
- }
- sub attachment {
- my $self = shift;
- return undef unless $self->attach_id;
- require Bugzilla::Attachment;
- $self->{'attachment'} ||= Bugzilla::Attachment->get($self->attach_id);
- return $self->{'attachment'};
- }
- ################################
- ## Searching/Retrieving Flags ##
- ################################
- =pod
- =over
- =item C<has_flags>
- Returns 1 if at least one flag exists in the DB, else 0. This subroutine
- is mainly used to decide to display the "(My )Requests" link in the footer.
- =back
- =cut
- sub has_flags {
- my $dbh = Bugzilla->dbh;
- my $has_flags = $dbh->selectrow_array('SELECT 1 FROM flags ' . $dbh->sql_limit(1));
- return $has_flags || 0;
- }
- =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
- ######################################################################
- =pod
- =over
- =item C<validate($bug_id, $attach_id, $skip_requestee_on_error)>
- Validates fields containing flag modifications.
- If the attachment is new, it has no ID yet and $attach_id is set
- to -1 to force its check anyway.
- =back
- =cut
- sub validate {
- my ($bug_id, $attach_id, $skip_requestee_on_error) = @_;
- my $cgi = Bugzilla->cgi;
- my $dbh = Bugzilla->dbh;
- # Get a list of flags to validate. Uses the "map" function
- # to extract flag IDs from form field names by matching fields
- # whose name looks like "flag_type-nnn" (new flags) or "flag-nnn"
- # (existing flags), where "nnn" is the ID, and returning just
- # the ID portion of matching field names.
- my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
- my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
- return unless (scalar(@flagtype_ids) || scalar(@flag_ids));
- # No flag reference should exist when changing several bugs at once.
- ThrowCodeError("flags_not_available", { type => 'b' }) unless $bug_id;
- # We don't check that these new flags are valid for this bug/attachment,
- # because the bug may be moved into another product meanwhile.
- # This check will be done later when creating new flags, see FormToNewFlags().
- if (scalar(@flag_ids)) {
- # No reference to existing flags should exist when creating a new
- # attachment.
- if ($attach_id && ($attach_id < 0)) {
- ThrowCodeError('flags_not_available', { type => 'a' });
- }
- # Make sure all existing flags belong to the bug/attachment
- # they pretend to be.
- my $field = ($attach_id) ? "attach_id" : "bug_id";
- my $field_id = $attach_id || $bug_id;
- my $not = ($attach_id) ? "" : "NOT";
- my $invalid_data =
- $dbh->selectrow_array(
- "SELECT 1 FROM flags
- WHERE "
- . $dbh->sql_in('id', \@flag_ids)
- . " AND ($field != ? OR attach_id IS $not NULL) "
- . $dbh->sql_limit(1), undef, $field_id);
- if ($invalid_data) {
- ThrowCodeError('invalid_flag_association',
- { bug_id => $bug_id,
- attach_id => $attach_id });
- }
- }
- # Validate new flags.
- foreach my $id (@flagtype_ids) {
- my $status = $cgi->param("flag_type-$id");
- my @requestees = $cgi->param("requestee_type-$id");
- my $private_attachment = $cgi->param('isprivate') ? 1 : 0;
- # Don't bother validating types the user didn't touch.
- next if $status eq 'X';
- # Make sure the flag type exists. If it doesn't, FormToNewFlags()
- # will ignore it, so it's safe to ignore it here.
- my $flag_type = new Bugzilla::FlagType($id);
- next unless $flag_type;
- # Make sure the flag type is active.
- unless ($flag_type->is_active) {
- ThrowCodeError('flag_type_inactive', {'type' => $flag_type->name});
- }
- _validate(undef, $flag_type, $status, undef, \@requestees, $private_attachment,
- $bug_id, $attach_id, $skip_requestee_on_error);
- }
- # Validate existing flags.
- foreach my $id (@flag_ids) {
- my $status = $cgi->param("flag-$id");
- my @requestees = $cgi->param("requestee-$id");
- my $private_attachment = $cgi->param('isprivate') ? 1 : 0;
- # Make sure the flag exists. If it doesn't, process() will ignore it,
- # so it's safe to ignore it here.
- my $flag = new Bugzilla::Flag($id);
- next unless $flag;
- _validate($flag, $flag->type, $status, undef, \@requestees, $private_attachment,
- undef, undef, $skip_requestee_on_error);
- }
- }
- sub _validate {
- my ($flag, $flag_type, $status, $setter, $requestees, $private_attachment,
- $bug_id, $attach_id, $skip_requestee_on_error) = @_;
- # By default, the flag setter (or requester) is the current user.
- $setter ||= Bugzilla->user;
- my $id = $flag ? $flag->id : $flag_type->id; # Used in the error messages below.
- $bug_id ||= $flag->bug_id;
- $attach_id ||= $flag->attach_id if $flag; # Maybe it's a bug flag.
- # Make sure the user chose a valid status.
- grep($status eq $_, qw(X + - ?))
- || ThrowCodeError('flag_status_invalid',
- { id => $id, status => $status });
- # 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 ($status eq '?'
- && (!$flag || $flag->status ne '?')
- && !$flag_type->is_requestable)
- {
- ThrowCodeError('flag_status_invalid',
- { id => $id, status => $status });
- }
- # 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, don't
- # let the user change the requestee, but let the user remove it by
- # entering an empty string for the requestee.
- if ($status eq '?' && !$flag_type->is_requesteeble) {
- my $old_requestee = ($flag && $flag->requestee) ?
- $flag->requestee->login : '';
- my $new_requestee = join('', @$requestees);
- if ($new_requestee && $new_requestee ne $old_requestee) {
- ThrowCodeError('flag_requestee_disabled',
- { type => $flag_type });
- }
- }
- # Make sure the user didn't enter multiple requestees for a flag
- # that can't be requested from more than one person at a time.
- if ($status eq '?'
- && !$flag_type->is_multiplicable
- && scalar(@$requestees) > 1)
- {
- ThrowUserError('flag_not_multiplicable', { type => $flag_type });
- }
- # Make sure the requestees are authorized to access the bug
- # (and attachment, if this installation is using the "insider group"
- # feature and the attachment is marked private).
- if ($status eq '?' && $flag_type->is_requesteeble) {
- my $old_requestee = ($flag && $flag->requestee) ?
- $flag->requestee->login : '';
- my @legal_requestees;
- foreach my $login (@$requestees) {
- if ($login eq $old_requestee) {
- # This requestee was already set. Leave him alone.
- push(@legal_requestees, $login);
- next;
- }
- # We know the requestee exists because we ran
- # Bugzilla::User::match_field before getting here.
- my $requestee = new Bugzilla::User({ name => $login });
- # Throw an error if the user can't see the bug.
- # Note that if permissions on this bug are changed,
- # can_see_bug() will refer to old settings.
- if (!$requestee->can_see_bug($bug_id)) {
- next if $skip_requestee_on_error;
- ThrowUserError('flag_requestee_unauthorized',
- { flag_type => $flag_type,
- requestee => $requestee,
- bug_id => $bug_id,
- attach_id => $attach_id });
- }
- # Throw an error if the target is a private attachment and
- # the requestee isn't in the group of insiders who can see it.
- if ($attach_id
- && $private_attachment
- && Bugzilla->params->{'insidergroup'}
- && !$requestee->in_group(Bugzilla->params->{'insidergroup'}))
- {
- next if $skip_requestee_on_error;
- ThrowUserError('flag_requestee_unauthorized_attachment',
- { flag_type => $flag_type,
- requestee => $requestee,
- bug_id => $bug_id,
- attach_id => $attach_id });
- }
- # Throw an error if the user won't be allowed to set the flag.
- if (!$requestee->can_set_flag($flag_type)) {
- next if $skip_requestee_on_error;
- ThrowUserError('flag_requestee_needs_privs',
- {'requestee' => $requestee,
- 'flagtype' => $flag_type});
- }
- # This requestee can be set.
- push(@legal_requestees, $login);
- }
- # Update the requestee list for this flag.
- if (scalar(@legal_requestees) < scalar(@$requestees)) {
- my $field_name = 'requestee_type-' . $flag_type->id;
- Bugzilla->cgi->delete($field_name);
- Bugzilla->cgi->param(-name => $field_name, -value => \@legal_requestees);
- }
- }
- # Make sure the user is authorized to modify flags, see bug 180879
- # - The flag exists and is unchanged.
- return if ($flag && ($status eq $flag->status));
- # - User in the request_group can clear pending requests and set flags
- # and can rerequest set flags.
- return if (($status eq 'X' || $status eq '?')
- && $setter->can_request_flag($flag_type));
- # - User in the grant_group can set/clear flags, including "+" and "-".
- return if $setter->can_set_flag($flag_type);
- # - Any other flag modification is denied
- ThrowUserError('flag_update_denied',
- { name => $flag_type->name,
- status => $status,
- old_status => $flag ? $flag->status : 'X' });
- }
- sub snapshot {
- my ($class, $bug_id, $attach_id) = @_;
- my $flags = $class->match({ 'bug_id' => $bug_id,
- 'attach_id' => $attach_id });
- my @summaries;
- foreach my $flag (@$flags) {
- my $summary = $flag->type->name . $flag->status;
- $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee;
- push(@summaries, $summary);
- }
- return @summaries;
- }
- =pod
- =over
- =item C<process($bug, $attachment, $timestamp, $hr_vars)>
- Processes changes to flags.
- The bug and/or the attachment objects are the ones this flag is about,
- the timestamp is the date/time the bug was last touched (so that changes
- to the flag can be stamped with the same date/time).
- =back
- =cut
- sub process {
- my ($class, $bug, $attachment, $timestamp, $hr_vars) = @_;
- my $dbh = Bugzilla->dbh;
- my $cgi = Bugzilla->cgi;
- # Make sure the bug (and attachment, if given) exists and is accessible
- # to the current user. Moreover, if an attachment object is passed,
- # make sure it belongs to the given bug.
- return if ($bug->error || ($attachment && $bug->bug_id != $attachment->bug_id));
- my $bug_id = $bug->bug_id;
- my $attach_id = $attachment ? $attachment->id : undef;
- # Use the date/time we were given if possible (allowing calling code
- # to synchronize the comment's timestamp with those of other records).
- $timestamp ||= $dbh->selectrow_array('SELECT NOW()');
- # Take a snapshot of flags before any changes.
- my @old_summaries = $class->snapshot($bug_id, $attach_id);
- # Cancel pending requests if we are obsoleting an attachment.
- if ($attachment && $cgi->param('isobsolete')) {
- $class->CancelRequests($bug, $attachment);
- }
- # Create new flags and update existing flags.
- my $new_flags = FormToNewFlags($bug, $attachment, $cgi, $hr_vars);
- foreach my $flag (@$new_flags) { create($flag, $bug, $attachment, $timestamp) }
- modify($bug, $attachment, $cgi, $timestamp);
- # In case the bug's product/component has changed, clear flags that are
- # no longer valid.
- 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 $flags = Bugzilla::Flag->new_from_list($flag_ids);
- foreach my $flag (@$flags) {
- my $is_retargetted = retarget($flag, $bug);
- unless ($is_retargetted) {
- clear($flag, $bug, $flag->attachment);
- $hr_vars->{'message'} = 'flag_cleared';
- }
- }
- $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);
- $flags = Bugzilla::Flag->new_from_list($flag_ids);
- foreach my $flag (@$flags) {
- my $is_retargetted = retarget($flag, $bug);
- clear($flag, $bug, $flag->attachment) unless $is_retargetted;
- }
- # Take a snapshot of flags after changes.
- my @new_summaries = $class->snapshot($bug_id, $attach_id);
- update_activity($bug_id, $attach_id, $timestamp, \@old_summaries, \@new_summaries);
- Bugzilla::Hook::process('flag-end_of_update', { bug => $bug,
- timestamp => $timestamp,
- old_flags => \@old_summaries,
- new_flags => \@new_summaries,
- });
- }
- sub update_activity {
- my ($bug_id, $attach_id, $timestamp, $old_summaries, $new_summaries) = @_;
- my $dbh = Bugzilla->dbh;
- $old_summaries = join(", ", @$old_summaries);
- $new_summaries = join(", ", @$new_summaries);
- my ($removed, $added) = diff_strings($old_summaries, $new_summaries);
- if ($removed ne $added) {
- my $field_id = get_field_id('flagtypes.name');
- $dbh->do('INSERT INTO bugs_activity
- (bug_id, attach_id, who, bug_when, fieldid, removed, added)
- VALUES (?, ?, ?, ?, ?, ?, ?)',
- undef, ($bug_id, $attach_id, Bugzilla->user->id,
- $timestamp, $field_id, $removed, $added));
- $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
- undef, ($timestamp, $bug_id));
- }
- }
- =pod
- =over
- =item C<create($flag, $bug, $attachment, $timestamp)>
- Creates a flag record in the database.
- =back
- =cut
- sub create {
- my ($flag, $bug, $attachment, $timestamp) = @_;
- my $dbh = Bugzilla->dbh;
- my $attach_id = $attachment ? $attachment->id : undef;
- my $requestee_id;
- # Be careful! At this point, $flag is *NOT* yet an object!
- $requestee_id = $flag->{'requestee'}->id if $flag->{'requestee'};
- $dbh->do('INSERT INTO flags (type_id, bug_id, attach_id, requestee_id,
- setter_id, status, creation_date, modification_date)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
- undef, ($flag->{'type'}->id, $bug->bug_id,
- $attach_id, $requestee_id, $flag->{'setter'}->id,
- $flag->{'status'}, $timestamp, $timestamp));
- # Now that the new flag has been added to the DB, create a real flag object.
- # This is required to call notify() correctly.
- my $flag_id = $dbh->bz_last_key('flags', 'id');
- $flag = new Bugzilla::Flag($flag_id);
- # Send an email notifying the relevant parties about the flag creation.
- if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
- $flag->{'addressee'} = $flag->requestee;
- }
- notify($flag, $bug, $attachment);
- # Return the new flag object.
- return $flag;
- }
- =pod
- =over
- =item C<modify($bug, $attachment, $cgi, $timestamp)>
- Modifies flags in the database when a user changes them.
- =back
- =cut
- sub modify {
- my ($bug, $attachment, $cgi, $timestamp) = @_;
- my $setter = Bugzilla->user;
- my $dbh = Bugzilla->dbh;
- # Extract a list of flags from the form data.
- my @ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
- # Loop over flags and update their record in the database if necessary.
- # Two kinds of changes can happen to a flag: it can be set to a different
- # state, and someone else can be asked to set it. We take care of both
- # those changes.
- my @flags;
- foreach my $id (@ids) {
- my $flag = new Bugzilla::Flag($id);
- # If the flag no longer exists, ignore it.
- next unless $flag;
- my $status = $cgi->param("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.
- my @requestees = $cgi->param("requestee-$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) {
- create({ type => $flag->type,
- setter => $setter,
- status => "?",
- requestee => new Bugzilla::User({ name => $login }) },
- $bug, $attachment, $timestamp);
- }
- }
- else {
- $requestee_email = trim($cgi->param("requestee-$id") || '');
- }
- # Ignore flags the user didn't change. There are two components here:
- # either the status changes (trivial) or the requestee changes.
- # Change of either field will cause full update of the flag.
- my $status_changed = ($status ne $flag->status);
- # Requestee is considered changed, if all of the following apply:
- # 1. Flag status is '?' (requested)
- # 2. Flag can have a requestee
- # 3. The requestee specified on the form is different from the
- # requestee specified in the db.
- my $old_requestee = $flag->requestee ? $flag->requestee->login : '';
- my $requestee_changed =
- ($status eq "?" &&
- $flag->type->is_requesteeble &&
- $old_requestee ne $requestee_email);
- next unless ($status_changed || $requestee_changed);
- # Since the status is validated, we know it's safe, but it's still
- # tainted, so we have to detaint it before using it in a query.
- trick_taint($status);
- if ($status eq '+' || $status eq '-') {
- $dbh->do('UPDATE flags
- SET setter_id = ?, requestee_id = NULL,
- status = ?, modification_date = ?
- WHERE id = ?',
- undef, ($setter->id, $status, $timestamp, $flag->id));
- # If the status of the flag was "?", we have to notify
- # the requester (if he wants to).
- my $requester;
- if ($flag->status eq '?') {
- $requester = $flag->setter;
- $flag->{'requester'} = $requester;
- }
- # Now update the flag object with its new values.
- $flag->{'setter'} = $setter;
- $flag->{'requestee'} = undef;
- $flag->{'requestee_id'} = undef;
- $flag->{'status'} = $status;
- # Send an email notifying the relevant parties about the fulfillment,
- # including the requester.
- if ($requester && $requester->wants_mail([EVT_REQUESTED_FLAG])) {
- $flag->{'addressee'} = $requester;
- }
- notify($flag, $bug, $attachment);
- }
- elsif ($status eq '?') {
- # If the one doing the change is the requestee, then this means he doesn't
- # want to reply to the request and he simply reassigns the request to
- # someone else. In this case, we keep the requester unaltered.
- my $new_setter = $setter;
- if ($flag->requestee && $flag->requestee->id == $setter->id) {
- $new_setter = $flag->setter;
- }
- # Get the requestee, if any.
- my $requestee_id;
- if ($requestee_email) {
- $requestee_id = login_to_id($requestee_email);
- $flag->{'requestee'} = new Bugzilla::User($requestee_id);
- $flag->{'requestee_id'} = $requestee_id;
- }
- else {
- # If the status didn't change but we only removed the
- # requestee, we have to clear the requestee field.
- $flag->{'requestee'} = undef;
- $flag->{'requestee_id'} = undef;
- }
- # Update the database with the changes.
- $dbh->do('UPDATE flags
- SET setter_id = ?, requestee_id = ?,
- status = ?, modification_date = ?
- WHERE id = ?',
- undef, ($new_setter->id, $requestee_id, $status,
- $timestamp, $flag->id));
- # Now update the flag object with its new values.
- $flag->{'setter'} = $new_setter;
- $flag->{'status'} = $status;
- # Send an email notifying the relevant parties about the request.
- if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
- $flag->{'addressee'} = $flag->requestee;
- }
- notify($flag, $bug, $attachment);
- }
- elsif ($status eq 'X') {
- clear($flag, $bug, $attachment);
- }
- push(@flags, $flag);
- }
- return \@flags;
- }
- =pod
- =over
- =item C<retarget($flag, $bug)>
- Change the type of the flag, if possible. The new flag type must have
- the same name as the current flag type, must exist in the product and
- component the bug is in, and the current settings of the flag must pass
- validation. If no such flag type can be found, the type remains unchanged.
- Retargetting flags is a good way to keep flags when moving bugs from one
- product where a flag type is available to another product where the flag
- type is unavailable, but another flag type having the same name exists.
- Most of the time, if they have the same name, this means that they have
- the same meaning, but with different settings.
- =back
- =cut
- sub retarget {
- my ($flag, $bug) = @_;
- my $dbh = Bugzilla->dbh;
- # We are looking for flagtypes having the same name as the flagtype
- # to which the current flag belongs, and being in the new product and
- # component of the bug.
- my $flagtypes = Bugzilla::FlagType::match(
- {'name' => $flag->name,
- 'target_type' => $flag->type->target_type,
- 'is_active' => 1,
- 'product_id' => $bug->product_id,
- 'component_id' => $bug->component_id});
- # If we found no flagtype, the flag will be deleted.
- return 0 unless scalar(@$flagtypes);
- # If we found at least one, change the type of the flag,
- # assuming the setter/requester is allowed to set/request flags
- # belonging to this flagtype.
- my $requestee = $flag->requestee ? [$flag->requestee->login] : [];
- my $is_private = ($flag->attachment) ? $flag->attachment->isprivate : 0;
- my $is_retargetted = 0;
- foreach my $flagtype (@$flagtypes) {
- # Get the number of flags of this type already set for this target.
- my $has_flags = __PACKAGE__->count(
- { 'type_id' => $flagtype->id,
- 'bug_id' => $bug->bug_id,
- 'attach_id' => $flag->attach_id });
- # Do not create a new flag of this type if this flag type is
- # not multiplicable and already has a flag set.
- next if (!$flagtype->is_multiplicable && $has_flags);
- # Check user privileges.
- my $error_mode_cache = Bugzilla->error_mode;
- Bugzilla->error_mode(ERROR_MODE_DIE);
- eval {
- _validate(undef, $flagtype, $flag->status, $flag->setter,
- $requestee, $is_private, $bug->bug_id, $flag->attach_id);
- };
- Bugzilla->error_mode($error_mode_cache);
- # If the validation failed, then we cannot use this flagtype.
- next if ($@);
- # Checks are successful, we can retarget the flag to this flagtype.
- $dbh->do('UPDATE flags SET type_id = ? WHERE id = ?',
- undef, ($flagtype->id, $flag->id));
- $is_retargetted = 1;
- last;
- }
- return $is_retargetted;
- }
- =pod
- =over
- =item C<clear($flag, $bug, $attachment)>
- Remove a flag from the DB.
- =back
- =cut
- sub clear {
- my ($flag, $bug, $attachment) = @_;
- my $dbh = Bugzilla->dbh;
- $dbh->do('DELETE FROM flags WHERE id = ?', undef, $flag->id);
- # If we cancel a pending request, we have to notify the requester
- # (if he wants to).
- my $requester;
- if ($flag->status eq '?') {
- $requester = $flag->setter;
- $flag->{'requester'} = $requester;
- }
- # Now update the flag object to its new values. The last
- # requester/setter and requestee are kept untouched (for the
- # record). Else we could as well delete the flag completely.
- $flag->{'exists'} = 0;
- $flag->{'status'} = "X";
- if ($requester && $requester->wants_mail([EVT_REQUESTED_FLAG])) {
- $flag->{'addressee'} = $requester;
- }
- notify($flag, $bug, $attachment);
- }
- ######################################################################
- # Utility Functions
- ######################################################################
- =pod
- =over
- =item C<FormToNewFlags($bug, $attachment, $cgi, $hr_vars)>
- Checks whether or not there are new flags to create and returns an
- array of flag objects. This array is then passed to Flag::create().
- =back
- =cut
- sub FormToNewFlags {
- my ($bug, $attachment, $cgi, $hr_vars) = @_;
- my $dbh = Bugzilla->dbh;
- my $setter = Bugzilla->user;
-
- # Extract a list of flag type IDs from field names.
- my @type_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
- @type_ids = grep($cgi->param("flag_type-$_") ne 'X', @type_ids);
- return () unless scalar(@type_ids);
- # 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 $type_id (@type_ids) {
- # Checks if there are unexpected flags for the product/component.
- if (!scalar(grep { $_->id == $type_id } @$flag_types)) {
- $hr_vars->{'message'} = 'unexpected_flag_types';
- last;
- }
- }
- my @flags;
- 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 } @type_ids);
- # Get the number of flags of this type already set for this target.
- my $has_flags = __PACKAGE__->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) > 0) {
- foreach my $login (@logins) {
- push (@flags, { type => $flag_type ,
- setter => $setter ,
- status => $status ,
- requestee =>
- new Bugzilla::User({ name => $login }) });
- last unless $flag_type->is_multiplicable;
- }
- }
- else {
- push (@flags, { type => $flag_type ,
- setter => $setter ,
- status => $status });
- }
- }
- # Return the list of flags.
- return \@flags;
- }
- =pod
- =over
- =item C<notify($flag, $bug, $attachment)>
- Sends an email notification about a flag being created, fulfilled
- or deleted.
- =back
- =cut
- sub notify {
- my ($flag, $bug, $attachment) = @_;
- #if WEBKIT_CHANGES
- # Don't send a notification when the flag is in-rietveld,
- # since it isn't a user visible flag, and that mail is spammy.
- return if ($flag->type->name eq 'in-rietveld');
- #endif // WEBKIT_CHANGES
- # There is nobody to notify.
- return unless ($flag->{'addressee'} || $flag->type->cc_list);
- # 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(/[, ]+/, $flag->type->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 ($flag->{'addressee'} && $flag->{'addressee'}->email_enabled) {
- $recipients{$flag->{'addressee'}->email} = $flag->{'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) {
- my $default_user = new Bugzilla::User();
- $default_lang = $default_user->settings->{'lang'}->{'value'};
- }
- foreach my $to (keys %recipients) {
- # Add threadingmarker to allow flag notification emails to be the
- # threaded similar to normal bug change emails.
- my $user_id = $recipients{$to} ? $recipients{$to}->id : 0;
- my $threadingmarker = build_thread_marker($bug->id, $user_id);
-
- my $vars = { 'flag' => $flag,
- 'to' => $to,
- 'bug' => $bug,
- 'attachment' => $attachment,
- 'threadingmarker' => $threadingmarker };
- my $lang = $recipients{$to} ?
- $recipients{$to}->settings->{'lang'}->{'value'} : $default_lang;
- my $template = Bugzilla->template_inner($lang);
- my $message;
- $template->process("request/email.txt.tmpl", $vars, \$message)
- || ThrowTemplateError($template->error());
- Bugzilla->template_inner("");
- MessageToMTA($message);
- }
- }
- # Cancel all request flags from the attachment being obsoleted.
- sub CancelRequests {
- my ($class, $bug, $attachment, $timestamp) = @_;
- my $dbh = Bugzilla->dbh;
- my $request_ids =
- $dbh->selectcol_arrayref("SELECT flags.id
- FROM flags
- LEFT JOIN attachments ON flags.attach_id = attachments.attach_id
- WHERE flags.attach_id = ?
- AND flags.status = '?'
- AND attachments.isobsolete = 0",
- undef, $attachment->id);
- return if (!scalar(@$request_ids));
- # Take a snapshot of flags before any changes.
- my @old_summaries = $class->snapshot($bug->bug_id, $attachment->id)
- if ($timestamp);
- my $flags = Bugzilla::Flag->new_from_list($request_ids);
- foreach my $flag (@$flags) { clear($flag, $bug, $attachment) }
- # If $timestamp is undefined, do not update the activity table
- return unless ($timestamp);
- # Take a snapshot of flags after any changes.
- my @new_summaries = $class->snapshot($bug->bug_id, $attachment->id);
- update_activity($bug->bug_id, $attachment->id, $timestamp,
- \@old_summaries, \@new_summaries);
- }
- =head1 SEE ALSO
- =over
- =item B<Bugzilla::FlagType>
- =back
- =head1 CONTRIBUTORS
- =over
- =item Myk Melez <myk@mozilla.org>
- =item Jouni Heikniemi <jouni@heikniemi.net>
- =item Kevin Benton <kevin.benton@amd.com>
- =item Frédéric Buclin <LpSolit@gmail.com>
- =back
- =cut
- 1;
|