summaryrefslogtreecommitdiffstats
path: root/Bugzilla
diff options
context:
space:
mode:
authormyk%mozilla.org <>2002-09-29 03:42:23 +0200
committermyk%mozilla.org <>2002-09-29 03:42:23 +0200
commit91b171e7584920d03abb9c45e779c84f3dee975c (patch)
treefc59becfe02d1a4dc84e5f3501f0139effcf1c7a /Bugzilla
parent90975fe914d066726d06f53abe8696399b13a61a (diff)
downloadbugzilla-91b171e7584920d03abb9c45e779c84f3dee975c.tar.gz
bugzilla-91b171e7584920d03abb9c45e779c84f3dee975c.tar.xz
Fix for bug 98801: Implementation of the request tracker, a set of enhancements to attachment statuses.
r=gerv,bbaetz
Diffstat (limited to 'Bugzilla')
-rw-r--r--Bugzilla/Attachment.pm42
-rw-r--r--Bugzilla/Flag.pm591
-rw-r--r--Bugzilla/FlagType.pm325
-rw-r--r--Bugzilla/Search.pm128
-rw-r--r--Bugzilla/User.pm176
5 files changed, 1176 insertions, 86 deletions
diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm
index 3a6248cf4..53690170e 100644
--- a/Bugzilla/Attachment.pm
+++ b/Bugzilla/Attachment.pm
@@ -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,
diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm
new file mode 100644
index 000000000..3feaae4cd
--- /dev/null
+++ b/Bugzilla/Flag.pm
@@ -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;
diff --git a/Bugzilla/FlagType.pm b/Bugzilla/FlagType.pm
new file mode 100644
index 000000000..2e272f67c
--- /dev/null
+++ b/Bugzilla/FlagType.pm
@@ -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;
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm
index 642965eb2..6d11c0739 100644
--- a/Bugzilla/Search.pm
+++ b/Bugzilla/Search.pm
@@ -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;
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
new file mode 100644
index 000000000..72870d544
--- /dev/null
+++ b/Bugzilla/User.pm
@@ -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;