# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.

package Bugzilla::Flag;

use 5.10.1;
use strict;
use warnings;

=head1 NAME

Bugzilla::Flag - A module to deal with Bugzilla flag values.

=head1 SYNOPSIS

Flag.pm provides an interface to flags as stored in Bugzilla.
See below for more information.

=head1 NOTES

=over

=item *

Import relevant functions from that script.

=item *

Use of private functions / variables outside this module may lead to
unexpected results after an upgrade.  Please avoid using private
functions in other files/modules.  Private functions are functions
whose names start with _ or a re specifically noted as being private.

=back

=cut

use Scalar::Util qw(blessed);
use Storable qw(dclone);

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

use base qw(Bugzilla::Object Exporter);
@Bugzilla::Flag::EXPORT = qw(SKIP_REQUESTEE_ON_ERROR);

###############################
####    Initialization     ####
###############################

use constant DB_TABLE   => 'flags';
use constant LIST_ORDER => 'id';

# Flags are tracked in bugs_activity.
use constant AUDIT_CREATES => 0;
use constant AUDIT_UPDATES => 0;
use constant AUDIT_REMOVES => 0;

use constant SKIP_REQUESTEE_ON_ERROR => 1;

our $disable_flagmail = 0;

sub DB_COLUMNS {
  my $dbh = Bugzilla->dbh;
  return qw(
    id
    type_id
    bug_id
    attach_id
    requestee_id
    setter_id
    status),
    $dbh->sql_date_format('creation_date', '%Y-%m-%d %H:%i:%s')
    . ' AS creation_date',
    $dbh->sql_date_format('modification_date', '%Y-%m-%d %H:%i:%s')
    . ' AS modification_date';
}

use constant UPDATE_COLUMNS => qw(
  requestee_id
  setter_id
  status
  type_id
);

use constant VALIDATORS => {};

use constant UPDATE_VALIDATORS =>
  {setter => \&_check_setter, status => \&_check_status,};

###############################
####      Accessors      ######
###############################

=head2 METHODS

=over

=item C<id>

Returns the ID of the flag.

=item C<name>

Returns the name of the flagtype the flag belongs to.

=item C<bug_id>

Returns the ID of the bug this flag belongs to.

=item C<attach_id>

Returns the ID of the attachment this flag belongs to, if any.

=item C<status>

Returns the status '+', '-', '?' of the flag.

=item C<creation_date>

Returns the timestamp when the flag was created.

=item C<modification_date>

Returns the timestamp when the flag was last modified.

=back

=cut

sub id                { return $_[0]->{'id'}; }
sub name              { return $_[0]->type->name; }
sub type_id           { return $_[0]->{'type_id'}; }
sub bug_id            { return $_[0]->{'bug_id'}; }
sub attach_id         { return $_[0]->{'attach_id'}; }
sub status            { return $_[0]->{'status'}; }
sub setter_id         { return $_[0]->{'setter_id'}; }
sub requestee_id      { return $_[0]->{'requestee_id'}; }
sub creation_date     { return $_[0]->{'creation_date'}; }
sub modification_date { return $_[0]->{'modification_date'}; }

###############################
####       Methods         ####
###############################

=pod

=over

=item C<type>

Returns the type of the flag, as a Bugzilla::FlagType object.

=item C<setter>

Returns the user who set the flag, as a Bugzilla::User object.

=item C<requestee>

Returns the user who has been requested to set the flag, as a
Bugzilla::User object.

=item C<attachment>

Returns the attachment object the flag belongs to if the flag
is an attachment flag, else undefined.

=back

=cut

sub type {
  my $self = shift;

  return $self->{'type'}
    ||= new Bugzilla::FlagType($self->{'type_id'}, cache => 1);
}

sub setter {
  my $self = shift;

  return $self->{'setter'}
    ||= new Bugzilla::User({id => $self->{'setter_id'}, cache => 1});
}

sub requestee {
  my $self = shift;

  if (!defined $self->{'requestee'} && $self->{'requestee_id'}) {
    $self->{'requestee'}
      = new Bugzilla::User({id => $self->{'requestee_id'}, cache => 1});
  }
  return $self->{'requestee'};
}

