# -*- 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>

=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

######################################################################
# Module Initialization
######################################################################

# Make it harder for us to do dangerous things in Perl.
use strict;

# This module implements bug and attachment flags.
package Bugzilla::Flag;

use Bugzilla::FlagType;
use Bugzilla::User;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Mailer;
use Bugzilla::Constants;
use Bugzilla::Field;

######################################################################
# Global Variables
######################################################################

use constant DB_COLUMNS => qw(
    flags.id
    flags.type_id
    flags.bug_id
    flags.attach_id
    flags.requestee_id
    flags.setter_id
    flags.status
);

my $columns = join(", ", DB_COLUMNS);

######################################################################
# Searching/Retrieving Flags
######################################################################

=head1 PUBLIC FUNCTIONS

=over

=item C<get($id)>

Retrieves and returns a flag from the database.

=back

=cut

sub get {
    my ($id) = @_;
    my $dbh = Bugzilla->dbh;

    my @flag = $dbh->selectrow_array("SELECT $columns FROM flags
                                      WHERE id = ?", undef, $id);

    return perlify_record(@flag);
}

=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 ($criteria) = @_;
    my $dbh = Bugzilla->dbh;

    my @criteria = sqlify_criteria($criteria);
    $criteria = join(' AND ', @criteria);

    my $flags = $dbh->selectall_arrayref("SELECT $columns FROM flags
                                          WHERE $criteria");

    my @flags;
    foreach my $flag (@$flags) {
        push(@flags, perlify_record(@$flag));
    }

    return \@flags;
}

=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 ($criteria) = @_;
    my $dbh = Bugzilla->dbh;

    my @criteria = sqlify_criteria($criteria);
    $criteria = join(' AND ', @criteria);

    my $count = $dbh->selectrow_array("SELECT COUNT(*) FROM flags WHERE $criteria");

    return $count;
}

######################################################################
# Creating and Modifying
######################################################################

=pod

=over

=item C<validate($cgi, $bug_id, $attach_id)>

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 ($cgi, $bug_id, $attach_id) = @_;

    my $user = Bugzilla->user;
    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-nnn", where "nnn" is the ID,
    # and returning just the ID portion of matching field names.
    my @ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());

    return unless scalar(@ids);
    
    # No flag reference should exist when changing several bugs at once.
    ThrowCodeError("flags_not_available", { type => 'b' }) unless $bug_id;

    # 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 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 id IN (" . join(',', @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 });
    }

    foreach my $id (@ids) {
        my $status = $cgi->param("flag-$id");
        my @requestees = $cgi->param("requestee-$id");
        
        # Make sure the flag exists.
        my $flag = get($id);
        $flag || ThrowCodeError("flag_nonexistent", { id => $id });

        # 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 was requested before it became unrequestable, leave it
        # as is.
        if ($status eq '?'
            && $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. 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->{'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->{'requestee'} ? $flag->{'requestee'}->login : '';
            foreach my $login (@requestees) {
                next if $login eq $old_requestee;

                # We know the requestee exists because we ran
                # Bugzilla::User::match_field before getting here.
                my $requestee = Bugzilla::User->new_from_login($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)) {
                    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
                    && $cgi->param('isprivate')
                    && Bugzilla->params->{"insidergroup"}
                    && !$requestee->in_group(Bugzilla->params->{"insidergroup"}))
                {
                    ThrowUserError("flag_requestee_unauthorized_attachment",
                                   { flag_type  => $flag->{'type'},
                                     requestee  => $requestee,
                                     bug_id     => $bug_id,
                                     attach_id  => $attach_id
                                   });
                }
            }
        }

        # Make sure the user is authorized to modify flags, see bug 180879
        # - The flag is unchanged
        next if ($status eq $flag->{status});

        # - User in the $request_gid group can clear pending requests and set flags
        #   and can rerequest set flags.
        next if (($status eq 'X' || $status eq '?')
                 && (!$flag->{type}->{request_gid}
                     || $user->in_group_id($flag->{type}->{request_gid})));

        # - User in the $grant_gid group can set/clear flags,
        #   including "+" and "-"
        next if (!$flag->{type}->{grant_gid}
                 || $user->in_group_id($flag->{type}->{grant_gid}));

        # - Any other flag modification is denied
        ThrowUserError("flag_update_denied",
                        { name       => $flag->{type}->{name},
                          status     => $status,
                          old_status => $flag->{status} });
    }
}

