Fix for bug 98801: Implementation of the request tracker, a set of enhancements to attachment statuses.

r=gerv,bbaetz
This commit is contained in:
myk%mozilla.org 2002-09-28 18:42:54 +00:00
Родитель eaa54d5402
Коммит 76cd798b52
35 изменённых файлов: 3411 добавлений и 327 удалений

Просмотреть файл

@ -31,10 +31,32 @@ package Attachment;
# This module requires that its caller have said "require CGI.pl" to import
# relevant functions from that script and its companion globals.pl.
# Use the Flag module to handle flags.
use Bugzilla::Flag;
############################################################################
# Functions
############################################################################
sub new {
# Returns a hash of information about the attachment with the given ID.
my ($invocant, $id) = @_;
return undef if !$id;
my $self = { 'id' => $id };
my $class = ref($invocant) || $invocant;
bless($self, $class);
&::PushGlobalSQLState();
&::SendSQL("SELECT 1, description, bug_id FROM attachments " .
"WHERE attach_id = $id");
($self->{'exists'}, $self->{'summary'}, $self->{'bug_id'}) =
&::FetchSQLData();
&::PopGlobalSQLState();
return $self;
}
sub query
{
# Retrieves and returns an array of attachment records for a given bug.
@ -65,23 +87,9 @@ sub query
$a{'date'} = "$1-$2-$3 $4:$5";
}
# Retrieve a list of status flags that have been set on the attachment.
&::PushGlobalSQLState();
&::SendSQL("
SELECT name
FROM attachstatuses, attachstatusdefs
WHERE attach_id = $a{'attachid'}
AND attachstatuses.statusid = attachstatusdefs.id
ORDER BY sortkey
");
my @statuses = ();
while (&::MoreSQLData()) {
my ($status) = &::FetchSQLData();
push @statuses , $status;
}
$a{'statuses'} = \@statuses;
&::PopGlobalSQLState();
# Retrieve a list of flags for this attachment.
$a{'flags'} = Bugzilla::Flag::match({ 'attach_id' => $a{'attachid'} });
# We will display the edit link if the user can edit the attachment;
# ie the are the submitter, or they have canedit.
# Also show the link if the user is not logged in - in that cae,

Просмотреть файл

@ -31,10 +31,32 @@ package Attachment;
# This module requires that its caller have said "require CGI.pl" to import
# relevant functions from that script and its companion globals.pl.
# Use the Flag module to handle flags.
use Bugzilla::Flag;
############################################################################
# Functions
############################################################################
sub new {
# Returns a hash of information about the attachment with the given ID.
my ($invocant, $id) = @_;
return undef if !$id;
my $self = { 'id' => $id };
my $class = ref($invocant) || $invocant;
bless($self, $class);
&::PushGlobalSQLState();
&::SendSQL("SELECT 1, description, bug_id FROM attachments " .
"WHERE attach_id = $id");
($self->{'exists'}, $self->{'summary'}, $self->{'bug_id'}) =
&::FetchSQLData();
&::PopGlobalSQLState();
return $self;
}
sub query
{
# Retrieves and returns an array of attachment records for a given bug.
@ -65,23 +87,9 @@ sub query
$a{'date'} = "$1-$2-$3 $4:$5";
}
# Retrieve a list of status flags that have been set on the attachment.
&::PushGlobalSQLState();
&::SendSQL("
SELECT name
FROM attachstatuses, attachstatusdefs
WHERE attach_id = $a{'attachid'}
AND attachstatuses.statusid = attachstatusdefs.id
ORDER BY sortkey
");
my @statuses = ();
while (&::MoreSQLData()) {
my ($status) = &::FetchSQLData();
push @statuses , $status;
}
$a{'statuses'} = \@statuses;
&::PopGlobalSQLState();
# Retrieve a list of flags for this attachment.
$a{'flags'} = Bugzilla::Flag::match({ 'attach_id' => $a{'attachid'} });
# We will display the edit link if the user can edit the attachment;
# ie the are the submitter, or they have canedit.
# Also show the link if the user is not logged in - in that cae,

Просмотреть файл

@ -0,0 +1,591 @@
# -*- 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>
################################################################################
# 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 Attachment;
use vars qw($template $vars);
# Note! This module requires that its caller have said "require CGI.pl"
# to import relevant functions from that script and its companion globals.pl.
################################################################################
# Global Variables
################################################################################
# basic sets of columns and tables for getting flags from the database
my @base_columns =
("1", "id", "type_id", "bug_id", "attach_id", "requestee_id", "setter_id",
"status");
# Note: when adding tables to @base_tables, make sure to include the separator
# (i.e. a comma or 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.
my @base_tables = ("flags");
################################################################################
# Searching/Retrieving Flags
################################################################################
# !!! Implement a cache for this function!
sub get {
# Retrieves and returns a flag from the database.
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;
}
sub match {
# 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.
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;
}
sub count {
# 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.
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
################################################################################
sub validate {
# Validates fields containing flag modifications.
my ($data) = @_;
# 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 : (), keys %$data);
foreach my $id (@ids)
{
my $status = $data->{"flag-$id"};
# Make sure the flag exists.
my $flag = get($id);
$flag || &::ThrowCodeError("flag_nonexistent", { id => $id });
# Don't bother validating flags the user didn't change.
next if $status eq $flag->{'status'};
# Make sure the user chose a valid status.
grep($status eq $_, qw(X + - ?))
|| &::ThrowCodeError("flag_status_invalid",
{ id => $id , status => $status });
}
}
sub process {
# 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 data is the form data
# with flag fields that the user submitted, the old bug is the bug record
# before the user made changes to it, and the new bug is the bug record
# after the user made changes to it.
my ($target, $timestamp, $data, $oldbug, $newbug) = @_;
# 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 $flags = match({ 'bug_id' => $target->{'bug'}->{'id'} ,
'attach_id' => $target->{'attachment'}->{'id'} });
my @old_summaries;
foreach my $flag (@$flags) {
my $summary = $flag->{'type'}->{'name'} . $flag->{'status'};
push(@old_summaries, $summary);
}
# Create new flags and update existing flags.
my $new_flags = FormToNewFlags($target, $data);
foreach my $flag (@$new_flags) { create($flag, $timestamp) }
modify($data, $timestamp);
# In case the bug's product/component has changed, clear flags that are
# no longer valid.
&::SendSQL("
SELECT flags.id
FROM flags, bugs LEFT OUTER JOIN flaginclusions 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 flags.type_id = $target->{'bug'}->{'id'}
AND flags.bug_id = bugs.bug_id
AND i.type_id IS NULL
");
clear(&::FetchOneColumn()) while &::MoreSQLData();
&::SendSQL("
SELECT flags.id
FROM flags, bugs, flagexclusions e
WHERE flags.type_id = $target->{'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)
");
clear(&::FetchOneColumn()) while &::MoreSQLData();
# Take a snapshot of flags after changes.
$flags = match({ 'bug_id' => $target->{'bug'}->{'id'} ,
'attach_id' => $target->{'attachment'}->{'id'} });
my @new_summaries;
foreach my $flag (@$flags) {
my $summary = $flag->{'type'}->{'name'} . $flag->{'status'};
push(@new_summaries, $summary);
}
my $old_summaries = join(", ", @old_summaries);
my $new_summaries = join(", ", @new_summaries);
my ($removed, $added) = &::DiffStrings($old_summaries, $new_summaries);
if ($removed ne $added) {
my $sql_removed = &::SqlQuote($removed);
my $sql_added = &::SqlQuote($added);
my $field_id = &::GetFieldID('flagtypes.name');
my $attach_id = $target->{'attachment'}->{'id'} || 'NULL';
&::SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, " .
"bug_when, fieldid, removed, added) VALUES " .
"($target->{'bug'}->{'id'}, $attach_id, $::userid, " .
"$timestamp, $field_id, $sql_removed, $sql_added)");
}
}
sub create {
# Creates a flag record in the database.
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'}->{'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'}->email_prefs->{'FlagRequestee'}
|| $flag->{'type'}->{'cc_list'}) {
notify($flag, "request/created-email.txt.tmpl");
}
}
sub migrate {
# Moves a flag from one attachment to another. Useful for migrating
# a flag from an obsolete attachment to the attachment that obsoleted it.
my ($old_attach_id, $new_attach_id) = @_;
# Update the record in the flags table to point to the new attachment.
&::SendSQL("UPDATE flags " .
"SET attach_id = $new_attach_id , " .
" modification_date = NOW() " .
"WHERE attach_id = $old_attach_id");
}
sub modify {
# Modifies flags in the database when a user changes them.
my ($data, $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()");
# Extract a list of flags from the form data.
my @ids = map(/^flag-(\d+)$/ ? $1 : (), keys %$data);
# Loop over flags and update their record in the database.
my @flags;
foreach my $id (@ids) {
my $flag = get($id);
my $status = $data->{"flag-$id"};
# Ignore flags the user didn't change.
next if $status eq $flag->{'status'};
# 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 = $::userid ,
status = '$status' ,
modification_date = $timestamp
WHERE id = $flag->{'id'}");
# Send an email notifying the relevant parties about the fulfillment.
if ($flag->{'setter'}->email_prefs->{'FlagRequester'}
|| $flag->{'type'}->{'cc_list'})
{
$flag->{'status'} = $status;
notify($flag, "request/fulfilled-email.txt.tmpl");
}
}
elsif ($status eq '?') {
&::SendSQL("UPDATE flags
SET status = '$status' ,
modification_date = $timestamp
WHERE id = $flag->{'id'}");
}
# The user unset the flag, so delete it from the database.
elsif ($status eq 'X') {
clear($flag->{'id'});
}
push(@flags, $flag);
}
return \@flags;
}
sub clear {
my ($id) = @_;
my $flag = get($id);
&::PushGlobalSQLState();
&::SendSQL("DELETE FROM flags WHERE id = $id");
&::PopGlobalSQLState();
# Set the flag's status to "cleared" so the email template
# knows why email is being sent about the request.
$flag->{'status'} = "X";
notify($flag, "request/fulfilled-email.txt.tmpl") if $flag->{'requestee'};
}
################################################################################
# Utility Functions
################################################################################
sub FormToNewFlags {
my ($target, $data) = @_;
# Flag for whether or not we must get verification of the requestees
# (if the user did not uniquely identify them).
my $verify_requestees = 0;
# Get information about the setter to add to each flag.
# Uses a conditional to suppress Perl's "used only once" warnings.
my $setter = new Bugzilla::User($::userid);
# Extract a list of flag type IDs from field names.
my @type_ids = map(/^flag_type-(\d+)$/ ? $1 : (), keys %$data);
@type_ids = grep($data->{"flag_type-$_"} ne 'X', @type_ids);
# Process the form data and create an array of flag objects.
my @flags;
foreach my $type_id (@type_ids) {
my $status = $data->{"flag_type-$type_id"};
&::trick_taint($status);
# Create the flag record and populate it with data from the form.
my $flag = {
type => Bugzilla::FlagType::get($type_id) ,
target => $target ,
setter => $setter ,
status => $status
};
my $requestee_str = $data->{"requestee-$type_id"} || $data->{'requestee'};
if ($requestee_str) {
$flag->{'requestee_str'} = $requestee_str;
MatchRequestees($flag);
$verify_requestees = 1 if scalar(@{$flag->{'requestees'}}) != 1;
}
# Add the flag to the array of flags.
push(@flags, $flag);
}
if ($verify_requestees) {
$vars->{'target'} = $target;
$vars->{'flags'} = \@flags;
$vars->{'form'} = $data;
$vars->{'mform'} = \%::MFORM || \%::MFORM;
print "Content-Type: text/html\n\n" unless $vars->{'header_done'};
$::template->process("request/verify.html.tmpl", $vars)
|| &::ThrowTemplateError($template->error());
exit;
}
# Return the list of flags.
return \@flags;
}
sub MatchRequestees {
my ($flag) = @_;
my $requestee_str = $flag->{'requestee_str'};
# To reduce the size of queries, require the user to enter at least
# three characters of each requestee's name unless this installation
# automatically appends an email suffix to each user's login name,
# in which case we can't guarantee their names are at least three
# characters long.
if (!&Param('emailsuffix') && length($requestee_str) < 3) {
&::ThrowUserError("requestee_too_short");
}
# Get a list of potential requestees whose email address or real name
# matches the substring entered by the user. Try an exact match first,
# then fall back to a substring search. Limit search to 100 matches,
# since at that point there are too many to make the user wade through,
# and we need to get the user to enter a more constrictive match string.
my $user_id = &::DBname_to_id($requestee_str);
if ($user_id) { $flag->{'requestees'} = [ new Bugzilla::User($user_id) ] }
else { $flag->{'requestees'} = Bugzilla::User::match($requestee_str, 101, 1) }
# If there is only one requestee match, make them the requestee.
if (scalar(@{$flag->{'requestees'}}) == 1) {
$flag->{'requestee'} = $flag->{'requestees'}[0];
}
# If there are too many requestee matches, throw an error.
elsif (scalar(@{$flag->{'requestees'}}) == 101) {
&::ThrowUserError("requestee_too_many_matches",
{ requestee => $requestee_str });
}
}
# 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 {
# Returns a hash of information about a target bug.
my ($id) = @_;
# Save the currently running query (if any) so we do not overwrite it.
&::PushGlobalSQLState();
&::SendSQL("SELECT 1, short_desc, product_id, component_id
FROM bugs
WHERE bug_id = $id");
my $bug = { 'id' => $id };
($bug->{'exists'}, $bug->{'summary'}, $bug->{'product_id'},
$bug->{'component_id'}) = &::FetchSQLData();
# Restore the previously running query (if any).
&::PopGlobalSQLState();
return $bug;
}
sub GetTarget {
my ($bug_id, $attach_id) = @_;
# Create an object representing the target bug/attachment.
my $target = { 'exists' => 0 };
if ($attach_id) {
$target->{'attachment'} = new Attachment($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).
$bug_id == $target->{'attachment'}->{'bug_id'}
|| return { 'exists' => 0 };
}
$target->{'bug'} = GetBug($target->{'attachment'}->{'bug_id'});
$target->{'exists'} = $target->{'attachment'}->{'exists'};
$target->{'type'} = "attachment";
}
elsif ($bug_id) {
$target->{'bug'} = GetBug($bug_id);
$target->{'exists'} = $target->{'bug'}->{'exists'};
$target->{'type'} = "bug";
}
return $target;
}
sub notify {
# Sends an email notification about a flag being created or fulfilled.
my ($flag, $template_file) = @_;
# Work around the intricacies of globals.pl not being templatized
# by defining local variables for the $::template and $::vars globals.
my $template = $::template;
my $vars = $::vars;
$vars->{'flag'} = $flag;
my $message;
my $rv =
$template->process($template_file, $vars, \$message);
if (!$rv) {
print "Content-Type: text/html\n\n" unless $vars->{'header_done'};
&::ThrowTemplateError($template->error());
}
my $delivery_mode = &::Param("sendmailnow") ? "" : "-ODeliveryMode=deferred";
open(SENDMAIL, "|/usr/lib/sendmail $delivery_mode -t -i")
|| die "Can't open sendmail";
print SENDMAIL $message;
close(SENDMAIL);
}
################################################################################
# Private Functions
################################################################################
sub sqlify_criteria {
# Converts a hash of criteria into a list of SQL 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;
}
sub perlify_record {
# Converts a row from the database into a Perl record.
my ($exists, $id, $type_id, $bug_id, $attach_id,
$requestee_id, $setter_id, $status) = @_;
my $flag =
{
exists => $exists ,
id => $id ,
type => Bugzilla::FlagType::get($type_id) ,
target => GetTarget($bug_id, $attach_id) ,
requestee => new Bugzilla::User($requestee_id) ,
setter => new Bugzilla::User($setter_id) ,
status => $status ,
};
return $flag;
}
1;

Просмотреть файл

@ -0,0 +1,325 @@
# -*- 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>
################################################################################
# Module Initialization
################################################################################
# Make it harder for us to do dangerous things in Perl.
use strict;
# This module implements flag types for the flag tracker.
package Bugzilla::FlagType;
# Use Bugzilla's User module which contains utilities for handling users.
use Bugzilla::User;
# Note! This module requires that its caller have said "require CGI.pl"
# to import relevant functions from that script and its companion globals.pl.
################################################################################
# Global Variables
################################################################################
# basic sets of columns and tables for getting flag types from the database
my @base_columns =
("1", "flagtypes.id", "flagtypes.name", "flagtypes.description",
"flagtypes.cc_list", "flagtypes.target_type", "flagtypes.sortkey",
"flagtypes.is_active", "flagtypes.is_requestable",
"flagtypes.is_requesteeble", "flagtypes.is_multiplicable");
# Note: when adding tables to @base_tables, make sure to include the separator
# (i.e. a comma or 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.
my @base_tables = ("flagtypes");
################################################################################
# Public Functions
################################################################################
sub get {
# Returns a hash of information about a flag type.
my ($id) = @_;
my $select_clause = "SELECT " . join(", ", @base_columns);
my $from_clause = "FROM " . join(" ", @base_tables);
&::PushGlobalSQLState();
&::SendSQL("$select_clause $from_clause WHERE flagtypes.id = $id");
my @data = &::FetchSQLData();
my $type = perlify_record(@data);
&::PopGlobalSQLState();
return $type;
}
sub get_inclusions {
my ($id) = @_;
return get_clusions($id, "in");
}
sub get_exclusions {
my ($id) = @_;
return get_clusions($id, "ex");
}
sub get_clusions {
my ($id, $type) = @_;
&::PushGlobalSQLState();
&::SendSQL("SELECT products.name, components.name " .
"FROM flagtypes, flag${type}clusions " .
"LEFT OUTER JOIN products ON flag${type}clusions.product_id = products.id " .
"LEFT OUTER JOIN components ON flag${type}clusions.component_id = components.id " .
"WHERE flagtypes.id = $id AND flag${type}clusions.type_id = flagtypes.id");
my @clusions = ();
while (&::MoreSQLData()) {
my ($product, $component) = &::FetchSQLData();
$product ||= "Any";
$component ||= "Any";
push(@clusions, "$product:$component");
}
&::PopGlobalSQLState();
return \@clusions;
}
sub match {
# Queries the database for flag types matching the given criteria
# and returns the set of matching types.
my ($criteria, $include_count) = @_;
my @tables = @base_tables;
my @columns = @base_columns;
my $having = "";
# Include a count of the number of flags per type if requested.
if ($include_count) {
push(@columns, "COUNT(flags.id)");
push(@tables, "LEFT OUTER JOIN flags ON flagtypes.id = flags.type_id");
}
# Generate the SQL WHERE criteria.
my @criteria = sqlify_criteria($criteria, \@tables, \@columns, \$having);
# Build the query, grouping the types if we are counting flags.
my $select_clause = "SELECT " . join(", ", @columns);
my $from_clause = "FROM " . join(" ", @tables);
my $where_clause = "WHERE " . join(" AND ", @criteria);
my $query = "$select_clause $from_clause $where_clause";
$query .= " GROUP BY flagtypes.id " if ($include_count || $having ne "");
$query .= " HAVING $having " if $having ne "";
$query .= " ORDER BY flagtypes.sortkey, flagtypes.name";
# Execute the query and retrieve the results.
&::PushGlobalSQLState();
&::SendSQL($query);
my @types;
while (&::MoreSQLData()) {
my @data = &::FetchSQLData();
my $type = perlify_record(@data);
push(@types, $type);
}
&::PopGlobalSQLState();
return \@types;
}
sub count {
# Returns the total number of flag types matching the given criteria.
my ($criteria) = @_;
# Generate query components.
my @tables = @base_tables;
my @columns = ("COUNT(flagtypes.id)");
my $having = "";
my @criteria = sqlify_criteria($criteria, \@tables, \@columns, \$having);
# Build the query.
my $select_clause = "SELECT " . join(", ", @columns);
my $from_clause = "FROM " . join(" ", @tables);
my $where_clause = "WHERE " . join(" AND ", @criteria);
my $query = "$select_clause $from_clause $where_clause";
$query .= " GROUP BY flagtypes.id HAVING $having " if $having ne "";
# Execute the query and get the results.
&::PushGlobalSQLState();
&::SendSQL($query);
my $count = &::FetchOneColumn();
&::PopGlobalSQLState();
return $count;
}
sub validate {
my ($data) = @_;
# Get a list of flags types to validate. Uses the "map" function
# to extract flag type IDs from form field names by matching columns
# whose name looks like "flag_type-nnn", where "nnn" is the ID,
# and returning just the ID portion of matching field names.
my @ids = map(/^flag_type-(\d+)$/ ? $1 : (), keys %$data);
foreach my $id (@ids)
{
my $status = $data->{"flag_type-$id"};
# Don't bother validating types the user didn't touch.
next if $status eq "X";
# Make sure the flag exists.
get($id)
|| &::ThrowCodeError("flag_type_nonexistent", { id => $id });
# Make sure the value of the field is a valid status.
grep($status eq $_, qw(X + - ?))
|| &::ThrowCodeError("flag_status_invalid",
{ id => $id , status => $status });
}
}
sub normalize {
# Given a list of flag types, checks its flags to make sure they should
# still exist after a change to the inclusions/exclusions lists.
# A list of IDs of flag types to normalize.
my (@ids) = @_;
my $ids = join(", ", @ids);
# Check for flags whose product/component is no longer included.
&::SendSQL("
SELECT flags.id
FROM flags, bugs LEFT OUTER 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 flags.type_id IN ($ids)
AND flags.bug_id = bugs.bug_id
AND i.type_id IS NULL
");
Bugzilla::Flag::clear(&::FetchOneColumn()) while &::MoreSQLData();
&::SendSQL("
SELECT flags.id
FROM flags, bugs, flagexclusions AS e
WHERE flags.type_id IN ($ids)
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)
");
Bugzilla::Flag::clear(&::FetchOneColumn()) while &::MoreSQLData();
}
################################################################################
# Private Functions
################################################################################
sub sqlify_criteria {
# Converts a hash of criteria into a list of SQL criteria.
# $criteria is a reference to the criteria (field => value),
# $tables is a reference to an array of tables being accessed
# by the query, $columns is a reference to an array of columns
# being returned by the query, and $having is a reference to
# a criterion to put into the HAVING clause.
my ($criteria, $tables, $columns, $having) = @_;
# 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 ($criteria->{name}) {
push(@criteria, "flagtypes.name = " . &::SqlQuote($criteria->{name}));
}
if ($criteria->{target_type}) {
# The target type is stored in the database as a one-character string
# ("a" for attachment and "b" for bug), but this function takes complete
# names ("attachment" and "bug") for clarity, so we must convert them.
my $target_type = &::SqlQuote(substr($criteria->{target_type}, 0, 1));
push(@criteria, "flagtypes.target_type = $target_type");
}
if (exists($criteria->{is_active})) {
my $is_active = $criteria->{is_active} ? "1" : "0";
push(@criteria, "flagtypes.is_active = $is_active");
}
if ($criteria->{product_id} && $criteria->{'component_id'}) {
my $product_id = $criteria->{product_id};
my $component_id = $criteria->{component_id};
# Add inclusions to the query, which simply involves joining the table
# by flag type ID and target product/component.
push(@$tables, ", flaginclusions");
push(@criteria, "flagtypes.id = flaginclusions.type_id");
push(@criteria, "(flaginclusions.product_id = $product_id " .
" OR flaginclusions.product_id IS NULL)");
push(@criteria, "(flaginclusions.component_id = $component_id " .
" OR flaginclusions.component_id IS NULL)");
# Add exclusions to the query, which is more complicated. First of all,
# we do a LEFT JOIN so we don't miss flag types with no exclusions.
# Then, as with inclusions, we join on flag type ID and target product/
# component. However, since we want flag types that *aren't* on the
# exclusions list, we count the number of exclusions records returned
# and use a HAVING clause to weed out types with one or more exclusions.
my $join_clause = "flagtypes.id = flagexclusions.type_id " .
"AND (flagexclusions.product_id = $product_id " .
"OR flagexclusions.product_id IS NULL) " .
"AND (flagexclusions.component_id = $component_id " .
"OR flagexclusions.component_id IS NULL)";
push(@$tables, "LEFT JOIN flagexclusions ON ($join_clause)");
push(@$columns, "COUNT(flagexclusions.type_id) AS num_exclusions");
$$having = "num_exclusions = 0";
}
return @criteria;
}
sub perlify_record {
# Converts data retrieved from the database into a Perl record.
my $type = {};
$type->{'exists'} = $_[0];
$type->{'id'} = $_[1];
$type->{'name'} = $_[2];
$type->{'description'} = $_[3];
$type->{'cc_list'} = $_[4];
$type->{'target_type'} = $_[5] eq "b" ? "bug" : "attachment";
$type->{'sortkey'} = $_[6];
$type->{'is_active'} = $_[7];
$type->{'is_requestable'} = $_[8];
$type->{'is_requesteeble'} = $_[9];
$type->{'is_multiplicable'} = $_[10];
$type->{'flag_count'} = $_[11];
return $type;
}
1;

Просмотреть файл

@ -62,6 +62,7 @@ sub init {
my @fields;
my @supptables;
my @wherepart;
my @having = ("(cntuseringroups = cntbugingroups OR canseeanyway)");
@fields = @$fieldsref if $fieldsref;
my %F;
my %M;
@ -265,8 +266,8 @@ sub init {
}
my $chartid;
# $statusid is used by the code that queries for attachment statuses.
my $statusid = 0;
# $type_id is used by the code that queries for attachment flags.
my $type_id = 0;
my $f;
my $ff;
my $t;
@ -358,69 +359,61 @@ sub init {
}
$f = "$table.$field";
},
"^attachstatusdefs.name," => sub {
# The below has Fun with the names for attachment statuses. This
# isn't needed for changed* queries, so exclude those - the
# generic stuff will cope
"^flagtypes.name," => sub {
# Matches bugs by flag name/status.
# Note that--for the purposes of querying--a flag comprises
# its name plus its status (i.e. a flag named "review"
# with a status of "+" can be found by searching for "review+").
# Don't do anything if this condition is about changes to flags,
# as the generic change condition processors can handle those.
return if ($t =~ m/^changed/);
# Searching for "status != 'bar'" wants us to look for an
# attachment without the 'bar' status, not for an attachment with
# a status not equal to 'bar' (Which would pick up an attachment
# with more than one status). We do this by LEFT JOINS, after
# grabbing the matching attachment status ids.
# Note that this still won't find bugs with no attachments, since
# that isn't really what people would expect.
# First, get the attachment status ids, using the other funcs
# to match the WHERE term.
# Note that we need to reverse the negated bits for this to work
# This somewhat abuses the definitions of the various terms -
# eg, does 'contains all' mean that the status has to contain all
# those words, or that all those words must be exact matches to
# statuses, which must all be on a single attachment, or should
# the match on the status descriptions be a contains match, too?
my $inverted = 0;
if ($t =~ m/not(.*)/) {
$t = $1;
$inverted = 1;
}
$ref = $funcsbykey{",$t"};
&$ref;
&::SendSQL("SELECT id FROM attachstatusdefs WHERE $term");
my @as_ids;
while (&::MoreSQLData()) {
push @as_ids, &::FetchOneColumn();
}
# When searching for multiple statuses within a single boolean chart,
# we want to match each status record separately. In other words,
# "status = 'foo' AND status = 'bar'" should match attachments with
# one status record equal to "foo" and another one equal to "bar",
# not attachments where the same status record equals both "foo" and
# "bar" (which is nonsensical). In order to do this we must add an
# additional counter to the end of the "attachstatuses" table
# reference.
++$statusid;
my $attachtable = "attachments_$chartid";
my $statustable = "attachstatuses_${chartid}_$statusid";
push(@supptables, "attachments $attachtable");
my $join = "LEFT JOIN attachstatuses $statustable ON ".
"($attachtable.attach_id = $statustable.attach_id AND " .
"$statustable.statusid IN (" . join(",", @as_ids) . "))";
push(@supptables, $join);
push(@wherepart, "bugs.bug_id = $attachtable.bug_id");
if ($inverted) {
$term = "$statustable.statusid IS NULL";
} else {
$term = "$statustable.statusid IS NOT NULL";
# Add the flags and flagtypes tables to the query. We do
# a left join here so bugs without any flags still match
# negative conditions (f.e. "flag isn't review+").
my $flags = "flags_$chartid";
push(@supptables, "LEFT JOIN flags $flags " .
"ON bugs.bug_id = $flags.bug_id");
my $flagtypes = "flagtypes_$chartid";
push(@supptables, "LEFT JOIN flagtypes $flagtypes " .
"ON $flags.type_id = $flagtypes.id");
# Generate the condition by running the operator-specific function.
# Afterwards the condition resides in the global $term variable.
$ff = "CONCAT($flagtypes.name, $flags.status)";
&{$funcsbykey{",$t"}};
# If this is a negative condition (f.e. flag isn't "review+"),
# we only want bugs where all flags match the condition, not
# those where any flag matches, which needs special magic.
# Instead of adding the condition to the WHERE clause, we select
# the number of flags matching the condition and the total number
# of flags on each bug, then compare them in a HAVING clause.
# If the numbers are the same, all flags match the condition,
# so this bug should be included.
if ($t =~ m/not/) {
push(@fields, "SUM($ff IS NOT NULL) AS allflags_$chartid");
push(@fields, "SUM($term) AS matchingflags_$chartid");
push(@having, "allflags_$chartid = matchingflags_$chartid");
$term = "0=0";
}
},
"^requesters.login_name," => sub {
push(@supptables, "flags flags_$chartid");
push(@wherepart, "bugs.bug_id = flags_$chartid.bug_id");
push(@supptables, "profiles requesters_$chartid");
push(@wherepart, "flags_$chartid.requester_id = requesters_$chartid.userid");
$f = "requesters_$chartid.login_name";
},
"^setters.login_name," => sub {
push(@supptables, "flags flags_$chartid");
push(@wherepart, "bugs.bug_id = flags_$chartid.bug_id");
push(@supptables, "profiles setters_$chartid");
push(@wherepart, "flags_$chartid.setter_id = setters_$chartid.userid");
$f = "setters_$chartid.login_name";
},
"^changedin," => sub {
$f = "(to_days(now()) - to_days(bugs.delta_ts))";
},
@ -817,8 +810,7 @@ sub init {
# Make sure we create a legal SQL query.
@andlist = ("1 = 1") if !@andlist;
my $query = ("SELECT DISTINCT " .
join(', ', @fields) .
my $query = ("SELECT " . join(', ', @fields) .
", COUNT(DISTINCT ugmap.group_id) AS cntuseringroups, " .
" COUNT(DISTINCT bgmap.group_id) AS cntbugingroups, " .
" ((COUNT(DISTINCT ccmap.who) AND cclist_accessible) " .
@ -834,11 +826,9 @@ sub init {
" LEFT JOIN cc AS ccmap " .
" ON ccmap.who = $::userid AND ccmap.bug_id = bugs.bug_id " .
" WHERE " . join(' AND ', (@wherepart, @andlist)) .
" GROUP BY bugs.bug_id " .
" HAVING cntuseringroups = cntbugingroups" .
" OR canseeanyway"
);
" GROUP BY bugs.bug_id" .
" HAVING " . join(" AND ", @having));
if ($debug) {
print "<p><code>" . value_quote($query) . "</code></p>\n";
exit;

Просмотреть файл

@ -0,0 +1,176 @@
# -*- 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>
################################################################################
# Module Initialization
################################################################################
# Make it harder for us to do dangerous things in Perl.
use strict;
# This module implements utilities for dealing with Bugzilla users.
package Bugzilla::User;
################################################################################
# Functions
################################################################################
my $user_cache = {};
sub new {
# Returns a hash of information about a particular user.
my $invocant = shift;
my $class = ref($invocant) || $invocant;
my $exists = 1;
my ($id, $name, $email) = @_;
return undef if !$id;
return $user_cache->{$id} if exists($user_cache->{$id});
my $self = { 'id' => $id };
bless($self, $class);
if (!$name && !$email) {
&::PushGlobalSQLState();
&::SendSQL("SELECT 1, realname, login_name FROM profiles WHERE userid = $id");
($exists, $name, $email) = &::FetchSQLData();
&::PopGlobalSQLState();
}
$self->{'name'} = $name;
$self->{'email'} = $email;
$self->{'exists'} = $exists;
# Generate a string to identify the user by name + email if the user
# has a name or by email only if she doesn't.
$self->{'identity'} = $name ? "$name <$email>" : $email;
# Generate a user "nickname" -- i.e. a shorter, not-necessarily-unique name
# by which to identify the user. Currently the part of the user's email
# address before the at sign (@), but that could change, especially if we
# implement usernames not dependent on email address.
my @email_components = split("@", $email);
$self->{'nick'} = $email_components[0];
$user_cache->{$id} = $self;
return $self;
}
sub match {
# Generates a list of users whose login name (email address) or real name
# matches a substring.
# $str contains the string to match against, while $limit contains the
# maximum number of records to retrieve.
my ($str, $limit, $exclude_disabled) = @_;
# Build the query.
my $sqlstr = &::SqlQuote($str);
my $qry = "
SELECT userid, realname, login_name
FROM profiles
WHERE (INSTR(login_name, $sqlstr) OR INSTR(realname, $sqlstr))
";
$qry .= "AND disabledtext = '' " if $exclude_disabled;
$qry .= "ORDER BY realname, login_name ";
$qry .= "LIMIT $limit " if $limit;
# Execute the query, retrieve the results, and make them into User objects.
my @users;
&::PushGlobalSQLState();
&::SendSQL($qry);
push(@users, new Bugzilla::User(&::FetchSQLData())) while &::MoreSQLData();
&::PopGlobalSQLState();
return \@users;
}
sub email_prefs {
# Get or set (not implemented) the user's email notification preferences.
my $self = shift;
# If the calling code is setting the email preferences, update the object
# but don't do anything else. This needs to write email preferences back
# to the database.
if (@_) { $self->{email_prefs} = shift; return; }
# If we already got them from the database, return the existing values.
return $self->{email_prefs} if $self->{email_prefs};
# Retrieve the values from the database.
&::SendSQL("SELECT emailflags FROM profiles WHERE userid = $self->{id}");
my ($flags) = &::FetchSQLData();
my @roles = qw(Owner Reporter QAcontact CClist Voter);
my @reasons = qw(Removeme Comments Attachments Status Resolved Keywords
CC Other Unconfirmed);
# If the prefs are empty, this user hasn't visited the email pane
# of userprefs.cgi since before the change to use the "emailflags"
# column, so initialize that field with the default prefs.
if (!$flags) {
# Create a default prefs string that causes the user to get all email.
$flags = "ExcludeSelf~on~FlagRequestee~on~FlagRequester~on~";
foreach my $role (@roles) {
foreach my $reason (@reasons) {
$flags .= "email$role$reason~on~";
}
}
chop $flags;
}
# Convert the prefs from the flags string from the database into
# a Perl record. The 255 param is here because split will trim
# any trailing null fields without a third param, which causes Perl
# to eject lots of warnings. Any suitably large number would do.
my $prefs = { split(/~/, $flags, 255) };
# Determine the value of the "excludeself" global email preference.
# Note that the value of "excludeself" is assumed to be off if the
# preference does not exist in the user's list, unlike other
# preferences whose value is assumed to be on if they do not exist.
$prefs->{ExcludeSelf} =
exists($prefs->{ExcludeSelf}) && $prefs->{ExcludeSelf} eq "on";
# Determine the value of the global request preferences.
foreach my $pref qw(FlagRequestee FlagRequester) {
$prefs->{$pref} = !exists($prefs->{$pref}) || $prefs->{$pref} eq "on";
}
# Determine the value of the rest of the preferences by looping over
# all roles and reasons and converting their values to Perl booleans.
foreach my $role (@roles) {
foreach my $reason (@reasons) {
my $key = "email$role$reason";
$prefs->{$key} = !exists($prefs->{$key}) || $prefs->{$key} eq "on";
}
}
$self->{email_prefs} = $prefs;
return $self->{email_prefs};
}
1;

Просмотреть файл

@ -46,6 +46,10 @@ if ($^O eq 'MSWin32') {
# Include the Bugzilla CGI and general utility library.
require "CGI.pl";
# Use these modules to handle flags.
use Bugzilla::Flag;
use Bugzilla::FlagType;
# Establish a connection to the database backend.
ConnectToDatabase();
@ -110,7 +114,8 @@ elsif ($action eq "update")
validateContentType() unless $::FORM{'ispatch'};
validateIsObsolete();
validatePrivate();
validateStatuses();
Bugzilla::Flag::validate(\%::FORM);
Bugzilla::FlagType::validate(\%::FORM);
update();
}
else
@ -240,29 +245,6 @@ sub validatePrivate
$::FORM{'isprivate'} = $::FORM{'isprivate'} ? 1 : 0;
}
sub validateStatuses
{
# Get a list of attachment statuses that are valid for this attachment.
PushGlobalSQLState();
SendSQL("SELECT attachstatusdefs.id
FROM attachments, bugs, attachstatusdefs
WHERE attachments.attach_id = $::FORM{'id'}
AND attachments.bug_id = bugs.bug_id
AND attachstatusdefs.product_id = bugs.product_id");
my @statusdefs;
push(@statusdefs, FetchSQLData()) while MoreSQLData();
PopGlobalSQLState();
foreach my $status (@{$::MFORM{'status'}})
{
grep($_ == $status, @statusdefs)
|| ThrowUserError("invalid_attach_status");
# We have tested that the status is valid, so it can be detainted
detaint_natural($status);
}
}
sub validateData
{
$::FORM{'data'}
@ -380,18 +362,6 @@ sub viewall
# !!! Yuck, what an ugly hack. Fix it!
$a{'isviewable'} = ( $a{'contenttype'} =~ /^(text|image|application\/vnd\.mozilla\.)/ );
# Retrieve a list of status flags that have been set on the attachment.
PushGlobalSQLState();
SendSQL("SELECT name
FROM attachstatuses, attachstatusdefs
WHERE attach_id = $a{'attachid'}
AND attachstatuses.statusid = attachstatusdefs.id
ORDER BY sortkey");
my @statuses;
push(@statuses, FetchSQLData()) while MoreSQLData();
$a{'statuses'} = \@statuses;
PopGlobalSQLState();
# Add the hash representing the attachment to the array of attachments.
push @attachments, \%a;
}
@ -491,10 +461,14 @@ sub insert
# Make existing attachments obsolete.
my $fieldid = GetFieldID('attachments.isobsolete');
foreach my $attachid (@{$::MFORM{'obsolete'}}) {
SendSQL("UPDATE attachments SET isobsolete = 1 WHERE attach_id = $attachid");
foreach my $obsolete_id (@{$::MFORM{'obsolete'}}) {
SendSQL("UPDATE attachments SET isobsolete = 1 WHERE attach_id = $obsolete_id");
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
VALUES ($::FORM{'bugid'}, $attachid, $::userid, NOW(), $fieldid, '0', '1')");
VALUES ($::FORM{'bugid'}, $obsolete_id, $::userid, NOW(), $fieldid, '0', '1')");
# If the obsolete attachment has pending flags, migrate them to the new attachment.
if (Bugzilla::Flag::count({ 'attach_id' => $obsolete_id , 'status' => 'pending' })) {
Bugzilla::Flag::migrate($obsolete_id, $attachid);
}
}
# Send mail to let people know the attachment has been created. Uses a
@ -544,32 +518,6 @@ sub edit
# !!! Yuck, what an ugly hack. Fix it!
my $isviewable = ( $contenttype =~ /^(text|image|application\/vnd\.mozilla\.)/ );
# Retrieve a list of status flags that have been set on the attachment.
my %statuses;
SendSQL("SELECT id, name
FROM attachstatuses JOIN attachstatusdefs
WHERE attachstatuses.statusid = attachstatusdefs.id
AND attach_id = $::FORM{'id'}");
while ( my ($id, $name) = FetchSQLData() )
{
$statuses{$id} = $name;
}
# Retrieve a list of statuses for this bug's product, and build an array
# of hashes in which each hash is a status flag record.
# ???: Move this into versioncache or its own routine?
my @statusdefs;
SendSQL("SELECT id, name
FROM attachstatusdefs, bugs
WHERE bug_id = $bugid
AND attachstatusdefs.product_id = bugs.product_id
ORDER BY sortkey");
while ( MoreSQLData() )
{
my ($id, $name) = FetchSQLData();
push @statusdefs, { 'id' => $id , 'name' => $name };
}
# Retrieve a list of attachments for this bug as well as a summary of the bug
# to use in a navigation bar across the top of the screen.
SendSQL("SELECT attach_id FROM attachments WHERE bug_id = $bugid ORDER BY attach_id");
@ -577,7 +525,20 @@ sub edit
push(@bugattachments, FetchSQLData()) while (MoreSQLData());
SendSQL("SELECT short_desc FROM bugs WHERE bug_id = $bugid");
my ($bugsummary) = FetchSQLData();
# Get a list of flag types that can be set for this attachment.
SendSQL("SELECT product_id, component_id FROM bugs WHERE bug_id = $bugid");
my ($product_id, $component_id) = FetchSQLData();
my $flag_types = Bugzilla::FlagType::match({ 'target_type' => 'attachment' ,
'product_id' => $product_id ,
'component_id' => $component_id ,
'is_active' => 1});
foreach my $flag_type (@$flag_types) {
$flag_type->{'flags'} = Bugzilla::Flag::match({ 'type_id' => $flag_type->{'id'},
'attach_id' => $::FORM{'id'} });
}
$vars->{'flag_types'} = $flag_types;
# Define the variables and functions that will be passed to the UI template.
$vars->{'attachid'} = $::FORM{'id'};
$vars->{'description'} = $description;
@ -589,8 +550,6 @@ sub edit
$vars->{'isobsolete'} = $isobsolete;
$vars->{'isprivate'} = $isprivate;
$vars->{'isviewable'} = $isviewable;
$vars->{'statuses'} = \%statuses;
$vars->{'statusdefs'} = \@statusdefs;
$vars->{'attachments'} = \@bugattachments;
# Return the appropriate HTTP response headers.
@ -604,7 +563,7 @@ sub edit
sub update
{
# Update an attachment record.
# Updates an attachment record.
# Get the bug ID for the bug to which this attachment is attached.
SendSQL("SELECT bug_id FROM attachments WHERE attach_id = $::FORM{'id'}");
@ -616,8 +575,11 @@ sub update
}
# Lock database tables in preparation for updating the attachment.
SendSQL("LOCK TABLES attachments WRITE , attachstatuses WRITE ,
attachstatusdefs READ , fielddefs READ , bugs_activity WRITE");
SendSQL("LOCK TABLES attachments WRITE , flags WRITE , " .
"flagtypes READ , fielddefs READ , bugs_activity WRITE, " .
"flaginclusions AS i READ, flagexclusions AS e READ, " .
"bugs READ, profiles READ");
# Get a copy of the attachment record before we make changes
# so we can record those changes in the activity table.
SendSQL("SELECT description, mimetype, filename, ispatch, isobsolete, isprivate
@ -625,41 +587,6 @@ sub update
my ($olddescription, $oldcontenttype, $oldfilename, $oldispatch,
$oldisobsolete, $oldisprivate) = FetchSQLData();
# Get the list of old status flags.
SendSQL("SELECT attachstatusdefs.name
FROM attachments, attachstatuses, attachstatusdefs
WHERE attachments.attach_id = $::FORM{'id'}
AND attachments.attach_id = attachstatuses.attach_id
AND attachstatuses.statusid = attachstatusdefs.id
ORDER BY attachstatusdefs.sortkey
");
my @oldstatuses;
while (MoreSQLData()) {
push(@oldstatuses, FetchSQLData());
}
my $oldstatuslist = join(', ', @oldstatuses);
# Update the database with the new status flags.
SendSQL("DELETE FROM attachstatuses WHERE attach_id = $::FORM{'id'}");
foreach my $statusid (@{$::MFORM{'status'}})
{
SendSQL("INSERT INTO attachstatuses (attach_id, statusid) VALUES ($::FORM{'id'}, $statusid)");
}
# Get the list of new status flags.
SendSQL("SELECT attachstatusdefs.name
FROM attachments, attachstatuses, attachstatusdefs
WHERE attachments.attach_id = $::FORM{'id'}
AND attachments.attach_id = attachstatuses.attach_id
AND attachstatuses.statusid = attachstatusdefs.id
ORDER BY attachstatusdefs.sortkey
");
my @newstatuses;
while (MoreSQLData()) {
push(@newstatuses, FetchSQLData());
}
my $newstatuslist = join(', ', @newstatuses);
# Quote the description and content type for use in the SQL UPDATE statement.
my $quoteddescription = SqlQuote($::FORM{'description'});
my $quotedcontenttype = SqlQuote($::FORM{'contenttype'});
@ -677,18 +604,23 @@ sub update
WHERE attach_id = $::FORM{'id'}
");
# Figure out when the changes were made.
SendSQL("SELECT NOW()");
my $timestamp = FetchOneColumn();
# Record changes in the activity table.
my $sql_timestamp = SqlQuote($timestamp);
if ($olddescription ne $::FORM{'description'}) {
my $quotedolddescription = SqlQuote($olddescription);
my $fieldid = GetFieldID('attachments.description');
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $quotedolddescription, $quoteddescription)");
VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $quotedolddescription, $quoteddescription)");
}
if ($oldcontenttype ne $::FORM{'contenttype'}) {
my $quotedoldcontenttype = SqlQuote($oldcontenttype);
my $fieldid = GetFieldID('attachments.mimetype');
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $quotedoldcontenttype, $quotedcontenttype)");
VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $quotedoldcontenttype, $quotedcontenttype)");
}
if ($oldfilename ne $::FORM{'filename'}) {
my $quotedoldfilename = SqlQuote($oldfilename);
@ -699,48 +631,26 @@ sub update
if ($oldispatch ne $::FORM{'ispatch'}) {
my $fieldid = GetFieldID('attachments.ispatch');
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $oldispatch, $::FORM{'ispatch'})");
VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $oldispatch, $::FORM{'ispatch'})");
}
if ($oldisobsolete ne $::FORM{'isobsolete'}) {
my $fieldid = GetFieldID('attachments.isobsolete');
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $oldisobsolete, $::FORM{'isobsolete'})");
VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $oldisobsolete, $::FORM{'isobsolete'})");
}
if ($oldisprivate ne $::FORM{'isprivate'}) {
my $fieldid = GetFieldID('attachments.isprivate');
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $oldisprivate, $::FORM{'isprivate'})");
}
if ($oldstatuslist ne $newstatuslist) {
my ($removed, $added) = DiffStrings($oldstatuslist, $newstatuslist);
my $quotedremoved = SqlQuote($removed);
my $quotedadded = SqlQuote($added);
my $fieldid = GetFieldID('attachstatusdefs.name');
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $quotedremoved, $quotedadded)");
}
# Update flags.
my $target = Bugzilla::Flag::GetTarget(undef, $::FORM{'id'});
Bugzilla::Flag::process($target, $timestamp, \%::FORM);
# Unlock all database tables now that we are finished updating the database.
SendSQL("UNLOCK TABLES");
# If this installation has enabled the request manager, let the manager know
# an attachment was updated so it can check for requests on that attachment
# and fulfill them. The request manager allows users to request database
# changes of other users and tracks the fulfillment of those requests. When
# an attachment record is updated and the request manager is called, it will
# fulfill those requests that were requested of the user performing the update
# which are requests for the attachment being updated.
#my $requests;
#if (Param('userequestmanager'))
#{
# use Request;
# # Specify the fieldnames that have been updated.
# my @fieldnames = ('description', 'mimetype', 'status', 'ispatch', 'isobsolete');
# # Fulfill pending requests.
# $requests = Request::fulfillRequest('attachment', $::FORM{'id'}, @fieldnames);
# $vars->{'requests'} = $requests;
#}
# If the user submitted a comment while editing the attachment,
# add the comment to the bug.
if ( $::FORM{'comment'} )
@ -772,10 +682,10 @@ sub update
my $neverused = $::userid;
# Append the comment to the list of comments in the database.
AppendComment($bugid, $who, $wrappedcomment, $::FORM{'isprivate'});
AppendComment($bugid, $who, $wrappedcomment, $::FORM{'isprivate'}, $timestamp);
}
# Send mail to let people know the bug has changed. Uses a special syntax
# of the "open" and "exec" commands to capture the output of "processmail",
# which "system" doesn't allow, without running the command through a shell,

Просмотреть файл

@ -28,6 +28,10 @@ use RelationSet;
# Use the Attachment module to display attachments for the bug.
use Attachment;
# Use the Flag modules to display flags on the bug.
use Bugzilla::Flag;
use Bugzilla::FlagType;
sub show_bug {
# Shut up misguided -w warnings about "used only once". For some reason,
# "use vars" chokes on me when I try it here.
@ -76,10 +80,10 @@ sub show_bug {
# Populate the bug hash with the info we get directly from the DB.
my $query = "
SELECT bugs.bug_id, alias, products.name, version, rep_platform,
op_sys, bug_status, resolution, priority,
bug_severity, components.name, assigned_to, reporter,
bug_file_loc, short_desc, target_milestone,
SELECT bugs.bug_id, alias, bugs.product_id, products.name, version,
rep_platform, op_sys, bug_status, resolution, priority,
bug_severity, bugs.component_id, components.name, assigned_to,
reporter, bug_file_loc, short_desc, target_milestone,
qa_contact, status_whiteboard,
date_format(creation_ts,'%Y-%m-%d %H:%i'),
delta_ts, sum(votes.count), delta_ts calc_disp_date
@ -101,12 +105,12 @@ sub show_bug {
my $value;
my $disp_date;
my @row = FetchSQLData();
foreach my $field ("bug_id", "alias", "product", "version", "rep_platform",
"op_sys", "bug_status", "resolution", "priority",
"bug_severity", "component", "assigned_to", "reporter",
"bug_file_loc", "short_desc", "target_milestone",
"qa_contact", "status_whiteboard", "creation_ts",
"delta_ts", "votes", "calc_disp_date")
foreach my $field ("bug_id", "alias", "product_id", "product", "version",
"rep_platform", "op_sys", "bug_status", "resolution",
"priority", "bug_severity", "component_id", "component",
"assigned_to", "reporter", "bug_file_loc", "short_desc",
"target_milestone", "qa_contact", "status_whiteboard",
"creation_ts", "delta_ts", "votes", "calc_disp_date")
{
$value = shift(@row);
if ($field eq "calc_disp_date") {
@ -197,6 +201,28 @@ sub show_bug {
# Attachments
$bug{'attachments'} = Attachment::query($id);
# The types of flags that can be set on this bug.
# If none, no UI for setting flags will be displayed.
my $flag_types =
Bugzilla::FlagType::match({ 'target_type' => 'bug',
'product_id' => $bug{'product_id'},
'component_id' => $bug{'component_id'},
'is_active' => 1 });
foreach my $flag_type (@$flag_types) {
$flag_type->{'flags'} =
Bugzilla::Flag::match({ 'bug_id' => $id ,
'target_type' => 'bug' });
}
$vars->{'flag_types'} = $flag_types;
# The number of types of flags that can be set on attachments
# to this bug. If none, flags won't be shown in the list of attachments.
$vars->{'num_attachment_flag_types'} =
Bugzilla::FlagType::count({ 'target_type' => 'a',
'product_id' => $bug{'product_id'},
'component_id' => $bug{'component_id'},
'is_active' => 1 });
# Dependencies
my @list;

Просмотреть файл

@ -1336,24 +1336,65 @@ $table{attachments} =
index(bug_id),
index(creation_ts)';
# 2001-05-05 myk@mozilla.org: Tables to support attachment statuses.
# "attachstatuses" stores one record for each status on each attachment.
# "attachstatusdefs" defines the statuses that can be set on attachments.
# September 2002 myk@mozilla.org: Tables to support status flags,
# which replace attachment statuses and allow users to flag bugs
# or attachments with statuses (review+, approval-, etc.).
#
# "flags" stores one record for each flag on each bug/attachment.
# "flagtypes" defines the types of flags that can be set.
# "flaginclusions" and "flagexclusions" specify the products/components
# a bug/attachment must belong to in order for flags of a given type
# to be set for them.
$table{attachstatuses} =
'
attach_id MEDIUMINT NOT NULL ,
statusid SMALLINT NOT NULL ,
PRIMARY KEY(attach_id, statusid)
$table{flags} =
'id MEDIUMINT NOT NULL PRIMARY KEY ,
type_id SMALLINT NOT NULL ,
status CHAR(1) NOT NULL ,
bug_id MEDIUMINT NOT NULL ,
attach_id MEDIUMINT NULL ,
creation_date DATETIME NOT NULL ,
modification_date DATETIME NULL ,
setter_id MEDIUMINT NULL ,
requestee_id MEDIUMINT NULL ,
INDEX(bug_id, attach_id) ,
INDEX(setter_id) ,
INDEX(requestee_id)
';
$table{attachstatusdefs} =
'
id SMALLINT NOT NULL PRIMARY KEY ,
name VARCHAR(50) NOT NULL ,
description MEDIUMTEXT NULL ,
sortkey SMALLINT NOT NULL DEFAULT 0 ,
product_id SMALLINT NOT NULL
$table{flagtypes} =
'id SMALLINT NOT NULL PRIMARY KEY ,
name VARCHAR(50) NOT NULL ,
description TEXT NULL ,
cc_list VARCHAR(200) NULL ,
target_type CHAR(1) NOT NULL DEFAULT \'b\' ,
is_active TINYINT NOT NULL DEFAULT 1 ,
is_requestable TINYINT NOT NULL DEFAULT 0 ,
is_requesteeble TINYINT NOT NULL DEFAULT 0 ,
is_multiplicable TINYINT NOT NULL DEFAULT 0 ,
sortkey SMALLINT NOT NULL DEFAULT 0
';
$table{flaginclusions} =
'type_id SMALLINT NOT NULL ,
product_id SMALLINT NULL ,
component_id SMALLINT NULL ,
INDEX(type_id, product_id, component_id)
';
$table{flagexclusions} =
'type_id SMALLINT NOT NULL ,
product_id SMALLINT NULL ,
component_id SMALLINT NULL ,
INDEX(type_id, product_id, component_id)
';
#
@ -1792,7 +1833,7 @@ AddFDef("attachments.mimetype", "Attachment mime type", 0);
AddFDef("attachments.ispatch", "Attachment is patch", 0);
AddFDef("attachments.isobsolete", "Attachment is obsolete", 0);
AddFDef("attachments.isprivate", "Attachment is private", 0);
AddFDef("attachstatusdefs.name", "Attachment Status", 0);
AddFDef("target_milestone", "Target Milestone", 0);
AddFDef("delta_ts", "Last changed date", 0);
AddFDef("(to_days(now()) - to_days(bugs.delta_ts))", "Days since bug changed",
@ -1807,6 +1848,10 @@ AddFDef("bug_group", "Group", 0);
# Oops. Bug 163299
$dbh->do("DELETE FROM fielddefs WHERE name='cc_accessible'");
AddFDef("flagtypes.name", "Flag", 0);
AddFDef("requesters.login_name", "Flag Requester", 0);
AddFDef("setters.login_name", "Flag Setter", 0);
###########################################################################
# Detect changed local settings
###########################################################################
@ -3246,6 +3291,133 @@ if (GetFieldDef("profiles", "groupset")) {
$dbh->do("DELETE FROM fielddefs WHERE name = " . $dbh->quote('groupset'));
}
# September 2002 myk@mozilla.org bug 98801
# Convert the attachment statuses tables into flags tables.
if (TableExists("attachstatuses") && TableExists("attachstatusdefs")) {
print "Converting attachment statuses to flags...\n";
# Get IDs for the old attachment status and new flag fields.
$sth = $dbh->prepare("SELECT fieldid FROM fielddefs " .
"WHERE name='attachstatusdefs.name'");
$sth->execute();
my $old_field_id = $sth->fetchrow_arrayref()->[0] || 0;
$sth = $dbh->prepare("SELECT fieldid FROM fielddefs " .
"WHERE name='flagtypes.name'");
$sth->execute();
my $new_field_id = $sth->fetchrow_arrayref()->[0];
# Convert attachment status definitions to flag types. If more than one
# status has the same name and description, it is merged into a single
# status with multiple inclusion records.
$sth = $dbh->prepare("SELECT id, name, description, sortkey, product_id " .
"FROM attachstatusdefs");
# status definition IDs indexed by name/description
my $def_ids = {};
# merged IDs and the IDs they were merged into. The key is the old ID,
# the value is the new one. This allows us to give statuses the right
# ID when we convert them over to flags. This map includes IDs that
# weren't merged (in this case the old and new IDs are the same), since
# it makes the code simpler.
my $def_id_map = {};
$sth->execute();
while (my ($id, $name, $desc, $sortkey, $prod_id) = $sth->fetchrow_array()) {
my $key = $name . $desc;
if (!$def_ids->{$key}) {
$def_ids->{$key} = $id;
my $quoted_name = $dbh->quote($name);
my $quoted_desc = $dbh->quote($desc);
$dbh->do("INSERT INTO flagtypes (id, name, description, sortkey, " .
"target_type) VALUES ($id, $quoted_name, $quoted_desc, " .
"$sortkey, 'a')");
}
$def_id_map->{$id} = $def_ids->{$key};
$dbh->do("INSERT INTO flaginclusions (type_id, product_id) " .
"VALUES ($def_id_map->{$id}, $prod_id)");
}
# Note: even though we've converted status definitions, we still can't drop
# the table because we need it to convert the statuses themselves.
# Convert attachment statuses to flags. To do this we select the statuses
# from the status table and then, for each one, figure out who set it
# and when they set it from the bugs activity table.
my $id = 0;
$sth = $dbh->prepare("SELECT attachstatuses.attach_id, attachstatusdefs.id, " .
"attachstatusdefs.name, attachments.bug_id " .
"FROM attachstatuses, attachstatusdefs, attachments " .
"WHERE attachstatuses.statusid = attachstatusdefs.id " .
"AND attachstatuses.attach_id = attachments.attach_id");
# a query to determine when the attachment status was set and who set it
my $sth2 = $dbh->prepare("SELECT added, who, bug_when " .
"FROM bugs_activity " .
"WHERE bug_id = ? AND attach_id = ? " .
"AND fieldid = $old_field_id " .
"ORDER BY bug_when DESC");
$sth->execute();
while (my ($attach_id, $def_id, $status, $bug_id) = $sth->fetchrow_array()) {
++$id;
# Determine when the attachment status was set and who set it.
# We should always be able to find out this info from the bug activity,
# but we fall back to default values just in case.
$sth2->execute($bug_id, $attach_id);
my ($added, $who, $when);
while (($added, $who, $when) = $sth2->fetchrow_array()) {
last if $added =~ /(^|[, ]+)\Q$status\E([, ]+|$)/;
}
$who = $dbh->quote($who); # "NULL" by default if $who is undefined
$when = $when ? $dbh->quote($when) : "NOW()";
$dbh->do("INSERT INTO flags (id, type_id, status, bug_id, attach_id, " .
"creation_date, modification_date, requestee_id, setter_id) " .
"VALUES ($id, $def_id_map->{$def_id}, '+', $bug_id, " .
"$attach_id, $when, $when, NULL, $who)");
}
# Now that we've converted both tables we can drop them.
$dbh->do("DROP TABLE attachstatuses");
$dbh->do("DROP TABLE attachstatusdefs");
# Convert activity records for attachment statuses into records for flags.
my $sth = $dbh->prepare("SELECT attach_id, who, bug_when, added, removed " .
"FROM bugs_activity WHERE fieldid = $old_field_id");
$sth->execute();
while (my ($attach_id, $who, $when, $old_added, $old_removed) =
$sth->fetchrow_array())
{
my @additions = split(/[, ]+/, $old_added);
@additions = map("$_+", @additions);
my $new_added = $dbh->quote(join(", ", @additions));
my @removals = split(/[, ]+/, $old_removed);
@removals = map("$_+", @removals);
my $new_removed = $dbh->quote(join(", ", @removals));
$old_added = $dbh->quote($old_added);
$old_removed = $dbh->quote($old_removed);
$who = $dbh->quote($who);
$when = $dbh->quote($when);
$dbh->do("UPDATE bugs_activity SET fieldid = $new_field_id, " .
"added = $new_added, removed = $new_removed " .
"WHERE attach_id = $attach_id AND who = $who " .
"AND bug_when = $when AND fieldid = $old_field_id " .
"AND added = $old_added AND removed = $old_removed");
}
# Remove the attachment status field from the field definitions.
$dbh->do("DELETE FROM fielddefs WHERE name='attachstatusdefs.name'");
print "done.\n";
}
# If you had to change the --TABLE-- definition in any way, then add your
# differential change code *** A B O V E *** this comment.
#

Просмотреть файл

@ -581,7 +581,9 @@ if ($action eq 'delete') {
bugs WRITE,
bugs_activity WRITE,
components WRITE,
dependencies WRITE");
dependencies WRITE,
flaginclusions WRITE,
flagexclusions WRITE");
# According to MySQL doc I cannot do a DELETE x.* FROM x JOIN Y,
# so I have to iterate over bugs and delete all the indivial entries
@ -610,6 +612,12 @@ if ($action eq 'delete') {
print "Bugs deleted.<BR>\n";
}
SendSQL("DELETE FROM flaginclusions
WHERE component_id=$component_id");
SendSQL("DELETE FROM flagexclusions
WHERE component_id=$component_id");
print "Flag inclusions and exclusions deleted.<BR>\n";
SendSQL("DELETE FROM components
WHERE id=$component_id");
print "Components deleted.<P>\n";

Просмотреть файл

@ -0,0 +1,494 @@
#!/usr/bonsaitools/bin/perl -wT
# -*- 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>
################################################################################
# Script Initialization
################################################################################
# Make it harder for us to do dangerous things in Perl.
use strict;
use lib ".";
# Include the Bugzilla CGI and general utility library.
require "CGI.pl";
# Establish a connection to the database backend.
ConnectToDatabase();
# Use Bugzilla's flag modules for handling flag types.
use Bugzilla::Flag;
use Bugzilla::FlagType;
use vars qw( $template $vars );
# Make sure the user is logged in and is an administrator.
confirm_login();
UserInGroup("editcomponents")
|| ThrowUserError("authorization_failure",
{ action => "administer flag types" });
# Suppress "used only once" warnings.
use vars qw(@legal_product @legal_components %components);
my $product_id;
my $component_id;
################################################################################
# Main Body Execution
################################################################################
# All calls to this script should contain an "action" variable whose value
# determines what the user wants to do. The code below checks the value of
# that variable and runs the appropriate code.
# Determine whether to use the action specified by the user or the default.
my $action = $::FORM{'action'} || 'list';
if ($::FORM{'categoryAction'}) {
processCategoryChange();
exit;
}
if ($action eq 'list') { list(); }
elsif ($action eq 'enter') { edit(); }
elsif ($action eq 'copy') { edit(); }
elsif ($action eq 'edit') { edit(); }
elsif ($action eq 'insert') { insert(); }
elsif ($action eq 'update') { update(); }
elsif ($action eq 'confirmdelete') { confirmDelete(); }
elsif ($action eq 'delete') { &delete(); }
elsif ($action eq 'deactivate') { deactivate(); }
else {
ThrowCodeError("action_unrecognized", { action => $action });
}
exit;
################################################################################
# Functions
################################################################################
sub list {
# Define the variables and functions that will be passed to the UI template.
$vars->{'bug_types'} = Bugzilla::FlagType::match({ 'target_type' => 'bug' }, 1);
$vars->{'attachment_types'} =
Bugzilla::FlagType::match({ 'target_type' => 'attachment' }, 1);
# Return the appropriate HTTP response headers.
print "Content-type: text/html\n\n";
# Generate and return the UI (HTML page) from the appropriate template.
$template->process("admin/flag-type/list.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
}
sub edit {
$action eq 'enter' ? validateTargetType() : validateID();
# Get this installation's products and components.
GetVersionTable();
# products and components and the function used to modify the components
# menu when the products menu changes; used by the template to populate
# the menus and keep the components menu consistent with the products menu
$vars->{'products'} = \@::legal_product;
$vars->{'components'} = \@::legal_components;
$vars->{'components_by_product'} = \%::components;
$vars->{'last_action'} = $::FORM{'action'};
if ($::FORM{'action'} eq 'enter' || $::FORM{'action'} eq 'copy') {
$vars->{'action'} = "insert";
}
else {
$vars->{'action'} = "update";
}
# If copying or editing an existing flag type, retrieve it.
if ($::FORM{'action'} eq 'copy' || $::FORM{'action'} eq 'edit') {
$vars->{'type'} = Bugzilla::FlagType::get($::FORM{'id'});
$vars->{'type'}->{'inclusions'} = Bugzilla::FlagType::get_inclusions($::FORM{'id'});
$vars->{'type'}->{'exclusions'} = Bugzilla::FlagType::get_exclusions($::FORM{'id'});
}
# Otherwise set the target type (the minimal information about the type
# that the template needs to know) from the URL parameter.
else {
$vars->{'type'} = { 'target_type' => $::FORM{'target_type'} };
}
# Return the appropriate HTTP response headers.
print "Content-type: text/html\n\n";
# Generate and return the UI (HTML page) from the appropriate template.
$template->process("admin/flag-type/edit.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
}
sub processCategoryChange {
validateIsActive();
validateIsRequestable();
validateIsRequesteeble();
validateAllowMultiple();
my @inclusions = $::MFORM{'inclusions'} ? @{$::MFORM{'inclusions'}} : ();
my @exclusions = $::MFORM{'exclusions'} ? @{$::MFORM{'exclusions'}} : ();
if ($::FORM{'categoryAction'} eq "Include") {
validateProduct();
validateComponent();
my $category = ($::FORM{'product'} || "__Any__") . ":" . ($::FORM{'component'} || "__Any__");
push(@inclusions, $category) unless grep($_ eq $category, @inclusions);
}
elsif ($::FORM{'categoryAction'} eq "Exclude") {
validateProduct();
validateComponent();
my $category = ($::FORM{'product'} || "__Any__") . ":" . ($::FORM{'component'} || "__Any__");
push(@exclusions, $category) unless grep($_ eq $category, @exclusions);
}
elsif ($::FORM{'categoryAction'} eq "Remove Inclusion") {
@inclusions = map(($_ eq $::FORM{'inclusion_to_remove'} ? () : $_), @inclusions);
}
elsif ($::FORM{'categoryAction'} eq "Remove Exclusion") {
@exclusions = map(($_ eq $::FORM{'exclusion_to_remove'} ? () : $_), @exclusions);
}
# Get this installation's products and components.
GetVersionTable();
# products and components; used by the template to populate the menus
# and keep the components menu consistent with the products menu
$vars->{'products'} = \@::legal_product;
$vars->{'components'} = \@::legal_components;
$vars->{'components_by_product'} = \%::components;
$vars->{'action'} = $::FORM{'action'};
my $type = {};
foreach my $key (keys %::FORM) { $type->{$key} = $::FORM{$key} }
$type->{'inclusions'} = \@inclusions;
$type->{'exclusions'} = \@exclusions;
$vars->{'type'} = $type;
# Return the appropriate HTTP response headers.
print "Content-type: text/html\n\n";
# Generate and return the UI (HTML page) from the appropriate template.
$template->process("admin/flag-type/edit.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
}
sub insert {
validateName();
validateDescription();
validateCCList();
validateTargetType();
validateSortKey();
validateIsActive();
validateIsRequestable();
validateIsRequesteeble();
validateAllowMultiple();
my $name = SqlQuote($::FORM{'name'});
my $description = SqlQuote($::FORM{'description'});
my $cc_list = SqlQuote($::FORM{'cc_list'});
my $target_type = $::FORM{'target_type'} eq "bug" ? "b" : "a";
SendSQL("LOCK TABLES flagtypes WRITE, products READ, components READ, " .
"flaginclusions WRITE, flagexclusions WRITE");
# Determine the new flag type's unique identifier.
SendSQL("SELECT MAX(id) FROM flagtypes");
my $id = FetchSQLData() + 1;
# Insert a record for the new flag type into the database.
SendSQL("INSERT INTO flagtypes (id, name, description, cc_list,
target_type, sortkey, is_active, is_requestable,
is_requesteeble, is_multiplicable)
VALUES ($id, $name, $description, $cc_list, '$target_type',
$::FORM{'sortkey'}, $::FORM{'is_active'},
$::FORM{'is_requestable'}, $::FORM{'is_requesteeble'},
$::FORM{'is_multiplicable'})");
# Populate the list of inclusions/exclusions for this flag type.
foreach my $category_type ("inclusions", "exclusions") {
foreach my $category (@{$::MFORM{$category_type}}) {
my ($product, $component) = split(/:/, $category);
my $product_id = get_product_id($product) || "NULL";
my $component_id =
get_component_id($product_id, $component) || "NULL";
SendSQL("INSERT INTO flag$category_type (type_id, product_id, " .
"component_id) VALUES ($id, $product_id, $component_id)");
}
}
SendSQL("UNLOCK TABLES");
$vars->{'name'} = $::FORM{'name'};
$vars->{'message'} = "flag_type_created";
# Return the appropriate HTTP response headers.
print "Content-type: text/html\n\n";
# Generate and return the UI (HTML page) from the appropriate template.
$template->process("global/message.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
}
sub update {
validateID();
validateName();
validateDescription();
validateCCList();
validateTargetType();
validateSortKey();
validateIsActive();
validateIsRequestable();
validateIsRequesteeble();
validateAllowMultiple();
my $name = SqlQuote($::FORM{'name'});
my $description = SqlQuote($::FORM{'description'});
my $cc_list = SqlQuote($::FORM{'cc_list'});
SendSQL("LOCK TABLES flagtypes WRITE, products READ, components READ, " .
"flaginclusions WRITE, flagexclusions WRITE");
SendSQL("UPDATE flagtypes
SET name = $name ,
description = $description ,
cc_list = $cc_list ,
sortkey = $::FORM{'sortkey'} ,
is_active = $::FORM{'is_active'} ,
is_requestable = $::FORM{'is_requestable'} ,
is_requesteeble = $::FORM{'is_requesteeble'} ,
is_multiplicable = $::FORM{'is_multiplicable'}
WHERE id = $::FORM{'id'}");
# Update the list of inclusions/exclusions for this flag type.
foreach my $category_type ("inclusions", "exclusions") {
SendSQL("DELETE FROM flag$category_type WHERE type_id = $::FORM{'id'}");
foreach my $category (@{$::MFORM{$category_type}}) {
my ($product, $component) = split(/:/, $category);
my $product_id = get_product_id($product) || "NULL";
my $component_id =
get_component_id($product_id, $component) || "NULL";
SendSQL("INSERT INTO flag$category_type (type_id, product_id, " .
"component_id) VALUES ($::FORM{'id'}, $product_id, " .
"$component_id)");
}
}
SendSQL("UNLOCK TABLES");
# Clear existing flags for bugs/attachments in categories no longer on
# the list of inclusions or that have been added to the list of exclusions.
SendSQL("
SELECT flags.id
FROM flags, bugs LEFT OUTER 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 flags.type_id = $::FORM{'id'}
AND flags.bug_id = bugs.bug_id
AND i.type_id IS NULL
");
Bugzilla::Flag::clear(FetchOneColumn()) while MoreSQLData();
SendSQL("
SELECT flags.id
FROM flags, bugs, flagexclusions AS e
WHERE flags.type_id = $::FORM{'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)
");
Bugzilla::Flag::clear(FetchOneColumn()) while MoreSQLData();
$vars->{'name'} = $::FORM{'name'};
$vars->{'message'} = "flag_type_changes_saved";
# Return the appropriate HTTP response headers.
print "Content-type: text/html\n\n";
# Generate and return the UI (HTML page) from the appropriate template.
$template->process("global/message.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
}
sub confirmDelete
{
validateID();
# check if we need confirmation to delete:
my $count = Bugzilla::Flag::count({ 'type_id' => $::FORM{'id'} });
if ($count > 0) {
$vars->{'flag_type'} = Bugzilla::FlagType::get($::FORM{'id'});
$vars->{'flag_count'} = scalar($count);
# Return the appropriate HTTP response headers.
print "Content-type: text/html\n\n";
# Generate and return the UI (HTML page) from the appropriate template.
$template->process("admin/flag-type/confirm-delete.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
}
else {
deleteType();
}
}
sub delete {
validateID();
SendSQL("LOCK TABLES flagtypes WRITE, flags WRITE, " .
"flaginclusions WRITE, flagexclusions WRITE");
# Get the name of the flag type so we can tell users
# what was deleted.
SendSQL("SELECT name FROM flagtypes WHERE id = $::FORM{'id'}");
$vars->{'name'} = FetchOneColumn();
SendSQL("DELETE FROM flags WHERE type_id = $::FORM{'id'}");
SendSQL("DELETE FROM flaginclusions WHERE type_id = $::FORM{'id'}");
SendSQL("DELETE FROM flagexclusions WHERE type_id = $::FORM{'id'}");
SendSQL("DELETE FROM flagtypes WHERE id = $::FORM{'id'}");
SendSQL("UNLOCK TABLES");
$vars->{'message'} = "flag_type_deleted";
# Return the appropriate HTTP response headers.
print "Content-type: text/html\n\n";
# Generate and return the UI (HTML page) from the appropriate template.
$template->process("global/message.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
}
sub deactivate {
validateID();
validateIsActive();
SendSQL("LOCK TABLES flagtypes WRITE");
SendSQL("UPDATE flagtypes SET is_active = 0 WHERE id = $::FORM{'id'}");
SendSQL("UNLOCK TABLES");
$vars->{'message'} = "flag_type_deactivated";
$vars->{'flag_type'} = Bugzilla::FlagType::get($::FORM{'id'});
# Return the appropriate HTTP response headers.
print "Content-type: text/html\n\n";
# Generate and return the UI (HTML page) from the appropriate template.
$template->process("global/message.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
}
################################################################################
# Data Validation / Security Authorization
################################################################################
sub validateID {
detaint_natural($::FORM{'id'})
|| ThrowCodeError("flag_type_id_invalid", { id => $::FORM{'id'} });
SendSQL("SELECT 1 FROM flagtypes WHERE id = $::FORM{'id'}");
FetchOneColumn()
|| ThrowCodeError("flag_type_nonexistent", { id => $::FORM{'id'} });
}
sub validateName {
$::FORM{'name'}
&& length($::FORM{'name'}) <= 50
|| ThrowUserError("flag_type_name_invalid", { name => $::FORM{'name'} });
}
sub validateDescription {
length($::FORM{'description'}) < 2^16-1
|| ThrowUserError("flag_type_description_invalid");
}
sub validateCCList {
length($::FORM{'cc_list'}) <= 200
|| ThrowUserError("flag_type_cc_list_invalid",
{ cc_list => $::FORM{'cc_list'} });
my @addresses = split(/[, ]+/, $::FORM{'cc_list'});
foreach my $address (@addresses) { CheckEmailSyntax($address) }
}
sub validateProduct {
return if !$::FORM{'product'};
$product_id = get_product_id($::FORM{'product'});
defined($product_id)
|| ThrowCodeError("flag_type_product_nonexistent",
{ product => $::FORM{'product'} });
}
sub validateComponent {
return if !$::FORM{'component'};
$product_id
|| ThrowCodeError("flag_type_component_without_product");
$component_id = get_component_id($product_id, $::FORM{'component'});
defined($component_id)
|| ThrowCodeError("flag_type_component_nonexistent",
{ product => $::FORM{'product'},
component => $::FORM{'component'} });
}
sub validateSortKey {
detaint_natural($::FORM{'sortkey'})
&& $::FORM{'sortkey'} < 32768
|| ThrowUserError("flag_type_sortkey_invalid",
{ sortkey => $::FORM{'sortkey'} });
}
sub validateTargetType {
grep($::FORM{'target_type'} eq $_, ("bug", "attachment"))
|| ThrowCodeError("flag_type_target_type_invalid",
{ target_type => $::FORM{'target_type'} });
}
sub validateIsActive {
$::FORM{'is_active'} = $::FORM{'is_active'} ? 1 : 0;
}
sub validateIsRequestable {
$::FORM{'is_requestable'} = $::FORM{'is_requestable'} ? 1 : 0;
}
sub validateIsRequesteeble {
$::FORM{'is_requesteeble'} = $::FORM{'is_requesteeble'} ? 1 : 0;
}
sub validateAllowMultiple {
$::FORM{'is_multiplicable'} = $::FORM{'is_multiplicable'} ? 1 : 0;
}

Просмотреть файл

@ -539,7 +539,9 @@ if ($action eq 'delete') {
products WRITE,
groups WRITE,
profiles WRITE,
milestones WRITE");
milestones WRITE,
flaginclusions WRITE,
flagexclusions WRITE);
# According to MySQL doc I cannot do a DELETE x.* FROM x JOIN Y,
# so I have to iterate over bugs and delete all the indivial entries
@ -581,6 +583,12 @@ if ($action eq 'delete') {
WHERE product_id=$product_id");
print "Milestones deleted.<BR>\n";
SendSQL("DELETE FROM flaginclusions
WHERE product_id=$product_id");
SendSQL("DELETE FROM flagexclusions
WHERE product_id=$product_id");
print "Flag inclusions and exclusions deleted.<BR>\n";
SendSQL("DELETE FROM products
WHERE id=$product_id");
print "Product '$product' deleted.<BR>\n";

Просмотреть файл

@ -300,7 +300,12 @@ sub FetchOneColumn {
"status", "resolution", "summary");
sub AppendComment {
my ($bugid,$who,$comment,$isprivate) = (@_);
my ($bugid, $who, $comment, $isprivate, $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()");
$comment =~ s/\r\n/\n/g; # Get rid of windows-style line endings.
$comment =~ s/\r/\n/g; # Get rid of mac-style line endings.
if ($comment =~ /^\s*$/) { # Nothin' but whitespace.
@ -310,7 +315,7 @@ sub AppendComment {
my $whoid = DBNameToIdAndCheck($who);
my $privacyval = $isprivate ? 1 : 0 ;
SendSQL("INSERT INTO longdescs (bug_id, who, bug_when, thetext, isprivate) " .
"VALUES($bugid, $whoid, now(), " . SqlQuote($comment) . ", " .
"VALUES($bugid, $whoid, $timestamp, " . SqlQuote($comment) . ", " .
$privacyval . ")");
SendSQL("UPDATE bugs SET delta_ts = now() WHERE bug_id = $bugid");
@ -902,8 +907,7 @@ sub get_product_name {
sub get_component_id {
my ($prod_id, $comp) = @_;
die "non-numeric prod_id '$prod_id' passed to get_component_id"
unless ($prod_id =~ /^\d+$/);
return undef unless ($prod_id =~ /^\d+$/);
PushGlobalSQLState();
SendSQL("SELECT id FROM components " .
"WHERE product_id = $prod_id AND name = " . SqlQuote($comp));

Просмотреть файл

@ -36,6 +36,9 @@ require "bug_form.pl";
use RelationSet;
# Use the Flag module to modify flag data if the user set flags.
use Bugzilla::Flag;
# Shut up misguided -w warnings about "used only once":
use vars qw(%versions
@ -1052,8 +1055,9 @@ foreach my $id (@idlist) {
"profiles $write, dependencies $write, votes $write, " .
"products READ, components READ, " .
"keywords $write, longdescs $write, fielddefs $write, " .
"bug_group_map $write, " .
"user_group_map READ, " .
"bug_group_map $write, flags $write, " .
"user_group_map READ, flagtypes READ, " .
"flaginclusions AS i READ, flagexclusions AS e READ, " .
"keyworddefs READ, groups READ, attachments READ");
my @oldvalues = SnapShotBug($id);
my %oldhash;
@ -1238,7 +1242,7 @@ foreach my $id (@idlist) {
LogActivityEntry($id, "bug_group", $groupDelNames, $groupAddNames);
if (defined $::FORM{'comment'}) {
AppendComment($id, $::COOKIE{'Bugzilla_login'}, $::FORM{'comment'},
$::FORM{'commentprivacy'});
$::FORM{'commentprivacy'}, $timestamp);
}
my $removedCcString = "";
@ -1399,6 +1403,14 @@ foreach my $id (@idlist) {
# what has changed since before we wrote out the new values.
#
my @newvalues = SnapShotBug($id);
my %newhash;
$i = 0;
foreach my $col (@::log_columns) {
# Consider NULL db entries to be equivalent to the empty string
$newvalues[$i] ||= '';
$newhash{$col} = $newvalues[$i];
$i++;
}
# for passing to processmail to ensure that when someone is removed
# from one of these fields, they get notified of that fact (if desired)
@ -1411,12 +1423,6 @@ foreach my $id (@idlist) {
# values in place.
my $old = shift @oldvalues;
my $new = shift @newvalues;
if (!defined $old) {
$old = "";
}
if (!defined $new) {
$new = "";
}
if ($old ne $new) {
# Products and components are now stored in the DB using ID's
@ -1461,6 +1467,11 @@ foreach my $id (@idlist) {
LogActivityEntry($id,$col,$old,$new);
}
}
# Set and update flags.
if ($UserInEditGroupSet) {
my $target = Bugzilla::Flag::GetTarget($id);
Bugzilla::Flag::process($target, $timestamp, \%::FORM);
}
if ($bug_changed) {
SendSQL("UPDATE bugs SET delta_ts = " . SqlQuote($timestamp) . " WHERE bug_id = $id");
}

Просмотреть файл

@ -0,0 +1,242 @@
// Adds to the target select object all elements in array that
// correspond to the elements selected in source.
// - array should be a array of arrays, indexed by product name. the
// array should contain the elements that correspont to that
// product. Example:
// var array = Array();
// array['ProductOne'] = [ 'ComponentA', 'ComponentB' ];
// updateSelect(array, source, target);
// - sel is a list of selected items, either whole or a diff
// depending on sel_is_diff.
// - sel_is_diff determines if we are sending in just a diff or the
// whole selection. a diff is used to optimize adding selections.
// - target should be the target select object.
// - single specifies if we selected a single item. if we did, no
// need to merge.
function updateSelect( array, sel, target, sel_is_diff, single, blank ) {
var i, j, comp;
// if single, even if it's a diff (happens when you have nothing
// selected and select one item alone), skip this.
if ( ! single ) {
// array merging/sorting in the case of multiple selections
if ( sel_is_diff ) {
// merge in the current options with the first selection
comp = merge_arrays( array[sel[0]], target.options, 1 );
// merge the rest of the selection with the results
for ( i = 1 ; i < sel.length ; i++ ) {
comp = merge_arrays( array[sel[i]], comp, 0 );
}
} else {
// here we micro-optimize for two arrays to avoid merging with a
// null array
comp = merge_arrays( array[sel[0]],array[sel[1]], 0 );
// merge the arrays. not very good for multiple selections.
for ( i = 2; i < sel.length; i++ ) {
comp = merge_arrays( comp, array[sel[i]], 0 );
}
}
} else {
// single item in selection, just get me the list
comp = array[sel[0]];
}
// save the selection in the target select so we can restore it later
var selections = new Array();
for ( i = 0; i < target.options.length; i++ )
if (target.options[i].selected) selections.push(target.options[i].value);
// clear select
target.options.length = 0;
// add empty "Any" value back to the list
if (blank) target.options[0] = new Option( blank, "" );
// load elements of list into select
for ( i = 0; i < comp.length; i++ ) {
target.options[target.options.length] = new Option( comp[i], comp[i] );
}
// restore the selection
for ( i=0 ; i<selections.length ; i++ )
for ( j=0 ; j<target.options.length ; j++ )
if (target.options[j].value == selections[i]) target.options[j].selected = true;
}
// Returns elements in a that are not in b.
// NOT A REAL DIFF: does not check the reverse.
// - a,b: arrays of values to be compare.
function fake_diff_array( a, b ) {
var newsel = new Array();
// do a boring array diff to see who's new
for ( var ia in a ) {
var found = 0;
for ( var ib in b ) {
if ( a[ia] == b[ib] ) {
found = 1;
}
}
if ( ! found ) {
newsel[newsel.length] = a[ia];
}
found = 0;
}
return newsel;
}
// takes two arrays and sorts them by string, returning a new, sorted
// array. the merge removes dupes, too.
// - a, b: arrays to be merge.
// - b_is_select: if true, then b is actually an optionitem and as
// such we need to use item.value on it.
function merge_arrays( a, b, b_is_select ) {
var pos_a = 0;
var pos_b = 0;
var ret = new Array();
var bitem, aitem;
// iterate through both arrays and add the larger item to the return
// list. remove dupes, too. Use toLowerCase to provide
// case-insensitivity.
while ( ( pos_a < a.length ) && ( pos_b < b.length ) ) {
if ( b_is_select ) {
bitem = b[pos_b].value;
} else {
bitem = b[pos_b];
}
aitem = a[pos_a];
// smaller item in list a
if ( aitem.toLowerCase() < bitem.toLowerCase() ) {
ret[ret.length] = aitem;
pos_a++;
} else {
// smaller item in list b
if ( aitem.toLowerCase() > bitem.toLowerCase() ) {
ret[ret.length] = bitem;
pos_b++;
} else {
// list contents are equal, inc both counters.
ret[ret.length] = aitem;
pos_a++;
pos_b++;
}
}
}
// catch leftovers here. these sections are ugly code-copying.
if ( pos_a < a.length ) {
for ( ; pos_a < a.length ; pos_a++ ) {
ret[ret.length] = a[pos_a];
}
}
if ( pos_b < b.length ) {
for ( ; pos_b < b.length; pos_b++ ) {
if ( b_is_select ) {
bitem = b[pos_b].value;
} else {
bitem = b[pos_b];
}
ret[ret.length] = bitem;
}
}
return ret;
}
// selectProduct reads the selection from f[productfield] and updates
// f.version, component and target_milestone accordingly.
// - f: a form containing product, component, varsion and
// target_milestone select boxes.
// globals (3vil!):
// - cpts, vers, tms: array of arrays, indexed by product name. the
// subarrays contain a list of names to be fed to the respective
// selectboxes. For bugzilla, these are generated with perl code
// at page start.
// - usetms: this is a global boolean that is defined if the
// bugzilla installation has it turned on. generated in perl too.
// - first_load: boolean, specifying if it's the first time we load
// the query page.
// - last_sel: saves our last selection list so we know what has
// changed, and optimize for additions.
function selectProduct( f , productfield, componentfield, blank ) {
// this is to avoid handling events that occur before the form
// itself is ready, which happens in buggy browsers.
if ( ( !f ) || ( ! f[productfield] ) ) {
return;
}
// if this is the first load and nothing is selected, no need to
// merge and sort all components; perl gives it to us sorted.
if ( ( first_load ) && ( f[productfield].selectedIndex == -1 ) ) {
first_load = 0;
return;
}
// turn first_load off. this is tricky, since it seems to be
// redundant with the above clause. It's not: if when we first load
// the page there is _one_ element selected, it won't fall into that
// clause, and first_load will remain 1. Then, if we unselect that
// item, selectProduct will be called but the clause will be valid
// (since selectedIndex == -1), and we will return - incorrectly -
// without merge/sorting.
first_load = 0;
// - sel keeps the array of products we are selected.
// - is_diff says if it's a full list or just a list of products that
// were added to the current selection.
// - single indicates if a single item was selected
var sel = Array();
var is_diff = 0;
var single;
// if nothing selected, pick all
if ( f[productfield].selectedIndex == -1 ) {
for ( var i = 0 ; i < f[productfield].length ; i++ ) {
sel[sel.length] = f[productfield].options[i].value;
}
single = 0;
} else {
for ( i = 0 ; i < f[productfield].length ; i++ ) {
if ( f[productfield].options[i].selected ) {
sel[sel.length] = f[productfield].options[i].value;
}
}
single = ( sel.length == 1 );
// save last_sel before we kill it
var tmp = last_sel;
last_sel = sel;
// this is an optimization: if we've added components, no need
// to remerge them; just merge the new ones with the existing
// options.
if ( ( tmp ) && ( tmp.length < sel.length ) ) {
sel = fake_diff_array(sel, tmp);
is_diff = 1;
}
}
// do the actual fill/update
updateSelect( cpts, sel, f[componentfield], is_diff, single, blank );
}

279
webtools/bugzilla/request.cgi Executable file
Просмотреть файл

@ -0,0 +1,279 @@
#!/usr/bonsaitools/bin/perl -wT
# -*- 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>
################################################################################
# Script Initialization
################################################################################
# Make it harder for us to do dangerous things in Perl.
use diagnostics;
use strict;
# Include the Bugzilla CGI and general utility library.
use lib qw(.);
require "CGI.pl";
# Establish a connection to the database backend.
ConnectToDatabase();
# Use Bugzilla's Request module which contains utilities for handling requests.
use Bugzilla::Flag;
use Bugzilla::FlagType;
# use Bugzilla's User module which contains utilities for handling users.
use Bugzilla::User;
use vars qw($template $vars @legal_product @legal_components %components);
# Make sure the user is logged in.
quietly_check_login();
################################################################################
# Main Body Execution
################################################################################
queue();
exit;
################################################################################
# Functions
################################################################################
sub queue {
validateStatus();
validateGroup();
my $attach_join_clause = "flags.attach_id = attachments.attach_id";
if (Param("insidergroup") && !UserInGroup(Param("insidergroup"))) {
$attach_join_clause .= " AND attachment.isprivate < 1";
}
my $query =
# Select columns describing each flag, the bug/attachment on which
# it has been set, who set it, and of whom they are requesting it.
" SELECT flags.id, flagtypes.name,
flags.status,
flags.bug_id, bugs.short_desc,
products.name, components.name,
flags.attach_id, attachments.description,
requesters.realname, requesters.login_name,
requestees.realname, requestees.login_name,
flags.creation_date,
" .
# Select columns that help us weed out secure bugs to which the user
# should not have access.
" COUNT(DISTINCT ugmap.group_id) AS cntuseringroups,
COUNT(DISTINCT bgmap.group_id) AS cntbugingroups,
((COUNT(DISTINCT ccmap.who) AND cclist_accessible)
OR ((bugs.reporter = $::userid) AND bugs.reporter_accessible)
OR bugs.assigned_to = $::userid ) AS canseeanyway
" .
# Use the flags and flagtypes tables for information about the flags,
# the bugs and attachments tables for target info, the profiles tables
# for setter and requestee info, the products/components tables
# so we can display product and component names, and the bug_group_map
# and user_group_map tables to help us weed out secure bugs to which
# the user should not have access.
" FROM flags
LEFT JOIN attachments ON ($attach_join_clause),
flagtypes,
profiles AS requesters
LEFT JOIN profiles AS requestees
ON flags.requestee_id = requestees.userid,
bugs
LEFT JOIN products ON bugs.product_id = products.id
LEFT JOIN components ON bugs.component_id = components.id
LEFT JOIN bug_group_map AS bgmap
ON bgmap.bug_id = bugs.bug_id
LEFT JOIN user_group_map AS ugmap
ON bgmap.group_id = ugmap.group_id
AND ugmap.user_id = $::userid
AND ugmap.isbless = 0
LEFT JOIN cc AS ccmap
ON ccmap.who = $::userid AND ccmap.bug_id = bugs.bug_id
" .
# All of these are inner join clauses. Actual match criteria are added
# in the code below.
" WHERE flags.type_id = flagtypes.id
AND flags.setter_id = requesters.userid
AND flags.bug_id = bugs.bug_id
";
# A list of columns to exclude from the report because the report conditions
# limit the data being displayed to exact matches for those columns.
# In other words, if we are only displaying "pending" , we don't
# need to display a "status" column in the report because the value for that
# column will always be the same.
my @excluded_columns = ();
# Filter requests by status: "pending", "granted", "denied", "all"
# (which means any), or "fulfilled" (which means "granted" or "denied").
$::FORM{'status'} ||= "?";
if ($::FORM{'status'} eq "+-") {
$query .= " AND flags.status IN ('+', '-')";
}
elsif ($::FORM{'status'} ne "all") {
$query .= " AND flags.status = '$::FORM{'status'}'";
push(@excluded_columns, 'status');
}
# Filter results by exact email address of requester or requestee.
if (defined($::FORM{'requester'}) && $::FORM{'requester'} ne "") {
$query .= " AND requesters.login_name = " . SqlQuote($::FORM{'requester'});
push(@excluded_columns, 'requester');
}
if (defined($::FORM{'requestee'}) && $::FORM{'requestee'} ne "") {
$query .= " AND requestees.login_name = " . SqlQuote($::FORM{'requestee'});
push(@excluded_columns, 'requestee');
}
# Filter results by exact product or component.
if (defined($::FORM{'product'}) && $::FORM{'product'} ne "") {
my $product_id = get_product_id($::FORM{'product'});
if ($product_id) {
$query .= " AND bugs.product_id = $product_id";
push(@excluded_columns, 'product');
if (defined($::FORM{'component'}) && $::FORM{'component'} ne "") {
my $component_id = get_component_id($product_id, $::FORM{'component'});
if ($component_id) {
$query .= " AND bugs.component_id = $component_id";
push(@excluded_columns, 'component');
}
else { ThrowCodeError("unknown_component", { %::FORM }) }
}
}
else { ThrowCodeError("unknown_product", { %::FORM }) }
}
# Filter results by flag types.
if (defined($::FORM{'type'}) && !grep($::FORM{'type'} eq $_, ("", "all"))) {
# Check if any matching types are for attachments. If not, don't show
# the attachment column in the report.
my $types = Bugzilla::FlagType::match({ 'name' => $::FORM{'type'} });
my $has_attachment_type = 0;
foreach my $type (@$types) {
if ($type->{'target_type'} eq "attachment") {
$has_attachment_type = 1;
last;
}
}
if (!$has_attachment_type) { push(@excluded_columns, 'attachment') }
$query .= " AND flagtypes.name = " . SqlQuote($::FORM{'type'});
push(@excluded_columns, 'type');
}
# Group the records by flag ID so we don't get multiple rows of data
# for each flag. This is only necessary because of the code that
# removes flags on bugs the user is unauthorized to access.
$query .= " GROUP BY flags.id " .
"HAVING cntuseringroups = cntbugingroups OR canseeanyway ";
# Group the records, in other words order them by the group column
# so the loop in the display template can break them up into separate
# tables every time the value in the group column changes.
$::FORM{'group'} ||= "requestee";
if ($::FORM{'group'} eq "requester") {
$query .= " ORDER BY requesters.realname, requesters.login_name";
}
elsif ($::FORM{'group'} eq "requestee") {
$query .= " ORDER BY requestees.realname, requestees.login_name";
}
elsif ($::FORM{'group'} eq "category") {
$query .= " ORDER BY products.name, components.name";
}
elsif ($::FORM{'group'} eq "type") {
$query .= " ORDER BY flagtypes.name";
}
# Order the records (within each group).
$query .= " , flags.creation_date";
# Pass the query to the template for use when debugging this script.
$vars->{'query'} = $query;
SendSQL($query);
my @requests = ();
while (MoreSQLData()) {
my @data = FetchSQLData();
my $request = {
'id' => $data[0] ,
'type' => $data[1] ,
'status' => $data[2] ,
'bug_id' => $data[3] ,
'bug_summary' => $data[4] ,
'category' => "$data[5]: $data[6]" ,
'attach_id' => $data[7] ,
'attach_summary' => $data[8] ,
'requester' => ($data[9] ? "$data[9] <$data[10]>" : $data[10]) ,
'requestee' => ($data[11] ? "$data[11] <$data[12]>" : $data[12]) ,
'created' => $data[13]
};
push(@requests, $request);
}
# Get a list of request type names to use in the filter form.
my @types = ("all");
SendSQL("SELECT DISTINCT(name) FROM flagtypes ORDER BY name");
push(@types, FetchOneColumn()) while MoreSQLData();
# products and components and the function used to modify the components
# menu when the products menu changes; used by the template to populate
# the menus and keep the components menu consistent with the products menu
GetVersionTable();
$vars->{'products'} = \@::legal_product;
$vars->{'components'} = \@::legal_components;
$vars->{'components_by_product'} = \%::components;
$vars->{'excluded_columns'} = \@excluded_columns;
$vars->{'group_field'} = $::FORM{'group'};
$vars->{'requests'} = \@requests;
$vars->{'form'} = \%::FORM;
$vars->{'types'} = \@types;
# Return the appropriate HTTP response headers.
print "Content-type: text/html\n\n";
# Generate and return the UI (HTML page) from the appropriate template.
$template->process("request/queue.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
}
################################################################################
# Data Validation / Security Authorization
################################################################################
sub validateStatus {
return if !defined($::FORM{'status'});
grep($::FORM{'status'} eq $_, qw(? +- + - all))
|| ThrowCodeError("flag_status_invalid", { status => $::FORM{'status'} });
}
sub validateGroup {
return if !defined($::FORM{'group'});
grep($::FORM{'group'} eq $_, qw(requester requestee category type))
|| ThrowCodeError("request_queue_group_invalid",
{ group => $::FORM{'group'} });
}

Просмотреть файл

@ -232,11 +232,11 @@ CrossCheck("fielddefs", "fieldid",
["bugs_activity", "fieldid"]);
CrossCheck("attachments", "attach_id",
["attachstatuses", "attach_id"],
["flags", "attach_id"],
["bugs_activity", "attach_id"]);
CrossCheck("attachstatusdefs", "id",
["attachstatuses", "statusid"]);
CrossCheck("flagtypes", "id",
["flags", "type_id"]);
CrossCheck("bugs", "bug_id",
["bugs_activity", "bug_id"],
@ -280,7 +280,7 @@ CrossCheck("products", "id",
["components", "product_id", "name"],
["milestones", "product_id", "value"],
["versions", "product_id", "value"],
["attachstatusdefs", "product_id", "name"]);
["flagtypes", "product_id", "name"]);
DateCheck("groups", "last_changed");
DateCheck("profiles", "refreshed_when");

Просмотреть файл

@ -83,9 +83,27 @@
<tr>
<td width="150"></td>
<td>
<label for="ExcludeSelf">Only email me reports of changes made by other people</label>
<input type="checkbox" name="ExcludeSelf" id="ExcludeSelf" value="on"
[% " checked" IF excludeself %]>
<label for="ExcludeSelf">Only email me reports of changes made by other people</label>
<br>
</td>
</tr>
<tr>
<td width="150"></td>
<td>
<input type="checkbox" name="FlagRequestee" id="FlagRequestee" value="on"
[% " checked" IF FlagRequestee %]>
<label for="FlagRequestee">Email me when someone asks me to set a flag</label>
<br>
</td>
</tr>
<tr>
<td width="150"></td>
<td>
<input type="checkbox" name="FlagRequester" id="FlagRequester" value="on"
[% " checked" IF FlagRequester %]>
<label for="FlagRequester">Email me when someone sets a flag I asked for</label>
<br>
</td>
</tr>

Просмотреть файл

@ -0,0 +1,58 @@
<!-- 1.0@bugzilla.org -->
[%# 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>
#%]
[%# Filter off the name here to be used multiple times below %]
[% name = BLOCK %][% flag_type.name FILTER html %][% END %]
[% PROCESS global/header.html.tmpl
title = "Confirm Deletion of Flag Type '$name'"
%]
<p>
There are [% flag_count %] flags of type [% name %].
If you delete this type, those flags will also be deleted. Note that
instead of deleting the type you can
<a href="editflagtypes.cgi?action=deactivate&id=[% flag_type.id %]">deactivate it</a>,
in which case the type and its flags will remain in the database
but will not appear in the Bugzilla UI.
</p>
<table>
<tr>
<td colspan=2>
Do you really want to delete this type?
</td>
</tr>
<tr>
<td>
<a href="editflagtypes.cgi?action=delete&id=[% flag_type.id %]">
Yes, delete
</a>
</td>
<td align="right">
<a href="editflagtypes.cgi">
No, don't delete
</a>
</td>
</tr>
</table>
[% PROCESS global/footer.html.tmpl %]

Просмотреть файл

@ -0,0 +1,189 @@
[%# 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>
#%]
[%# The javascript and header_html blocks get used in header.html.tmpl. %]
[% javascript = BLOCK %]
var usetms = 0; // do we have target milestone?
var first_load = 1; // is this the first time we load the page?
var last_sel = []; // caches last selection
var cpts = new Array();
[% FOREACH p = products %]
cpts['[% p FILTER js %]'] = [
[%- FOREACH item = components_by_product.$p %]'[% item FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
[% END %]
[% END %]
[% header_html = BLOCK %]
<script language="JavaScript" type="text/javascript" src="productmenu.js"></script>
[% END %]
[% IF type.target_type == "bug" %]
[% title = "Create Flag Type for Bugs" %]
[% ELSE %]
[% title = "Create Flag Type for Attachments" %]
[% END %]
[% IF last_action == "copy" %]
[% title = "Create Flag Type Based on $type.name" %]
[% ELSIF last_action == "edit" %]
[% title = "Edit Flag Type $type.name" %]
[% END %]
[% PROCESS global/header.html.tmpl
title = title
style = "
table#form th { text-align: right; vertical-align: baseline; white-space: nowrap; }
table#form td { text-align: left; vertical-align: baseline; }
"
onload="selectProduct(forms[0], 'product', 'component', '__Any__');"
%]
<form method="post" action="editflagtypes.cgi">
<input type="hidden" name="action" value="[% action %]">
<input type="hidden" name="id" value="[% type.id %]">
<input type="hidden" name="target_type" value="[% type.target_type %]">
[% FOREACH category = type.inclusions %]
<input type="hidden" name="inclusions" value="[% category %]">
[% END %]
[% FOREACH category = type.exclusions %]
<input type="hidden" name="exclusions" value="[% category %]">
[% END %]
<table id="form" cellspacing="0" cellpadding="4" border="0">
<tr>
<th>Name:</th>
<td>
a short name identifying this type<br>
<input type="text" name="name" value="[% type.name FILTER html %]"
size="50" maxlength="50">
</td>
</tr>
<tr>
<th>Description:</th>
<td>
a comprehensive description of this type<br>
<textarea name="description" rows="4" cols="80">[% type.description FILTER html %]</textarea>
</td>
</tr>
<tr>
<th>Category:</th>
<td>
the products/components to which [% type.target_type %]s must
(inclusions) or must not (exclusions) belong in order for users
to be able to set flags of this type for them
<table>
<tr>
<td style="vertical-align: top;">
<b>Product/Component:</b><br>
<select name="product" onChange="selectProduct(this.form, 'product', 'component', '__Any__');">
<option value="">__Any__</option>
[% FOREACH item = products %]
<option value="[% item %]" [% "selected" IF type.product.name == item %]>[% item %]</option>
[% END %]
</select><br>
<select name="component">
<option value="">__Any__</option>
[% FOREACH item = components %]
<option value="[% item %]" [% "selected" IF type.component.name == item %]>[% item %]</option>
[% END %]
</select><br>
<input type="submit" name="categoryAction" value="Include">
<input type="submit" name="categoryAction" value="Exclude">
</td>
<td style="vertical-align: top;">
<b>Inclusions:</b><br>
[% PROCESS "global/select-menu.html.tmpl" name="inclusion_to_remove" multiple=1 size=4 options=type.inclusions %]<br>
<input type="submit" name="categoryAction" value="Remove Inclusion">
</td>
<td style="vertical-align: top;">
<b>Exclusions:</b><br>
[% PROCESS "global/select-menu.html.tmpl" name="exclusion_to_remove" multiple=1 size=4 options=type.exclusions %]<br>
<input type="submit" name="categoryAction" value="Remove Exclusion">
</td>
</tr>
</table>
</td>
</tr>
<tr>
<th>Sort Key:</th>
<td>
a number between 1 and 32767 by which this type will be sorted
when displayed to users in a list; ignore if you don't care
what order the types appear in or if you want them to appear
in alphabetical order<br>
<input type="text" name="sortkey" value="[% type.sortkey || 1 %]" size="5" maxlength="5">
</td>
</tr>
<tr>
<th>&nbsp;</th>
<td>
<input type="checkbox" name="is_active" [% "checked" IF type.is_active || !type.is_active.defined %]>
active (flags of this type appear in the UI and can be set)
</td>
</tr>
<tr>
<th>&nbsp;</th>
<td>
<input type="checkbox" name="is_requestable" [% "checked" IF type.is_requestable || !type.is_requestable.defined %]>
requestable (users can ask for flags of this type to be set)
</td>
</tr>
<tr>
<th>CC List:</th>
<td>
if requestable, who should get carbon copied on email notification of requests<br>
<input type="text" name="cc_list" value="[% type.cc_list FILTER html %]" size="80" maxlength="200">
</td>
</tr>
<tr>
<th>&nbsp;</th>
<td>
<input type="checkbox" name="is_requesteeble" [% "checked" IF type.is_requesteeble || !type.is_requesteeble.defined %]>
specifically requestable (users can ask specific other users to set flags of this type as opposed to just asking the wind)
</td>
</tr>
<tr>
<th>&nbsp;</th>
<td>
<input type="checkbox" name="is_multiplicable" [% "checked" IF type.is_multiplicable || !type.is_multiplicable.defined %]>
multiplicable (multiple flags of this type can be set on the same [% type.target_type %])
</td>
</tr>
<tr>
<th></th>
<td>
<input type="submit" value="[% (last_action == "enter" || last_action == "copy") ? "Create" : "Save Changes" %]">
</td>
</tr>
</table>
</form>
[% PROCESS global/footer.html.tmpl %]

Просмотреть файл

@ -0,0 +1,107 @@
[%# 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>
#%]
[% PROCESS global/header.html.tmpl
title = 'Administer Flag Types'
style = "
table#flag_types tr th { text-align: left; }
.inactive { color: #787878; }
"
%]
<p>
Flags are markers that identify whether a bug or attachment has been granted
or denied some status. Flags appear in the UI as a name and a status symbol
("+" for granted, "-" for denied, and "?" for statuses requested by users).
</p>
<p>
For example, you might define a "review" status for users to request review
for their patches. When a patch writer requests review, the string "review?"
will appear in the attachment. When a patch reviewer reviews the patch,
either the string "review+" or the string "review-" will appear in the patch,
depending on whether the patch passed or failed review.
</p>
<h3>Flag Types for Bugs</h3>
[% PROCESS display_flag_types types=bug_types %]
<p>
<a href="editflagtypes.cgi?action=enter&target_type=bug">Create Flag Type for Bugs</a>
</p>
<h3>Flag Types for Attachments</h3>
[% PROCESS display_flag_types types=attachment_types %]
<p>
<a href="editflagtypes.cgi?action=enter&target_type=attachment">Create Flag Type For Attachments</a>
</p>
<script language="JavaScript">
<!--
function confirmDelete(id, name, count)
{
if (count > 0) {
var msg = 'There are ' + count + ' flags of type ' + name + '. ' +
'If you delete this type, those flags will also be ' +
'deleted.\n\nNote: to deactivate the type instead ' +
'of deleting it, edit it and uncheck its "is active" ' +
'flag.\n\nDo you really want to delete this flag type?';
if (!confirm(msg)) return false;
}
location.href = "editflagtypes.cgi?action=delete&id=" + id;
return false; // prevent strict JavaScript warning that this function
// does not always return a value
}
//-->
</script>
[% PROCESS global/footer.html.tmpl %]
[% BLOCK display_flag_types %]
<table id="flag_types" cellspacing="0" cellpadding="4" border="1">
<tr>
<th>Name</th>
<th>Description</th>
<th>Actions</th>
</tr>
[% FOREACH type = types %]
<tr class="[% type.is_active ? "active" : "inactive" %]">
<td>[% type.name FILTER html %]</td>
<td>[% type.description FILTER html %]</td>
<td>
<a href="editflagtypes.cgi?action=edit&id=[% type.id %]">Edit</a>
| <a href="editflagtypes.cgi?action=copy&id=[% type.id %]">Copy</a>
| <a href="editflagtypes.cgi?action=confirmdelete&id=[% type.id %]"
onclick="return confirmDelete([% type.id %], '[% type.name FILTER js %]',
[% type.flag_count %]);">Delete</a>
</td>
</tr>
[% END %]
</table>
[% END %]

Просмотреть файл

@ -32,6 +32,8 @@
table.attachment_info th { text-align: right; vertical-align: top; }
table.attachment_info td { text-align: left; vertical-align: top; }
#noview { text-align: left; vertical-align: center; }
table#flags th, table#flags td { font-size: small; vertical-align: baseline; }
"
%]
@ -158,8 +160,7 @@
<b>MIME Type:</b><br>
<input type="text" size="20" name="contenttypeentry" value="[% contenttype FILTER html %]"><br>
<b>Flags:</b><br>
<input type="checkbox" id="ispatch" name="ispatch" value="1"
[% 'checked="checked"' IF ispatch %]>
<label for="ispatch">patch</label>
@ -168,20 +169,14 @@
<label for="isobsolete">obsolete</label><br>
[% IF (Param("insidergroup") && UserInGroup(Param("insidergroup"))) %]
<input type="checkbox" name="isprivate" value="1"[% " checked" IF isprivate %]> private<br><br>
[% ELSE %]<br>
[% END %]
[% IF statusdefs.size %]
<b>Status:</b><br>
[% FOREACH def = statusdefs %]
<input type="checkbox" id="status-[% def.id %]" name="status"
value="[% def.id %]"
[% 'checked="checked"' IF statuses.${def.id} %]>
<label for="status-[% def.id %]">
[% def.name FILTER html %]
</label><br>
[% END %]
[% IF flag_types.size > 0 %]
<b>Flags:</b><br>
[% PROCESS "flag/list.html.tmpl" bug_id=bugid attach_id=attachid %]<br>
[% END %]
<div id="smallCommentFrame">
<b>Comment (on the bug):</b><br>
<textarea name="comment" rows="5" cols="25" wrap="soft"></textarea><br>

Просмотреть файл

@ -19,13 +19,18 @@
# Contributor(s): Myk Melez <myk@mozilla.org>
#%]
[%# Whether or not to include flags. %]
[% display_flags = num_attachment_flag_types > 0 %]
<br>
<table cellspacing="0" cellpadding="4" border="1">
<tr>
<th bgcolor="#cccccc" align="left">Attachment</th>
<th bgcolor="#cccccc" align="left">Type</th>
<th bgcolor="#cccccc" align="left">Created</th>
<th bgcolor="#cccccc" align="left">Status</th>
[% IF display_flags %]
<th bgcolor="#cccccc" align="left">Flags</th>
[% END %]
<th bgcolor="#cccccc" align="left">Actions</th>
</tr>
[% canseeprivate = !Param("insidergroup") || UserInGroup(Param("insidergroup")) %]
@ -50,16 +55,24 @@
<td valign="top">[% attachment.date %]</td>
<td valign="top">
[% IF attachment.statuses.size == 0 %]
<i>none</i>
[% ELSE %]
[% FOREACH s = attachment.statuses %]
[% s FILTER html FILTER replace('\s', '&nbsp;') %]<br>
[% IF display_flags %]
<td valign="top">
[% IF attachment.flags.size == 0 %]
<i>none</i>
[% ELSE %]
[% FOR flag = attachment.flags %]
[% IF flag.setter %]
[% flag.setter.nick FILTER html %]:
[% END %]
[%+ flag.type.name %][% flag.status %]
[%+ IF flag.status == "?" && flag.requestee %]
([% flag.requestee.nick %])
[% END %]<br>
[% END %]
[% END %]
[% END %]
</td>
</td>
[% END %]
<td valign="top">
[% IF attachment.canedit %]
<a href="attachment.cgi?id=[% attachment.attachid %]&amp;action=edit">Edit</a>
@ -72,7 +85,7 @@
[% END %]
<tr>
<td colspan="4">
<td colspan="[% display_flags ? 4 : 3 %]">
<a href="attachment.cgi?bugid=[% bugid %]&amp;action=enter">Create a New Attachment</a> (proposed patch, testcase, etc.)
</td>
<td colspan="1">

Просмотреть файл

@ -194,7 +194,7 @@
[% END %]
</tr>
[%# *** QAContact URL Summary Whiteboard Keywords *** %]
[%# *** QAContact URL Requests Summary Whiteboard Keywords *** %]
[% IF Param('useqacontact') %]
<tr>
@ -218,17 +218,23 @@
[% END %]
</b>
</td>
<td colspan="7">
<td colspan="5">
<input name="bug_file_loc" accesskey="u"
value="[% bug.bug_file_loc FILTER html %]" size="60">
</td>
<td rowspan="4" colspan="2" valign="top">
[% IF flag_types.size > 0 %]
<b>Flags:</b><br>
[% PROCESS "flag/list.html.tmpl" %]
[% END %]
</td>
</tr>
<tr>
<td align="right">
<b><u>S</u>ummary:</b>
</td>
<td colspan="7">
<td colspan="5">
<input name="short_desc" accesskey="s"
value="[% bug.short_desc FILTER html %]" size="60">
</td>
@ -239,7 +245,7 @@
<td align="right">
<b>Status <u>W</u>hiteboard:</b>
</td>
<td colspan="7">
<td colspan="5">
<input name="status_whiteboard" accesskey="w"
value="[% bug.status_whiteboard FILTER html %]" size="60">
</td>
@ -252,7 +258,7 @@
<b>
<a href="describekeywords.cgi"><u>K</u>eywords</a>:
</b>
<td colspan="7">
<td colspan="5">
<input name="keywords" accesskey="k"
value="[% bug.keywords.join(', ') FILTER html %]" size="60">
</td>
@ -263,8 +269,8 @@
[%# *** Attachments *** %]
[% PROCESS attachment/list.html.tmpl
attachments = bug.attachments
bugid = bug.bug_id %]
attachments = bug.attachments
bugid = bug.bug_id %]
[%# *** Dependencies Votes *** %]

Просмотреть файл

@ -0,0 +1,94 @@
[%# 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>
#%]
<table id="flags">
[% FOREACH type = flag_types %]
[% FOREACH flag = type.flags %]
<tr>
<td>
[% flag.setter.nick FILTER html %]:
</td>
<td>
[% type.name FILTER html %]
</td>
<td>
<select name="flag-[% flag.id %]">
<option value="X"></option>
<option value="+" [% "selected" IF flag.status == "+" %]>+</option>
<option value="-" [% "selected" IF flag.status == "-" %]>-</option>
<option value="?" [% "selected" IF flag.status == "?" %]>?</option>
</select>
</td>
<td>
[% IF flag.status == "?" && flag.requestee %]([% flag.requestee.nick FILTER html %])[% END %]
</td>
</tr>
[% END %]
[% IF !type.flags || type.flags.size == 0 %]
<tr>
<td>&nbsp;</td>
<td>[% type.name %]</td>
<td>
<select name="flag_type-[% type.id %]">
<option value="X"></option>
<option value="+">+</option>
<option value="-">-</option>
[% IF type.is_requestable %]
<option value="?">?</option>
[% END %]
</select>
</td>
<td>
[% IF type.is_requestable && type.is_requesteeble %]
(<input type="text" name="requestee-[% type.id %]" size="8" maxlength="255">)
[% END %]
</td>
</tr>
[% END %]
[% END %]
[% FOREACH type = flag_types %]
[% NEXT UNLESS type.flags.size > 0 && type.is_multiplicable %]
[% IF !separator_displayed %]
<tr><td colspan="3"><hr></td></tr>
[% separator_displayed = 1 %]
[% END %]
<tr>
<td colspan="2">addl. [% type.name %]</td>
<td>
<select name="flag_type-[% type.id %]">
<option value="X"></option>
<option value="+">+</option>
<option value="-">-</option>
[% IF type.is_requestable %]
<option value="?">?</option>
[% END %]
</select>
</td>
<td>
[% IF type.is_requestable && type.is_requesteeble %]
(<input type="text" name="requestee-[% type.id %]" size="8" maxlength="255">)
[% END %]
</td>
</tr>
[% END %]
</table>

Просмотреть файл

@ -40,6 +40,10 @@
to any [% parameters %] which you may have set before calling
ThrowCodeError.
[% ELSIF error == "action_unrecognized" %]
I don't recognize the value (<em>[% variables.action FILTER html %]</em>)
of the <em>action</em> variable.
[% ELSIF error == "attachment_already_obsolete" %]
Attachment #[% attachid FILTER html %] ([% description FILTER html %])
is already obsolete.
@ -78,10 +82,40 @@
[% ELSIF error == "no_bug_data" %]
No data when fetching bug [% bug_id %].
[% ELSIF error == "flag_nonexistent" %]
There is no flag with ID #[% variables.id %].
[% ELSIF error == "flag_status_invalid" %]
The flag status <em>[% variables.status FILTER html %]</em> is invalid.
[% ELSIF error == "flag_type_component_nonexistent" %]
The component <em>[% variables.component FILTER html %] does not exist
in the product <em>[% variables.product FILTER html %]</em>.
[% ELSIF error == "flag_type_component_without_product" %]
A component was selected without a product being selected.
[% ELSIF error == "flag_type_id_invalid" %]
The flag type ID <em>[% variables.id FILTER html %]</em> is not
a positive integer.
[% ELSIF error == "flag_type_nonexistent" %]
There is no flag type with the ID <em>[% variables.id %]</em>.
[% ELSIF error == "flag_type_product_nonexistent" %]
The product <em>[% variables.product FILTER html %]</em> does not exist.
[% ELSIF error == "flag_type_target_type_invalid" %]
The target type was neither <em>bug</em> nor <em>attachment</em>
but rather <em>[% variables.target_type FILTER html %]</em>.
[% ELSIF error == "no_y_axis_defined" %]
No Y axis was defined when creating report. The X axis is optional,
but the Y axis is compulsory.
[% ELSIF error == "request_queue_group_invalid" %]
The group field <em>[% group FILTER html %]</em> is invalid.
[% ELSIF error == "template_error" %]
[% template_error_msg %]
@ -91,6 +125,14 @@
[% ELSIF error == "unknown_action" %]
Unknown action [% action FILTER html %]!
[% ELSIF error == "unknown_component" %]
[% title = "Unknown Component" %]
There is no component named <em>[% variables.component FILTER html %]</em>.
[% ELSIF error == "unknown_product" %]
[% title = "Unknown Product" %]
There is no product named <em>[% variables.product FILTER html %]</em>.
[% ELSE %]
[%# Give sensible error if error functions are used incorrectly.
#%]

Просмотреть файл

@ -81,6 +81,34 @@
[% title = "Password Changed" %]
Your password has been changed.
[% ELSIF message_tag == "flag_type_created" %]
[% title = "Flag Type Created" %]
The flag type <em>[% name FILTER html %]</em> has been created.
<a href="editflagtypes.cgi">Back to flag types.</a>
[% ELSIF message_tag == "flag_type_changes_saved" %]
[% title = "Flag Type Changes Saved" %]
<p>
Your changes to the flag type <em>[% name FILTER html %]</em>
have been saved.
<a href="editflagtypes.cgi">Back to flag types.</a>
</p>
[% ELSIF message_tag == "flag_type_deleted" %]
[% title = "Flag Type Deleted" %]
<p>
The flag type <em>[% name FILTER html %]</em> has been deleted.
<a href="editflagtypes.cgi">Back to flag types.</a>
</p>
[% ELSIF message_tag == "flag_type_deactivated" %]
[% title = "Flag Type Deactivated" %]
<p>
The flag type <em>[% flag_type.name FILTER html %]</em>
has been deactivated.
<a href="editflagtypes.cgi">Back to flag types.</a>
</p>
[% ELSIF message_tag == "shutdown" %]
[% title = "Bugzilla is Down" %]
[% Param("shutdownhtml") %]

Просмотреть файл

@ -22,12 +22,18 @@
[%# INTERFACE:
# name: string; the name of the menu.
#
# multiple: boolean; whether or not the menu is multi-select
#
# size: integer; if multi-select, the number of items to display at once
#
# options: array or hash; the items with which to populate the array.
# If a hash is passed, the hash keys become the names displayed
# to the user while the hash values become the value of the item.
#
# default: string; the item selected in the menu by default.
#
# onchange: code; JavaScript to be run when the user changes the value
# selected in the menu.
#%]
[%# Get the scalar representation of the options reference,
@ -37,7 +43,9 @@
#%]
[% options_type = BLOCK %][% options %][% END %]
<select name="[% name FILTER html %]">
<select name="[% name FILTER html %]"
[% IF onchange %]onchange="[% onchange %]"[% END %]
[% IF multiple %] multiple [% IF size %] size="[% size %]" [% END %] [% END %]>
[% IF options_type.search("ARRAY") %]
[% FOREACH value = options %]
<option value="[% value FILTER html %]"
@ -45,7 +53,7 @@
[% value FILTER html %]
</option>
[% END %]
[% ELSIF values_type.search("HASH") %]
[% ELSIF options_type.search("HASH") %]
[% FOREACH option = options %]
<option value="[% option.value FILTER html %]"
[% " selected" IF option.value == default %]>

Просмотреть файл

@ -50,6 +50,8 @@
<input name="id" size="6"> |
<a href="reports.cgi">Reports</a>
| <a href="request.cgi">Requests</a>
[% IF user.login && Param('usevotes') %]
| <a href="votes.cgi?action=show_user">My Votes</a>
@ -68,7 +70,7 @@
|| user.canblessany %]
[% ', <a href="editproducts.cgi">products</a>'
IF user.groups.editcomponents %]
[% ', <a href="editattachstatuses.cgi"> attachment&nbsp;statuses</a>'
[% ', <a href="editflagtypes.cgi">flags</a>'
IF user.groups.editcomponents %]
[% ', <a href="editgroups.cgi">groups</a>'
IF user.groups.creategroups %]

Просмотреть файл

@ -30,7 +30,7 @@
#%]
[% DEFAULT title = "Error" %]
[% error_message = BLOCK %]
[% IF error == "aaa_example_error_tag" %]
[% title = "Example Error" %]
@ -75,6 +75,10 @@
Bug aliases cannot be longer than 20 characters.
Please choose a shorter alias.
[% ELSIF error == "authorization_failure" %]
[% title = "Authorization Failed" %]
You are not allowed to [% action %].
[% ELSIF error == "attachment_access_denied" %]
[% title = "Access Denied" %]
You are not permitted access to this attachment.
@ -129,6 +133,23 @@
format like JPG or PNG, or put it elsewhere on the web and
link to it from the bug's URL field or in a comment on the bug.
[% ELSIF error == "flag_type_cc_list_invalid" %]
[% title = "Flag Type CC List Invalid" %]
The CC list [% cc_list FILTER html %] must be less than 200 characters long.
[% ELSIF error == "flag_type_description_invalid" %]
[% title = "Flag Type Description Invalid" %]
The description must be less than 32K.
[% ELSIF error == "flag_type_name_invalid" %]
[% title = "Flag Type Name Invalid" %]
The name <em>[% name FILTER html %]</em> must be 1-50 characters long.
[% ELSIF error == "flag_type_sortkey_invalid" %]
[% title = "Flag Type Sort Key Invalid" %]
The sort key must be an integer between 0 and 32767 inclusive.
It cannot be <em>[% variables.sortkey %]</em>.
[% ELSIF error == "illegal_at_least_x_votes" %]
[% title = "Your Query Makes No Sense" %]
The <em>At least ___ votes</em> field must be a simple number.
@ -176,10 +197,6 @@
[% title = "Invalid Attachment ID" %]
The attachment id [% attach_id FILTER html %] is invalid.
[% ELSIF error == "invalid_attach_status" %]
[% title = "Invalid Attachment Status" %]
One of the statuses you entered is not a valid status for this attachment.
[% ELSIF error == "invalid_content_type" %]
[% title = "Invalid Content-Type" %]
The content type <em>[% contenttype FILTER html %]</em> is invalid.
@ -281,6 +298,18 @@
intentionally cleared out the "Reassign bug to"
field, [% Param("browserbugmessage") %]
[% ELSIF error == "requestee_too_short" %]
[% title = "Requestee Name Too Short" %]
One or two characters match too many users, so please enter at least
three characters of the name/email address of the user you want to set
the flag.
[% ELSIF error == "requestee_too_many_matches" %]
[% title = "Requestee String Matched Too Many Times" %]
The string <em>[% requestee FILTER html %]</em> matched more than
100 users. Enter more of the name to bring the number of matches
down to a reasonable amount.
[% ELSIF error == "unknown_keyword" %]
[% title = "Unknown Keyword" %]
<code>[% keyword FILTER html %]</code> is not a known keyword.

Просмотреть файл

@ -0,0 +1,41 @@
[%# 1.0@bugzilla.org %]
[%# 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>
#%]
From: bugzilla-request-daemon
To: [% flag.requestee.email IF flag.requestee.email_prefs.FlagRequestee %]
CC: [% flag.type.cc_list %]
Subject: [% flag.type.name %]: [Bug [% flag.target.bug.id %]] [% flag.target.bug.summary %]
[%- IF flag.target.attachment.exists %] :
[Attachment [% flag.target.attachment.id %]] [% flag.target.attachment.summary %][% END %]
[%+ USE wrap -%]
[%- FILTER bullet = wrap(80) -%]
[% flag.setter.identity %] has asked you for [% flag.type.name %] on bug #
[%- flag.target.bug.id %] ([% flag.target.bug.summary %])
[%- IF flag.target.attachment.exists %], attachment #
[%- flag.target.attachment.id %] ([% flag.target.attachment.summary %])[% END %].
[%+ IF flag.target.type == 'bug' -%]
[% Param('urlbase') %]show_bug.cgi?id=[% flag.target.bug.id %]
[%- ELSIF flag.target.type == 'attachment' -%]
[% Param('urlbase') %]attachment.cgi?id=[% flag.target.attachment.id %]&action=edit
[%- END %]
[%- END %]

Просмотреть файл

Просмотреть файл

@ -0,0 +1,193 @@
[%# 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>
#%]
[%# The javascript and header_html blocks get used in header.html.tmpl. %]
[% javascript = BLOCK %]
var usetms = 0; // do we have target milestone?
var first_load = 1; // is this the first time we load the page?
var last_sel = []; // caches last selection
var cpts = new Array();
[% FOREACH p = products %]
cpts['[% p FILTER js %]'] = [
[%- FOREACH item = components_by_product.$p %]'[% item FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
[% END %]
[% END %]
[% header_html = BLOCK %]
<script language="JavaScript" type="text/javascript" src="productmenu.js"></script>
[% END %]
[% PROCESS global/header.html.tmpl
title="Request Queue"
style = "
table.requests th { text-align: left; }
table#filter th { text-align: right; }
"
%]
[% column_headers = {
"type" => "Flag" ,
"status" => "Status" ,
"bug" => "Bug" ,
"attachment" => "Attachment" ,
"requester" => "Requester" ,
"requestee" => "Requestee" ,
"created" => "Created" ,
"category" => "Product/Component" } %]
[% DEFAULT display_columns = ["requester", "requestee", "type", "bug", "attachment", "created"]
group_field = "Requestee"
group_value = ""
%]
[% IF requests.size == 0 %]
<p>
No requests.
</p>
[% ELSE %]
[% FOREACH request = requests %]
[% PROCESS start_new_table IF request.$group_field != group_value %]
<tr>
[% FOREACH column = display_columns %]
[% NEXT IF column == group_field || excluded_columns.contains(column) %]
<td>[% PROCESS "display_$column" %]</td>
[% END %]
</tr>
[% END %]
</table>
[% END %]
<h3>Filter the Queue</h3>
<form action="request.cgi" method="get">
<input type="hidden" name="action" value="queue">
<table id="filter">
<tr>
<th>Requester:</th>
<td><input type="text" name="requester" value="[% form.requester FILTER html %]" size="20"></td>
<th>Product:</th>
<td>
<select name="product" onChange="selectProduct(this.form, 'product', 'component', 'Any');">
<option value="">Any</option>
[% FOREACH item = products %]
<option value="[% item FILTER html %]"
[% "selected" IF form.product == item %]>[% item FILTER html %]</option>
[% END %]
</select>
</td>
<th>Flag:</th>
<td>
[% PROCESS "global/select-menu.html.tmpl"
name="type"
options=types
default=form.type %]
</td>
[%# We could let people see a "queue" of non-pending requests. %]
<!--
<th>Status:</th>
<td>
[%# PROCESS "global/select-menu.html.tmpl"
name="status"
options=["all", "?", "+-", "+", "-"]
default=form.status %]
</td>
-->
</tr>
<tr>
<th>Requestee:</th>
<td><input type="text" name="requestee" value="[% form.requestee FILTER html %]" size="20"></td>
<th>Component:</th>
<td>
<select name="component">
<option value="">Any</option>
[% FOREACH item = components %]
<option value="[% item FILTER html %]" [% "selected" IF form.component == item %]>
[% item FILTER html %]</option>
[% END %]
</select>
</td>
<th>Group By:</th>
<td>
[% groups = {
"Requester" => 'requester' ,
"Requestee" => 'requestee',
"Flag" => 'type' ,
"Product/Component" => 'category'
} %]
[% PROCESS "global/select-menu.html.tmpl" name="group" options=groups default=form.group %]
</td>
<td><input type="submit" value="Filter"></td>
</tr>
</table>
</form>
[% PROCESS global/footer.html.tmpl %]
[% BLOCK start_new_table %]
[% "</table>" UNLESS group_value == "" %]
<h3>[% column_headers.$group_field %]: [% request.$group_field FILTER html %]</h3>
<table class="requests" cellspacing="0" cellpadding="4" border="1">
<tr>
[% FOREACH column = display_columns %]
[% NEXT IF column == group_field || excluded_columns.contains(column) %]
<th>[% column_headers.$column %]</th>
[% END %]
</tr>
[% group_value = request.$group_field %]
[% END %]
[% BLOCK display_type %]
[% request.type FILTER html %]
[% END %]
[% BLOCK display_status %]
[% request.status %]
[% END %]
[% BLOCK display_bug %]
<a href="show_bug.cgi?id=[% request.bug_id %]">
[% request.bug_id %]: [%+ request.bug_summary FILTER html %]</a>
[% END %]
[% BLOCK display_attachment %]
[% IF request.attach_id %]
<a href="attachment.cgi?id=[% request.attach_id %]&action=edit">
[% request.attach_id %]: [%+ request.attach_summary FILTER html %]</a>
[% ELSE %]
N/A
[% END %]
[% END %]
[% BLOCK display_requestee %]
[% request.requestee FILTER html %]
[% END %]
[% BLOCK display_requester %]
[% request.requester FILTER html %]
[% END %]
[% BLOCK display_created %]
[% request.created FILTER html %]
[% END %]

Просмотреть файл

Просмотреть файл

@ -207,6 +207,11 @@ sub DoEmail {
$vars->{'excludeself'} = 0;
}
foreach my $flag qw(FlagRequestee FlagRequester) {
$vars->{$flag} =
!exists($emailflags{$flag}) || $emailflags{$flag} eq 'on';
}
# Parse the info into a hash of hashes; the first hash keyed by role,
# the second by reason, and the value being 1 or 0 for (on or off).
# Preferences not existing in the user's list are assumed to be on.
@ -234,6 +239,10 @@ sub SaveEmail {
$updateString .= 'ExcludeSelf~';
}
foreach my $flag qw(FlagRequestee FlagRequester) {
$updateString .= "~$flag~" . (defined($::FORM{$flag}) ? "on" : "");
}
foreach my $role (@roles) {
foreach my $reason (@reasons) {
# Add this preference to the list without giving it a value,