sub attachment {
  my $self = shift;
  return undef unless $self->attach_id;

  require Bugzilla::Attachment;
  return $self->{'attachment'}
    ||= new Bugzilla::Attachment({id => $self->attach_id, cache => 1});
}

sub bug {
  my $self = shift;

  require Bugzilla::Bug;
  return $self->{'bug'} ||= new Bugzilla::Bug({id => $self->bug_id, cache => 1});
}

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

=pod

=over

=item C<match($criteria)>

Queries the database for flags matching the given criteria
(specified as a hash of field names and their matching values)
and returns an array of matching records.

=back

=cut

sub match {
  my $class = shift;
  my ($criteria) = @_;

  # If the caller specified only bug or attachment flags,
  # limit the query to those kinds of flags.
  if (my $type = delete $criteria->{'target_type'}) {
    if ($type eq 'bug') {
      $criteria->{'attach_id'} = IS_NULL;
    }
    elsif (!defined $criteria->{'attach_id'}) {
      $criteria->{'attach_id'} = NOT_NULL;
    }
  }

  # Flag->snapshot() calls Flag->match() with bug_id and attach_id
  # as hash keys, even if attach_id is undefined.
  if (exists $criteria->{'attach_id'} && !defined $criteria->{'attach_id'}) {
    $criteria->{'attach_id'} = IS_NULL;
  }

  return $class->SUPER::match(@_);
}

=pod

=over

=item C<count($criteria)>

Queries the database for flags matching the given criteria
(specified as a hash of field names and their matching values)
and returns an array of matching records.

=back

=cut

sub count {
  my $class = shift;
  return scalar @{$class->match(@_)};
}

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

sub set_flag {
  my ($class, $obj, $params) = @_;

  my ($bug, $attachment, $obj_flag, $requestee_changed);
  if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) {
    $attachment = $obj;
    $bug        = $attachment->bug;
  }
  elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) {
    $bug = $obj;
  }
  else {
    ThrowCodeError('flag_unexpected_object', {'caller' => ref $obj});
  }

  # Make sure the user can change flags
  my $privs;
  $bug->check_can_change_field('flagtypes.name', 0, 1, \$privs)
    || ThrowUserError('illegal_change',
    {field => 'flagtypes.name', privs => $privs});

  # Update (or delete) an existing flag.
  if ($params->{id}) {
    my $flag = $class->check({id => $params->{id}});

    # Security check: make sure the flag belongs to the bug/attachment.
    # We don't check that the user editing the flag can see
    # the bug/attachment. That's the job of the caller.
    ($attachment && $flag->attach_id && $attachment->id == $flag->attach_id)
      || (!$attachment && !$flag->attach_id && $bug->id == $flag->bug_id)
      || ThrowCodeError('invalid_flag_association',
      {bug_id => $bug->id, attach_id => $attachment ? $attachment->id : undef});

    # Extract the current flag object from the object.
    my ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types};

    # If no flagtype can be found for this flag, this means the bug is being
    # moved into a product/component where the flag is no longer valid.
    # So either we can attach the flag to another flagtype having the same
    # name, or we remove the flag.
    if (!$obj_flagtype) {
      my $success = $flag->retarget($obj);
      return unless $success;

      ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types};
      push(@{$obj_flagtype->{flags}}, $flag);
    }
    ($obj_flag) = grep { $_->id == $flag->id } @{$obj_flagtype->{flags}};

    # If the flag has the correct type but cannot be found above, this means
    # the flag is going to be removed (e.g. because this is a pending request
    # and the attachment is being marked as obsolete).
    return unless $obj_flag;

    ($obj_flag, $requestee_changed)
      = $class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment);
  }

  # Create a new flag.
  elsif ($params->{type_id}) {

    # Don't bother validating types the user didn't touch.
    return if $params->{status} eq 'X';

    my $flagtype = Bugzilla::FlagType->check({id => $params->{type_id}});

    # Security check: make sure the flag type belongs to the bug/attachment.
    (      $attachment
        && $flagtype->target_type eq 'attachment'
        && scalar(grep { $_->id == $flagtype->id } @{$attachment->flag_types}))
      || (!$attachment
      && $flagtype->target_type eq 'bug'
      && scalar(grep { $_->id == $flagtype->id } @{$bug->flag_types}))
      || ThrowCodeError('invalid_flag_association',
      {bug_id => $bug->id, attach_id => $attachment ? $attachment->id : undef});

    # Make sure the flag type is active.
    $flagtype->is_active
      || ThrowCodeError('flag_type_inactive', {type => $flagtype->name});

    # Extract the current flagtype object from the object.
    my ($obj_flagtype) = grep { $_->id == $flagtype->id } @{$obj->flag_types};

    # We cannot create a new flag if there is already one and this
    # flag type is not multiplicable.
    if (!$flagtype->is_multiplicable) {
      if (scalar @{$obj_flagtype->{flags}}) {
        ThrowUserError('flag_type_not_multiplicable', {type => $flagtype});
      }
    }

    ($obj_flag, $requestee_changed)
      = $class->_validate(undef, $obj_flagtype, $params, $bug, $attachment);
  }
  else {
    ThrowCodeError('param_required',
      {function => $class . '->set_flag', param => 'id/type_id'});
  }

  if ( $obj_flag
    && $requestee_changed
    && $obj_flag->requestee_id
    && $obj_flag->requestee->setting('requestee_cc') eq 'on'
    && $bug->reporter->id != $obj_flag->requestee->id)
  {
    $bug->add_cc($obj_flag->requestee);
  }
}

