mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-06 09:05:45 +00:00
1163 lines
37 KiB
Perl
1163 lines
37 KiB
Perl
# -*- 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 *
|
|
|
|
Prior to calling routines in this module, it's assumed that you have
|
|
already done a C<require globals.pl>.
|
|
|
|
=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 usi8ng 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::Config;
|
|
use Bugzilla::Util;
|
|
use Bugzilla::Error;
|
|
use Bugzilla::Attachment;
|
|
use Bugzilla::BugMail;
|
|
use Bugzilla::Constants;
|
|
use Bugzilla::Field;
|
|
|
|
######################################################################
|
|
# Global Variables
|
|
######################################################################
|
|
|
|
# basic sets of columns and tables for getting flags from the database
|
|
|
|
=begin private
|
|
|
|
=head1 PRIVATE VARIABLES/CONSTANTS
|
|
|
|
=over
|
|
|
|
=item C<@base_columns>
|
|
|
|
basic sets of columns and tables for getting flag types from th
|
|
database. B<Used by get, match, sqlify_criteria and perlify_record>
|
|
|
|
=cut
|
|
|
|
my @base_columns =
|
|
("is_active", "id", "type_id", "bug_id", "attach_id", "requestee_id",
|
|
"setter_id", "status");
|
|
|
|
=pod
|
|
|
|
=item C<@base_tables>
|
|
|
|
Which database(s) is the data coming from?
|
|
|
|
Note: when adding tables to @base_tables, make sure to include the separator
|
|
(i.e. words like "LEFT OUTER JOIN") before the table name, since tables take
|
|
multiple separators based on the join type, and therefore it is not possible
|
|
to join them later using a single known separator.
|
|
B<Used by get, match, sqlify_criteria and perlify_record>
|
|
|
|
=back
|
|
|
|
=end private
|
|
|
|
=cut
|
|
|
|
my @base_tables = ("flags");
|
|
|
|
######################################################################
|
|
# Searching/Retrieving Flags
|
|
######################################################################
|
|
|
|
=head1 PUBLIC FUNCTIONS
|
|
|
|
=over
|
|
|
|
=item C<get($id)>
|
|
|
|
Retrieves and returns a flag from the database.
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
# !!! Implement a cache for this function!
|
|
sub get {
|
|
my ($id) = @_;
|
|
|
|
my $select_clause = "SELECT " . join(", ", @base_columns);
|
|
my $from_clause = "FROM " . join(" ", @base_tables);
|
|
|
|
# Execute the query, retrieve the result, and write it into a record.
|
|
&::PushGlobalSQLState();
|
|
&::SendSQL("$select_clause $from_clause WHERE flags.id = $id");
|
|
my $flag = perlify_record(&::FetchSQLData());
|
|
&::PopGlobalSQLState();
|
|
|
|
return $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 $select_clause = "SELECT " . join(", ", @base_columns);
|
|
my $from_clause = "FROM " . join(" ", @base_tables);
|
|
|
|
my @criteria = sqlify_criteria($criteria);
|
|
|
|
my $where_clause = "WHERE " . join(" AND ", @criteria);
|
|
|
|
# Execute the query, retrieve the results, and write them into records.
|
|
&::PushGlobalSQLState();
|
|
&::SendSQL("$select_clause $from_clause $where_clause");
|
|
my @flags;
|
|
while (&::MoreSQLData()) {
|
|
my $flag = perlify_record(&::FetchSQLData());
|
|
push(@flags, $flag);
|
|
}
|
|
&::PopGlobalSQLState();
|
|
|
|
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 @criteria = sqlify_criteria($criteria);
|
|
|
|
my $where_clause = "WHERE " . join(" AND ", @criteria);
|
|
|
|
# Execute the query, retrieve the result, and write it into a record.
|
|
&::PushGlobalSQLState();
|
|
&::SendSQL("SELECT COUNT(id) FROM flags $where_clause");
|
|
my $count = &::FetchOneColumn();
|
|
&::PopGlobalSQLState();
|
|
|
|
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 });
|
|
|
|
# Note that the deletedness of the flag (is_active or not) is not
|
|
# checked here; we do want to allow changes to deleted flags in
|
|
# certain cases. Flag::modify() will revive the modified flags.
|
|
# See bug 223878 for details.
|
|
|
|
# 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,
|
|
attachment => $flag->{target}->{attachment}
|
|
});
|
|
}
|
|
|
|
# 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 ($flag->{target}->{attachment}
|
|
&& $cgi->param('isprivate')
|
|
&& Param("insidergroup")
|
|
&& !$requestee->in_group(Param("insidergroup")))
|
|
{
|
|
ThrowUserError("flag_requestee_unauthorized_attachment",
|
|
{ flag_type => $flag->{'type'},
|
|
requestee => $requestee,
|
|
bug_id => $bug_id,
|
|
attachment => $flag->{target}->{attachment}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
# 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(&::GroupIdToName($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(&::GroupIdToName($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,
|
|
'is_active' => 1 });
|
|
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($target, $timestamp, $cgi)>
|
|
|
|
Processes changes to flags.
|
|
|
|
The target is the bug or attachment 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_id, $attach_id, $timestamp, $cgi) = @_;
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
my $target = get_target($bug_id, $attach_id);
|
|
# Make sure the target exists.
|
|
return unless $target->{'exists'};
|
|
|
|
# Use the date/time we were given if possible (allowing calling code
|
|
# to synchronize the comment's timestamp with those of other records).
|
|
$timestamp = ($timestamp ? &::SqlQuote($timestamp) : "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 ($attach_id && $cgi->param('isobsolete')) {
|
|
CancelRequests($bug_id, $attach_id);
|
|
}
|
|
|
|
# Create new flags and update existing flags.
|
|
my $new_flags = FormToNewFlags($target, $cgi);
|
|
foreach my $flag (@$new_flags) { create($flag, $timestamp) }
|
|
modify($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 flags.is_active = 1
|
|
AND i.type_id IS NULL",
|
|
undef, $bug_id);
|
|
|
|
foreach my $flag_id (@$flag_ids) { clear($flag_id) }
|
|
|
|
$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 flags.is_active = 1
|
|
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) }
|
|
|
|
# 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;
|
|
my $user_id = Bugzilla->user->id;
|
|
|
|
$attach_id ||= 'NULL';
|
|
$old_summaries = join(", ", @$old_summaries);
|
|
$new_summaries = join(", ", @$new_summaries);
|
|
my ($removed, $added) = diff_strings($old_summaries, $new_summaries);
|
|
if ($removed ne $added) {
|
|
my $sql_removed = &::SqlQuote($removed);
|
|
my $sql_added = &::SqlQuote($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 ($bug_id, $attach_id, $user_id, $timestamp,
|
|
$field_id, $sql_removed, $sql_added)");
|
|
|
|
$dbh->do("UPDATE bugs SET delta_ts = $timestamp WHERE bug_id = ?",
|
|
undef, $bug_id);
|
|
}
|
|
}
|
|
|
|
=pod
|
|
|
|
=over
|
|
|
|
=item C<create($flag, $timestamp)>
|
|
|
|
Creates a flag record in the database.
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub create {
|
|
my ($flag, $timestamp) = @_;
|
|
|
|
# Determine the ID for the flag record by retrieving the last ID used
|
|
# and incrementing it.
|
|
&::SendSQL("SELECT MAX(id) FROM flags");
|
|
$flag->{'id'} = (&::FetchOneColumn() || 0) + 1;
|
|
|
|
# Insert a record for the flag into the flags table.
|
|
my $attach_id =
|
|
$flag->{target}->{attachment} ? $flag->{target}->{attachment}->{id}
|
|
: "NULL";
|
|
my $requestee_id = $flag->{'requestee'} ? $flag->{'requestee'}->id : "NULL";
|
|
&::SendSQL("INSERT INTO flags (id, type_id,
|
|
bug_id, attach_id,
|
|
requestee_id, setter_id, status,
|
|
creation_date, modification_date)
|
|
VALUES ($flag->{'id'},
|
|
$flag->{'type'}->{'id'},
|
|
$flag->{'target'}->{'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, "request/email.txt.tmpl");
|
|
}
|
|
|
|
=pod
|
|
|
|
=over
|
|
|
|
=item C<migrate($old_attach_id, $new_attach_id, $timestamp)>
|
|
|
|
Moves a flag from one attachment to another. Useful for migrating
|
|
a flag from an obsolete attachment to the attachment that obsoleted it.
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub migrate {
|
|
my ($old_attach_id, $new_attach_id, $timestamp) = @_;
|
|
|
|
# Use the date/time we were given if possible (allowing calling code
|
|
# to synchronize the comment's timestamp with those of other records).
|
|
$timestamp = ($timestamp ? &::SqlQuote($timestamp) : "NOW()");
|
|
|
|
# Update the record in the flags table to point to the new attachment.
|
|
&::SendSQL("UPDATE flags " .
|
|
"SET attach_id = $new_attach_id , " .
|
|
" modification_date = $timestamp " .
|
|
"WHERE attach_id = $old_attach_id");
|
|
}
|
|
|
|
=pod
|
|
|
|
=over
|
|
|
|
=item C<modify($cgi, $timestamp)>
|
|
|
|
Modifies flags in the database when a user changes them.
|
|
Note that modified flags are always set active (is_active = 1) -
|
|
this will revive deleted flags that get changed through
|
|
attachment.cgi midairs. See bug 223878 for details.
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub modify {
|
|
my ($cgi, $timestamp) = @_;
|
|
my $setter = Bugzilla->user;
|
|
|
|
# Use the date/time we were given if possible (allowing calling code
|
|
# to synchronize the comment's timestamp with those of other records).
|
|
my $sql_timestamp = ($timestamp ? &::SqlQuote($timestamp) : "NOW()");
|
|
|
|
# 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} ,
|
|
target => $flag->{target} ,
|
|
setter => $setter,
|
|
status => "?",
|
|
requestee => new Bugzilla::User(login_to_id($login)) },
|
|
$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 '-') {
|
|
&::SendSQL("UPDATE flags
|
|
SET setter_id = " . $setter->id . ",
|
|
requestee_id = NULL ,
|
|
status = '$status' ,
|
|
modification_date = $sql_timestamp ,
|
|
is_active = 1
|
|
WHERE id = $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, "request/email.txt.tmpl");
|
|
}
|
|
elsif ($status eq '?') {
|
|
# Get the requestee, if any.
|
|
my $requestee_id = "NULL";
|
|
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.
|
|
&::SendSQL("UPDATE flags
|
|
SET setter_id = " . $setter->id . ",
|
|
requestee_id = $requestee_id ,
|
|
status = '$status' ,
|
|
modification_date = $sql_timestamp ,
|
|
is_active = 1
|
|
WHERE id = $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, "request/email.txt.tmpl");
|
|
}
|
|
# The user unset the flag; set is_active = 0
|
|
elsif ($status eq 'X') {
|
|
clear($flag->{'id'});
|
|
}
|
|
|
|
push(@flags, $flag);
|
|
}
|
|
|
|
return \@flags;
|
|
}
|
|
|
|
=pod
|
|
|
|
=over
|
|
|
|
=item C<clear($id)>
|
|
|
|
Deactivate a flag.
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub clear {
|
|
my ($id) = @_;
|
|
|
|
my $flag = get($id);
|
|
|
|
&::PushGlobalSQLState();
|
|
&::SendSQL("UPDATE flags SET is_active = 0 WHERE id = $id");
|
|
&::PopGlobalSQLState();
|
|
|
|
# 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, "request/email.txt.tmpl");
|
|
}
|
|
|
|
|
|
######################################################################
|
|
# Utility Functions
|
|
######################################################################
|
|
|
|
=pod
|
|
|
|
=over
|
|
|
|
=item C<FormToNewFlags($target, $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 ($target, $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' => $target->{'type'},
|
|
'product_id' => $target->{'product_id'},
|
|
'component_id' => $target->{'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 active flags of this type already set for this target.
|
|
my $has_flags = count(
|
|
{ 'type_id' => $type_id,
|
|
'target_type' => $target->{'type'},
|
|
'bug_id' => $target->{'bug'}->{'id'},
|
|
'attach_id' => $target->{'attachment'} ?
|
|
$target->{'attachment'}->{'id'} : undef,
|
|
'is_active' => 1 });
|
|
|
|
# Do not create a new flag of this type if this flag type is
|
|
# not multiplicable and already has an active 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 ,
|
|
target => $target ,
|
|
setter => $setter ,
|
|
status => $status ,
|
|
requestee => $requestee });
|
|
last if !$flag_type->{'is_multiplicable'};
|
|
}
|
|
}
|
|
else {
|
|
push (@flags, { type => $flag_type ,
|
|
target => $target ,
|
|
setter => $setter ,
|
|
status => $status });
|
|
}
|
|
}
|
|
|
|
# Return the list of flags.
|
|
return \@flags;
|
|
}
|
|
|
|
=pod
|
|
|
|
=over
|
|
|
|
=item C<GetBug($id)>
|
|
|
|
Returns a hash of information about a target bug.
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
# Ideally, we'd use Bug.pm, but it's way too heavyweight, and it can't be
|
|
# made lighter without totally rewriting it, so we'll use this function
|
|
# until that one gets rewritten.
|
|
sub GetBug {
|
|
my ($id) = @_;
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
# Save the currently running query (if any) so we do not overwrite it.
|
|
&::PushGlobalSQLState();
|
|
|
|
&::SendSQL("SELECT 1, short_desc, product_id, component_id,
|
|
COUNT(bug_group_map.group_id)
|
|
FROM bugs LEFT JOIN bug_group_map
|
|
ON (bugs.bug_id = bug_group_map.bug_id)
|
|
WHERE bugs.bug_id = $id " .
|
|
$dbh->sql_group_by('bugs.bug_id',
|
|
'short_desc, product_id, component_id'));
|
|
|
|
my $bug = { 'id' => $id };
|
|
|
|
($bug->{'exists'}, $bug->{'summary'}, $bug->{'product_id'},
|
|
$bug->{'component_id'}, $bug->{'restricted'}) = &::FetchSQLData();
|
|
|
|
# Restore the previously running query (if any).
|
|
&::PopGlobalSQLState();
|
|
|
|
return $bug;
|
|
}
|
|
|
|
=pod
|
|
|
|
=over
|
|
|
|
=item C<get_target($bug_id, $attach_id)>
|
|
|
|
Someone please document this function.
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub get_target {
|
|
my ($bug_id, $attach_id) = @_;
|
|
|
|
# Create an object representing the target bug/attachment.
|
|
my $target = { 'exists' => 0 };
|
|
|
|
if ($attach_id) {
|
|
$target->{'attachment'} = Bugzilla::Attachment->get($attach_id);
|
|
if ($bug_id) {
|
|
# Make sure the bug and attachment IDs correspond to each other
|
|
# (i.e. this is the bug to which this attachment is attached).
|
|
if (!$target->{'attachment'}
|
|
|| $target->{'attachment'}->{'bug_id'} != $bug_id)
|
|
{
|
|
return { 'exists' => 0 };
|
|
}
|
|
}
|
|
$target->{'bug'} = GetBug($bug_id);
|
|
$target->{'exists'} = 1;
|
|
$target->{'type'} = "attachment";
|
|
}
|
|
elsif ($bug_id) {
|
|
$target->{'bug'} = GetBug($bug_id);
|
|
$target->{'exists'} = $target->{'bug'}->{'exists'};
|
|
$target->{'type'} = "bug";
|
|
}
|
|
|
|
return $target;
|
|
}
|
|
|
|
=pod
|
|
|
|
=over
|
|
|
|
=item C<notify($flag, $template_file)>
|
|
|
|
Sends an email notification about a flag being created, fulfilled
|
|
or deleted.
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub notify {
|
|
my ($flag, $template_file) = @_;
|
|
|
|
my $template = Bugzilla->template;
|
|
|
|
# There is nobody to notify.
|
|
return unless ($flag->{'addressee'} || $flag->{'type'}->{'cc_list'});
|
|
|
|
my $attachment_is_private = $flag->{'target'}->{'attachment'} ?
|
|
$flag->{'target'}->{'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 ($flag->{'target'}->{'bug'}->{'restricted'} || $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 $flag->{'target'}->{'bug'}->{'restricted'}
|
|
&& !$ccuser->can_see_bug($flag->{'target'}->{'bug'}->{'id'});
|
|
next if $attachment_is_private
|
|
&& Param("insidergroup")
|
|
&& !$ccuser->in_group(Param("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 };
|
|
my $message;
|
|
my $rv = $template->process($template_file, $vars, \$message);
|
|
if (!$rv) {
|
|
Bugzilla->cgi->header();
|
|
ThrowTemplateError($template->error());
|
|
}
|
|
|
|
Bugzilla::BugMail::MessageToMTA($message);
|
|
}
|
|
}
|
|
|
|
# Cancel all request flags from the attachment being obsoleted.
|
|
sub CancelRequests {
|
|
my ($bug_id, $attach_id, $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 flags.is_active = 1
|
|
AND attachments.isobsolete = 0",
|
|
undef, $attach_id);
|
|
|
|
return if (!scalar(@$request_ids));
|
|
|
|
# Take a snapshot of flags before any changes.
|
|
my @old_summaries = snapshot($bug_id, $attach_id) if ($timestamp);
|
|
foreach my $flag (@$request_ids) { clear($flag) }
|
|
|
|
# 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_id, $attach_id);
|
|
update_activity($bug_id, $attach_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'") }
|
|
elsif ($field eq 'is_active') { push(@criteria, "is_active = $value") }
|
|
}
|
|
|
|
return @criteria;
|
|
}
|
|
|
|
=pod
|
|
|
|
=over
|
|
|
|
=item C<perlify_record($exists, $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 ($exists, $id, $type_id, $bug_id, $attach_id,
|
|
$requestee_id, $setter_id, $status) = @_;
|
|
|
|
return undef unless defined($exists);
|
|
|
|
my $flag =
|
|
{
|
|
exists => $exists ,
|
|
id => $id ,
|
|
type => Bugzilla::FlagType::get($type_id) ,
|
|
target => get_target($bug_id, $attach_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>
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
1;
|