sub snapshot {
    my ($bug_id, $attach_id) = @_;

    my $flags = 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, $cgi)>

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), the cgi is the CGI
object used to obtain the flag fields that the user submitted.

=back

=cut

sub process {
    my ($bug, $attachment, $timestamp, $cgi) = @_;
    my $dbh = Bugzilla->dbh;

    # 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 = snapshot($bug_id, $attach_id);

    # Cancel pending requests if we are obsoleting an attachment.
    if ($attachment && $cgi->param('isobsolete')) {
        CancelRequests($bug, $attachment);
    }

    # Create new flags and update existing flags.
    my $new_flags = FormToNewFlags($bug, $attachment, $cgi);
    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 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);

    foreach my $flag_id (@$flag_ids) { clear($flag_id, $bug, $attachment) }

    $flag_ids = $dbh->selectcol_arrayref(
        "SELECT 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);

    foreach my $flag_id (@$flag_ids) { clear($flag_id, $bug, $attachment) }

    # Take a snapshot of flags after changes.
    my @new_summaries = snapshot($bug_id, $attach_id);

    update_activity($bug_id, $attach_id, $timestamp, \@old_summaries, \@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;
    $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));

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

=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 = get($id);

        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(login_to_id($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'};
            }
            # Now update the flag object with its new values.
            $flag->{'setter'} = $setter;
            $flag->{'requestee'} = 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 '?') {
            # 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);
            }
            else {
                # If the status didn't change but we only removed the
                # requestee, we have to clear the requestee field.
                $flag->{'requestee'} = undef;
            }

            # Update the database with the changes.
            $dbh->do('UPDATE flags
                         SET setter_id = ?, requestee_id = ?,
                             status = ?, modification_date = ?
                       WHERE id = ?',
                       undef, ($setter->id, $requestee_id, $status,
                               $timestamp, $flag->{'id'}));

            # Now update the flag object with its new values.
            $flag->{'setter'} = $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->{'id'}, $bug, $attachment);
        }

        push(@flags, $flag);
    }

    return \@flags;
}

=pod

=over

=item C<clear($id, $bug, $attachment)>

Remove a flag from the DB.

=back

=cut

sub clear {
    my ($id, $bug, $attachment) = @_;
    my $dbh = Bugzilla->dbh;

    my $flag = get($id);
    $dbh->do('DELETE FROM flags WHERE id = ?', undef, $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'};
    }

    # 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)>

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) = @_;
    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 target.
    my $flag_types = Bugzilla::FlagType::match(
        { 'target_type'  => $attachment ? 'attachment' : 'bug',
          'product_id'   => $bug->{'product_id'},
          'component_id' => $bug->{'component_id'},
          'is_active'    => 1 });

    my @flags;
    foreach my $flag_type (@$flag_types) {
        my $type_id = $flag_type->{'id'};

        # 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 = 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) {
                my $requestee = new Bugzilla::User(login_to_id($login));
                push (@flags, { type      => $flag_type ,
                                setter    => $setter , 
                                status    => $status ,
                                requestee => $requestee });
                last if !$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) = @_;

    my $template = Bugzilla->template;

    # There is nobody to notify.
    return unless ($flag->{'addressee'} || $flag->{'type'}->{'cc_list'});

    my $attachment_is_private = $attachment ? $attachment->isprivate : undef;

    # 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.
    if ($bug->groups || $attachment_is_private) {
        my @new_cc_list;
        foreach my $cc (split(/[, ]+/, $flag->{'type'}->{'cc_list'})) {
            my $ccuser = Bugzilla::User->new_from_login($cc) || next;

            next if ($bug->groups && !$ccuser->can_see_bug($bug->bug_id));
            next if $attachment_is_private
              && Bugzilla->params->{"insidergroup"}
              && !$ccuser->in_group(Bugzilla->params->{"insidergroup"});
            push(@new_cc_list, $cc);
        }
        $flag->{'type'}->{'cc_list'} = join(", ", @new_cc_list);
    }

    # If there is nobody left to notify, return.
    return unless ($flag->{'addressee'} || $flag->{'type'}->{'cc_list'});

    # Process and send notification for each recipient
    foreach my $to ($flag->{'addressee'} ? $flag->{'addressee'}->email : '',
                    split(/[, ]+/, $flag->{'type'}->{'cc_list'}))
    {
        next unless $to;
        my $vars = { 'flag'       => $flag,
                     'to'         => $to,
                     'bug'        => $bug,
                     'attachment' => $attachment};
        my $message;
        my $rv = $template->process("request/email.txt.tmpl", $vars, \$message);
        if (!$rv) {
            Bugzilla->cgi->header();
            ThrowTemplateError($template->error());
        }

        MessageToMTA($message);
    }
}