sub _validate {
  my ($class, $flag, $flag_type, $params, $bug, $attachment) = @_;

  # If it's a new flag, let's create it now.
  my $obj_flag = $flag || bless(
    {
      type_id   => $flag_type->id,
      status    => '',
      bug_id    => $bug->id,
      attach_id => $attachment ? $attachment->id : undef
    },
    $class
  );

  my $old_status       = $obj_flag->status;
  my $old_requestee_id = $obj_flag->requestee_id;

  $obj_flag->_set_status($params->{status});
  $obj_flag->_set_requestee($params->{requestee}, $bug, $attachment,
    $params->{skip_roe});

  # The requestee ID can be undefined.
  my $requestee_changed
    = ($obj_flag->requestee_id || 0) != ($old_requestee_id || 0);

  # The setter field MUST NOT be updated if neither the status
  # nor the requestee fields changed.
  if (($obj_flag->status ne $old_status) || $requestee_changed) {
    $obj_flag->_set_setter($params->{setter});
  }

  # If the flag is deleted, remove it from the list.
  if ($obj_flag->status eq 'X') {
    @{$flag_type->{flags}}
      = grep { $_->id != $obj_flag->id } @{$flag_type->{flags}};
    return;
  }

  # Add the newly created flag to the list.
  elsif (!$obj_flag->id) {
    push(@{$flag_type->{flags}}, $obj_flag);
  }
  return wantarray ? ($obj_flag, $requestee_changed) : $obj_flag;
}

=pod

=over

=item C<create($flag, $timestamp)>

Creates a flag record in the database.

=back

=cut

sub create {
  my ($class, $flag, $timestamp) = @_;
  $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');

  my $params = {};
  my @columns = grep { $_ ne 'id' } $class->_get_db_columns;

  # Some columns use date formatting so use alias instead
  @columns = map { /\s+AS\s+(.*)$/ ? $1 : $_ } @columns;

  $params->{$_} = $flag->{$_} foreach @columns;

  $params->{creation_date} = $params->{modification_date} = $timestamp;

  $flag = $class->SUPER::create($params);
  return $flag;
}

sub update {
  my $self      = shift;
  my $dbh       = Bugzilla->dbh;
  my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');

  my $changes = $self->SUPER::update(@_);

  if (scalar(keys %$changes)) {
    $dbh->do('UPDATE flags SET modification_date = ? WHERE id = ?',
      undef, ($timestamp, $self->id));
    $self->{'modification_date'}
      = format_time($timestamp, '%Y-%m-%d %T', Bugzilla->local_timezone);
    Bugzilla->memcached->clear({table => 'flags', id => $self->id});
  }

  # BMO - provide a hook which passes the flag object
  Bugzilla::Hook::process('flag_updated',
    {flag => $self, changes => $changes, timestamp => $timestamp});

  return $changes;
}

