# -*- 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'} ||= new Bugzilla::Attachment($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->setter->nick . ':' . $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;

    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);
        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) = @_;

    # 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 $thread_user_id = $recipients{$to} ? $recipients{$to}->id : 0;
    
        my $vars = { 'flag'            => $flag,
                     'to'              => $to,
                     'bug'             => $bug,
                     'attachment'      => $attachment,
                     'threadingmarker' => build_thread_marker($bug->id, $thread_user_id) };

        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);
}

# 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 $vars = shift;

    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 = Bugzilla::Flag->match({target_type => 'bug', bug_id => $bug_id});
    }
    elsif ($target_type eq 'attachment') {
        my $attach_id = delete $vars->{attach_id};
        $flags = Bugzilla::Flag->match({attach_id => $attach_id});
    }
    else {
        ThrowCodeError('bad_arg', {argument => 'target_type',
                                   function => 'Bugzilla::Flag::_flag_types'});
    }

    # Get all available flag types for the given product and component.
    my $flag_types = Bugzilla::FlagType::match($vars);

    $_->{flags} = [] foreach @$flag_types;
    my %flagtypes = map { $_->id => $_ } @$flag_types;

    # Group existing flags per type.
    # Call the internal 'type_id' variable instead of the method
    # to not create a flagtype object.
    push(@{$flagtypes{$_->{type_id}}->{flags}}, $_) foreach @$flags;

    return [sort {$a->sortkey <=> $b->sortkey || $a->name cmp $b->name} values %flagtypes];
}

=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;