# Cancel all request flags from the attachment being obsoleted.
sub CancelRequests {
    my ($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 = snapshot($bug->bug_id, $attachment->id) if ($timestamp);
    foreach my $flag (@$request_ids) { 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 = snapshot($bug->bug_id, $attachment->id);
    update_activity($bug->bug_id, $attachment->id, $timestamp,
                    \@old_summaries, \@new_summaries);
}

######################################################################
# Private Functions
######################################################################

=begin private

=head1 PRIVATE FUNCTIONS

=over

=item C<sqlify_criteria($criteria)>

Converts a hash of criteria into a list of SQL criteria.

=back

=cut

sub sqlify_criteria {
    # a reference to a hash containing the criteria (field => value)
    my ($criteria) = @_;

    # the generated list of SQL criteria; "1=1" is a clever way of making sure
    # there's something in the list so calling code doesn't have to check list
    # size before building a WHERE clause out of it
    my @criteria = ("1=1");
    
    # If the caller specified only bug or attachment flags,
    # limit the query to those kinds of flags.
    if (defined($criteria->{'target_type'})) {
        if    ($criteria->{'target_type'} eq 'bug')        { push(@criteria, "attach_id IS NULL") }
        elsif ($criteria->{'target_type'} eq 'attachment') { push(@criteria, "attach_id IS NOT NULL") }
    }
    
    # Go through each criterion from the calling code and add it to the query.
    foreach my $field (keys %$criteria) {
        my $value = $criteria->{$field};
        next unless defined($value);
        if    ($field eq 'type_id')      { push(@criteria, "type_id      = $value") }
        elsif ($field eq 'bug_id')       { push(@criteria, "bug_id       = $value") }
        elsif ($field eq 'attach_id')    { push(@criteria, "attach_id    = $value") }
        elsif ($field eq 'requestee_id') { push(@criteria, "requestee_id = $value") }
        elsif ($field eq 'setter_id')    { push(@criteria, "setter_id    = $value") }
        elsif ($field eq 'status')       { push(@criteria, "status       = '$value'") }
    }
    
    return @criteria;
}

=pod

=over

=item C<perlify_record($id, $type_id, $bug_id, $attach_id, $requestee_id, $setter_id, $status)>

Converts a row from the database into a Perl record.

=back

=end private

=cut

sub perlify_record {
    my ($id, $type_id, $bug_id, $attach_id,
        $requestee_id, $setter_id, $status) = @_;

    return undef unless $id;

    my $flag =
      {
        id        => $id ,
        type      => Bugzilla::FlagType::get($type_id) ,
        requestee => $requestee_id ? new Bugzilla::User($requestee_id) : undef,
        setter    => new Bugzilla::User($setter_id) ,
        status    => $status , 
      };

    return $flag;
}

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