sub snapshot {
  my ($class, $flags) = @_;

  my @summaries;
  foreach my $flag (@$flags) {
    my $summary = $flag->setter->nick . ':' . $flag->type->name . $flag->status;
    $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee;
    push(@summaries, $summary);
  }
  return @summaries;
}

sub update_activity {
  my ($class, $old_summaries, $new_summaries) = @_;

  my ($removed, $added) = diff_arrays($old_summaries, $new_summaries);
  if (scalar @$removed || scalar @$added) {

    # Remove flag requester/setter information
    foreach (@$removed, @$added) {s/^[^:]+://}

    $removed = join(", ", @$removed);
    $added   = join(", ", @$added);
    return ($removed, $added);
  }
  return ();
}

sub update_flags {
  my ($class, $self, $old_self, $timestamp) = @_;

  my @old_summaries = $class->snapshot($old_self->flags);
  my %old_flags = map { $_->id => $_ } @{$old_self->flags};

  foreach my $new_flag (@{$self->flags}) {
    if (!$new_flag->id) {

      # This is a new flag.
      my $flag = $class->create($new_flag, $timestamp);
      $new_flag->{id}                = $flag->id;
      $new_flag->{creation_date}     = format_time($timestamp, '%Y-%m-%d %H:%i:%s');
      $new_flag->{modification_date} = format_time($timestamp, '%Y-%m-%d %H:%i:%s');
      $class->notify($new_flag, undef, $self, $timestamp);
    }
    else {
      my $changes = $new_flag->update($timestamp);
      if (scalar(keys %$changes)) {
        $class->notify($new_flag, $old_flags{$new_flag->id}, $self, $timestamp);
      }
      delete $old_flags{$new_flag->id};
    }
  }

  # These flags have been deleted.
  foreach my $old_flag (values %old_flags) {
    $class->notify(undef, $old_flag, $self, $timestamp);

    # BMO - provide a hook which passes the timestamp,
    # because that isn't passed to remove_from_db().
    Bugzilla::Hook::process('flag_deleted',
      {flag => $old_flag, timestamp => $timestamp});
    $old_flag->remove_from_db();
  }

  # If the bug has been moved into another product or component,
  # we must also take care of attachment flags which are no longer valid,
  # as well as all bug flags which haven't been forgotten above.
  if ($self->isa('Bugzilla::Bug')
    && ($self->{_old_product_name} || $self->{_old_component_name}))
  {
    my @removed = $class->force_cleanup($self);
    push(@old_summaries, @removed);
  }

  my @new_summaries = $class->snapshot($self->flags);
  my @changes = $class->update_activity(\@old_summaries, \@new_summaries);

  Bugzilla::Hook::process(
    'flag_end_of_update',
    {
      object    => $self,
      timestamp => $timestamp,
      old_flags => \@old_summaries,
      new_flags => \@new_summaries,
    }
  );
  return @changes;
}

sub retarget {
  my ($self, $obj) = @_;

  my @flagtypes = grep { $_->name eq $self->type->name } @{$obj->flag_types};

  my $success = 0;
  foreach my $flagtype (@flagtypes) {
    next if !$flagtype->is_active;
    next if (!$flagtype->is_multiplicable && scalar @{$flagtype->{flags}});
    next
      unless (($self->status eq '?' && $self->setter->can_request_flag($flagtype))
      || $self->setter->can_set_flag($flagtype));

    $self->{type_id} = $flagtype->id;
    delete $self->{type};
    $success = 1;
    last;
  }
  return $success;
}

# In case the bug's product/component has changed, clear flags that are
# no longer valid.
sub force_cleanup {
  my ($class, $bug) = @_;
  my $dbh = Bugzilla->dbh;

  my $flag_ids = $dbh->selectcol_arrayref(
    'SELECT DISTINCT flags.id
           FROM flags
          INNER JOIN bugs
                ON flags.bug_id = bugs.bug_id
           LEFT JOIN flaginclusions AS i
                ON flags.type_id = i.type_id
                AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
                AND (bugs.component_id = i.component_id OR i.component_id IS NULL)
          WHERE bugs.bug_id = ? AND i.type_id IS NULL', undef, $bug->id
  );

  my @removed = $class->force_retarget($flag_ids, $bug);

  $flag_ids = $dbh->selectcol_arrayref(
    'SELECT DISTINCT flags.id
           FROM flags, bugs, flagexclusions e
          WHERE bugs.bug_id = ?
                AND flags.bug_id = bugs.bug_id
                AND flags.type_id = e.type_id
                AND (bugs.product_id = e.product_id OR e.product_id IS NULL)
                AND (bugs.component_id = e.component_id OR e.component_id IS NULL)',
    undef, $bug->id
  );

  push(@removed, $class->force_retarget($flag_ids, $bug));
  return @removed;
}

sub force_retarget {
  my ($class, $flag_ids, $bug) = @_;
  my $dbh = Bugzilla->dbh;

  my $flags = $class->new_from_list($flag_ids);
  my @removed;
  foreach my $flag (@$flags) {

    # $bug is undefined when e.g. editing inclusion and exclusion lists.
    my $obj = $flag->attachment || $bug || $flag->bug;
    my $is_retargeted = $flag->retarget($obj);
    if ($is_retargeted) {
      $dbh->do('UPDATE flags SET type_id = ? WHERE id = ?',
        undef, ($flag->type_id, $flag->id));
      Bugzilla->memcached->clear({table => 'flags', id => $flag->id});
    }
    else {
      # Track deleted attachment flags.
      push(@removed, $class->snapshot([$flag])) if $flag->attach_id;
      $class->notify(undef, $flag, $bug || $flag->bug);

      # BMO - provide a hook which passes the timestamp,
      # because that isn't passed to remove_from_db().
      my ($timestamp) = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
      Bugzilla::Hook::process('flag_deleted',
        {flag => $flag, timestamp => $timestamp});
      $flag->remove_from_db();
    }
  }
  return @removed;
}

###############################
####      Validators     ######
###############################

sub _set_requestee {
  my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_;

  $self->{requestee} = $self->_check_requestee($requestee, $bug, $attachment,
    $skip_requestee_on_error);

  $self->{requestee_id} = $self->{requestee} ? $self->{requestee}->id : undef;
}

sub _set_setter {
  my ($self, $setter) = @_;

  $self->set('setter', $setter);
  $self->{setter_id} = $self->setter->id;
}

sub _set_status {
  my ($self, $status) = @_;

  # Store the old flag status. It's needed by _check_setter().
  $self->{_old_status} = $self->status;
  $self->set('status', $status);
}

sub _check_requestee {
  my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_;

  # If the flag status is not "?", then no requestee can be defined.
  return undef if ($self->status ne '?');

  # Store this value before updating the flag object.
  my $old_requestee = $self->requestee ? $self->requestee->login : '';

  if ($self->status eq '?' && $requestee) {
    $requestee = Bugzilla::User->check($requestee);
  }
  else {
    undef $requestee;
  }

  if ($requestee && $requestee->login ne $old_requestee) {

    # Make sure the user didn't specify a requestee unless the flag
    # is specifically requestable. For existing flags, if the requestee
    # was set before the flag became specifically unrequestable, the
    # user can either remove them or leave them alone.
    ThrowCodeError('flag_type_requestee_disabled', {type => $self->type})
      if !$self->type->is_requesteeble;

    # BMO customization:
    # You can't ask a disabled account, as they don't have the ability to
    # set the flag.
    ThrowUserError('flag_requestee_disabled', {requestee => $requestee})
      if !$requestee->is_enabled;

    # Make sure the requestee can see the bug.
    # Note that can_see_bug() will query the DB, so if the bug
    # is being added/removed from some groups and these changes
    # haven't been committed to the DB yet, they won't be taken
    # into account here. In this case, old group restrictions matter.
    # However, if the user has just been changed to the assignee,
    # qa_contact, or added to the cc list of the bug and the bug
    # is cclist_accessible, the requestee is allowed.
    if (
      !$requestee->can_see_bug($self->bug_id)
      && ( !$bug->cclist_accessible
        || !grep($_->id == $requestee->id, @{$bug->cc_users})
        && $requestee->id != $bug->assigned_to->id
        && (!$bug->qa_contact || $requestee->id != $bug->qa_contact->id))
      )
    {
      if ($skip_requestee_on_error) {
        undef $requestee;
      }
      else {
        ThrowUserError(
          'flag_requestee_unauthorized',
          {
            flag_type => $self->type,
            requestee => $requestee,
            bug_id    => $self->bug_id,
            attach_id => $self->attach_id
          }
        );
      }
    }

    # Make sure the requestee can see the private attachment.
    elsif ($self->attach_id && $attachment->isprivate && !$requestee->is_insider) {
      if ($skip_requestee_on_error) {
        undef $requestee;
      }
      else {
        ThrowUserError(
          'flag_requestee_unauthorized_attachment',
          {
            flag_type => $self->type,
            requestee => $requestee,
            bug_id    => $self->bug_id,
            attach_id => $self->attach_id
          }
        );
      }
    }

    # Make sure the user is allowed to set the flag.
    elsif (!$requestee->can_set_flag($self->type)) {
      if ($skip_requestee_on_error) {
        undef $requestee;
      }
      else {
        ThrowUserError('flag_requestee_needs_privs',
          {'requestee' => $requestee, 'flagtype' => $self->type});
      }
    }
  }
  return $requestee;
}

sub _check_setter {
  my ($self, $setter) = @_;

  # By default, the currently logged in user is the setter.
  $setter ||= Bugzilla->user;
  (blessed($setter) && $setter->isa('Bugzilla::User') && $setter->id)
    || ThrowCodeError('invalid_user');

  # set_status() has already been called. So this refers
  # to the new flag status.
  my $status = $self->status;

  ThrowUserError(
    'flag_update_denied',
    {
      name       => $self->type->name,
      status     => $status,
      old_status => $self->{_old_status}
    }
    )
    unless $setter->can_change_flag($self->type, $self->{_old_status} || 'X',
    $status);

  # If the request is being retargeted, we don't update
  # the setter, so that the setter gets the notification.
  if ($status eq '?' && $self->{_old_status} eq '?') {
    return $self->setter;
  }
  return $setter;
}

sub _check_status {
  my ($self, $status) = @_;

  # - Make sure the status is valid.
  # - Make sure the user didn't request the flag unless it's requestable.
  #   If the flag existed and was requested before it became unrequestable,
  #   leave it as is.
  if (!grep($status eq $_, qw(X + - ?))
    || ($status eq '?' && $self->status ne '?' && !$self->type->is_requestable))
  {
    ThrowUserError('flag_status_invalid', {id => $self->id, status => $status});
  }
  return $status;
}

######################################################################
# Utility Functions
######################################################################

=pod

=over

=item C<extract_flags_from_cgi($bug, $attachment, $hr_vars)>

Checks whether or not there are new flags to create and returns an
array of hashes. This array is then passed to Flag::create().

=back

=cut

sub extract_flags_from_cgi {
  my ($class, $bug, $attachment, $vars, $skip) = @_;
  my $cgi = Bugzilla->cgi;

  my $match_status
    = Bugzilla::User::match_field(
    {'^requestee(_type)?-(\d+)$' => {'type' => 'multi'},},
    undef, $skip);

  $vars->{'match_field'} = 'requestee';
  if ($match_status == USER_MATCH_FAILED) {
    $vars->{'message'} = 'user_match_failed';
  }
  elsif ($match_status == USER_MATCH_MULTIPLE) {
    $vars->{'message'} = 'user_match_multiple';
  }

  # Extract a list of flag type IDs from field names.
  my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());

  # Extract a list of existing flag IDs.
  my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());

  return ([], []) unless (scalar(@flagtype_ids) || scalar(@flag_ids));

  my (@new_flags, @flags);
  foreach my $flag_id (@flag_ids) {
    my $flag = $class->new($flag_id);

    # If the flag no longer exists, ignore it.
    next unless $flag;

    my $status = $cgi->param("flag-$flag_id");

    # If the user entered more than one name into the requestee field
    # (i.e. they want more than one person to set the flag) we can reuse
    # the existing flag for the first person (who may well be the existing
    # requestee), but we have to create new flags for each additional requestee.
    my @requestees = $cgi->param("requestee-$flag_id");
    my $requestee_email;
    if ($status eq "?" && scalar(@requestees) > 1 && $flag->type->is_multiplicable)
    {
      # The first person, for which we'll reuse the existing flag.
      $requestee_email = shift(@requestees);

      # Create new flags like the existing one for each additional person.
      foreach my $login (@requestees) {
        push(
          @new_flags,
          {
            type_id   => $flag->type_id,
            status    => "?",
            requestee => $login,
            skip_roe  => $skip
          }
        );
      }
    }
    elsif ($status eq "?" && scalar(@requestees)) {

      # If there are several requestees and the flag type is not multiplicable,
      # this will fail. But that's the job of the validator to complain. All
      # we do here is to extract and convert data from the CGI.
      $requestee_email = trim($cgi->param("requestee-$flag_id") || '');
    }

    push(
      @flags,
      {
        id        => $flag_id,
        status    => $status,
        requestee => $requestee_email,
        skip_roe  => $skip
      }
    );
  }

  # Get a list of active flag types available for this product/component.
  my $flag_types = Bugzilla::FlagType::match({
    'product_id'   => $bug->{'product_id'},
    'component_id' => $bug->{'component_id'},
    'is_active'    => 1
  });

  foreach my $flagtype_id (@flagtype_ids) {

    # Checks if there are unexpected flags for the product/component.
    if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) {
      $vars->{'message'} = 'unexpected_flag_types';
      last;
    }
  }

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

    # Bug flags are only valid for bugs, and attachment flags are
    # only valid for attachments. So don't mix both.
    next unless ($flag_type->target_type eq 'bug' xor $attachment);

    # We are only interested in flags the user tries to create.
    next unless scalar(grep { $_ == $type_id } @flagtype_ids);

    # Get the current flags of this type already set for this target.
    my $current_flags = $class->match({
      'type_id'     => $type_id,
      'target_type' => $attachment ? 'attachment' : 'bug',
      'bug_id'      => $bug->bug_id,
      'attach_id'   => $attachment ? $attachment->id : undef
    });

    my $status = $cgi->param("flag_type-$type_id");

    my @logins = $cgi->param("requestee_type-$type_id");
    if ($status eq "?" && scalar(@logins)) {
      foreach my $login (@logins) {
        push(
          @new_flags,
          {
            type_id   => $type_id,
            status    => $status,
            requestee => $login,
            skip_roe  => $skip
          }
        );
        last unless $flag_type->is_multiplicable;
      }
    }
    elsif (!$flag_type->is_multiplicable && scalar @$current_flags) {
      # Update or delete the current flag if the type is not multiplicable.
      push(@new_flags, {id => @$current_flags[0]->id, status => $status});
    }
    else {
      push(@new_flags, {type_id => $type_id, status => $status});
    }
  }

  # Return the list of flags to update and/or to create.
  return (\@flags, \@new_flags);
}

=pod

=over

=item C<notify($flag, $old_flag, $object, $timestamp)>

Sends an email notification about a flag being created, fulfilled
or deleted.

=back

=cut

sub notify {
  my ($class, $flag, $old_flag, $obj, $timestamp) = @_;

  if ($disable_flagmail) {
    return;
  }

  my ($bug, $attachment);
  if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) {
    $attachment = $obj;
    $bug        = $attachment->bug;
  }
  elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) {
    $bug = $obj;
  }
  else {
    # Not a good time to throw an error.
    return;
  }

  my $addressee;

  # If the flag is set to '?', maybe the requestee wants a notification.
  if ( $flag
    && $flag->requestee_id
    && (!$old_flag || ($old_flag->requestee_id || 0) != $flag->requestee_id))
  {
    if ($flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
      $addressee = $flag->requestee;
    }
  }
  elsif ($old_flag
    && $old_flag->status eq '?'
    && (!$flag || $flag->status ne '?'))
  {
    if ($old_flag->setter->wants_mail([EVT_REQUESTED_FLAG])) {
      $addressee = $old_flag->setter;
    }
  }

  my $cc_list = $flag ? $flag->type->cc_list : $old_flag->type->cc_list;
  $cc_list //= '';

  # Is there someone to notify?
  return unless ($addressee || $cc_list);

  # The email client will display the Date: header in the desired timezone,
  # so we can always use UTC here.
  $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
  $timestamp = format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC');

  # If the target bug is restricted to one or more groups, then we need
  # to make sure we don't send email about it to unauthorized users
  # on the request type's CC: list, so we have to trawl the list for users
  # not in those groups or email addresses that don't have an account.
  my @bug_in_groups = grep { $_->{'ison'} || $_->{'mandatory'} } @{$bug->groups};
  my $attachment_is_private = $attachment ? $attachment->isprivate : undef;

  my %recipients;
  foreach my $cc (split(/[, ]+/, $cc_list)) {
    my $ccuser = new Bugzilla::User({name => $cc, cache => 1});
    next
      if (scalar(@bug_in_groups)
      && (!$ccuser || !$ccuser->can_see_bug($bug->bug_id)));
    next if $attachment_is_private && (!$ccuser || !$ccuser->is_insider);

    # Prevent duplicated entries due to case sensitivity.
    $cc = $ccuser ? $ccuser->email : $cc;
    $recipients{$cc} = $ccuser;
  }

  # Only notify if the addressee is allowed to receive the email.
  if ($addressee && $addressee->email_enabled) {
    $recipients{$addressee->email} = $addressee;
  }

  # Process and send notification for each recipient.
  # If there are users in the CC list who don't have an account,
  # use the default language for email notifications.
  my $default_lang;
  if (grep { !$_ } values %recipients) {
    $default_lang = Bugzilla::User->new()->setting('lang');
  }

  # Get comments on the bug
  my $all_comments = $bug->comments({after => $bug->lastdiffed});
  @$all_comments = grep { $_->type || $_->body =~ /\S/ } @$all_comments;

  # Get public only comments
  my $public_comments = [grep { !$_->is_private } @$all_comments];

  foreach my $to (keys %recipients) {

    # Add threadingmarker to allow flag notification emails to be the
    # threaded similar to normal bug change emails.
    my $thread_user_id = $recipients{$to} ? $recipients{$to}->id : 0;

    # We only want to show private comments to users in the is_insider group
    my $comments = $recipients{$to}
      && $recipients{$to}->is_insider ? $all_comments : $public_comments;

    my $vars = {
      flag            => $flag,
      old_flag        => $old_flag,
      to              => $to,
      date            => $timestamp,
      bug             => $bug,
      attachment      => $attachment,
      threadingmarker => build_thread_marker($bug->id, $thread_user_id),
      new_comments    => $comments,
    };

    my $lang = $recipients{$to} ? $recipients{$to}->setting('lang') : $default_lang;

    my $template = Bugzilla->template_inner($lang);
    my $message;
    $template->process("request/email.txt.tmpl", $vars, \$message)
      || ThrowTemplateError($template->error());

    MessageToMTA($message);
  }
}

# This is an internal function used by $bug->flag_types
# and $attachment->flag_types to collect data about available
# flag types and existing flags set on them. You should never
# call this function directly.
sub _flag_types {
  my ($class, $vars) = @_;

  my $target_type = $vars->{target_type};
  my $flags;

  # Retrieve all existing flags for this bug/attachment.
  if ($target_type eq 'bug') {
    my $bug_id = delete $vars->{bug_id};
    $flags = $class->match({target_type => 'bug', bug_id => $bug_id});
  }
  elsif ($target_type eq 'attachment') {
    my $attach_id = delete $vars->{attach_id};
    $flags = $class->match({attach_id => $attach_id});
  }
  else {
    ThrowCodeError('bad_arg',
      {argument => 'target_type', function => $class . '->_flag_types'});
  }

  # Get all available flag types for the given product and component.
  my $cache
    = Bugzilla->request_cache->{flag_types_per_component}->{$vars->{target_type}}
    ||= {};
  my $flag_data = $cache->{$vars->{component_id}}
    ||= Bugzilla::FlagType::match($vars);
  my $flag_types = dclone($flag_data);

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

  # Group existing flags per type, and skip those becoming invalid
  # (which can happen when a bug is being moved into a new product
  # or component).
  @$flags = grep { exists $flagtypes{$_->type_id} } @$flags;
  push(@{$flagtypes{$_->type_id}->{flags}}, $_) foreach @$flags;
  return $flag_types;
}

1;

__END__

=head1 B<Methods in need of POD>

=over

=item update_activity

=item setter_id

=item bug

=item requestee_id

=item DB_COLUMNS

=item set_flag

=item type_id

=item snapshot

=item update_flags

=item update

=back
