diff options
36 files changed, 3562 insertions, 675 deletions
diff --git a/Attachment.pm b/Attachment.pm index 3a6248cf4..53690170e 100644 --- a/Attachment.pm +++ b/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/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; diff --git a/attachment.cgi b/attachment.cgi index 4d5fea475..b185312c6 100755 --- a/attachment.cgi +++ b/attachment.cgi @@ -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, diff --git a/bug_form.pl b/bug_form.pl index dfffca9b8..d087b4db2 100644 --- a/bug_form.pl +++ b/bug_form.pl @@ -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; diff --git a/checksetup.pl b/checksetup.pl index 27bcf26f9..737a629e8 100755 --- a/checksetup.pl +++ b/checksetup.pl @@ -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. - -$table{attachstatuses} = - ' - attach_id MEDIUMINT NOT NULL , - statusid SMALLINT NOT NULL , - PRIMARY KEY(attach_id, statusid) +# 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{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. # diff --git a/editattachstatuses.cgi b/editattachstatuses.cgi deleted file mode 100755 index eedf2add4..000000000 --- a/editattachstatuses.cgi +++ /dev/null @@ -1,347 +0,0 @@ -#!/usr/bonsaitools/bin/perl -w -# -*- 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): Terry Weissman <terry@mozilla.org> -# Myk Melez <myk@mozilla.org> - -################################################################################ -# Script Initialization -################################################################################ - -# Make it harder for us to do dangerous things in Perl. -use strict; -use lib "."; - -use vars qw( - $template - $vars -); - -# Include the Bugzilla CGI and general utility library. -require "CGI.pl"; - -# Establish a connection to the database backend. -ConnectToDatabase(); - -# Make sure the user is logged in and is allowed to edit products -# (i.e. the user has "editcomponents" privileges), since attachment -# statuses are product-specific. -confirm_login(); -UserInGroup("editcomponents") - || DisplayError("You are not authorized to administer attachment statuses.") - && exit; - -################################################################################ -# 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 ($action eq "list") -{ - list(); -} -elsif ($action eq "create") -{ - create(); -} -elsif ($action eq "insert") -{ - validateName(); - validateDescription(); - validateSortKey(); - validateProduct(); - insert(); -} -elsif ($action eq "edit") -{ - edit(); -} -elsif ($action eq "update") -{ - validateID(); - validateName(); - validateDescription(); - validateSortKey(); - update(); -} -elsif ($action eq "confirmdelete") -{ - validateID(); - confirmDelete(); -} -elsif ($action eq "delete") -{ - validateID(); - deleteStatus(); -} -else -{ - DisplayError("I could not figure out what you wanted to do.") -} - -exit; - -################################################################################ -# Data Validation -################################################################################ - -sub validateID -{ - $::FORM{'id'} =~ /^[1-9][0-9]*$/ - || DisplayError("The status ID is not a positive integer.") - && exit; - - SendSQL("SELECT 1 FROM attachstatusdefs WHERE id = $::FORM{'id'}"); - my ($defexists) = FetchSQLData(); - $defexists - || DisplayError("The status with ID #$::FORM{'id'} does not exist.") - && exit; -} - -sub validateName -{ - $::FORM{'name'} - || DisplayError("You must enter a name for the status.") - && exit; - - $::FORM{'name'} !~ /[\s,]/ - || DisplayError("The status name cannot contain commas or whitespace.") - && exit; - - length($::FORM{'name'}) <= 50 - || DisplayError("The status name cannot be more than 50 characters long.") - && exit; -} - -sub validateDescription -{ - $::FORM{'desc'} - || DisplayError("You must enter a description of the status.") - && exit; -} - -sub validateSortKey -{ - $::FORM{'sortkey'} =~ /^\d+$/ - && $::FORM{'sortkey'} < 32768 - || DisplayError("The sort key must be an integer between 0 and 32767 inclusive.") - && exit; -} - -sub validateProduct -{ - # Retrieve a list of products. - SendSQL("SELECT name FROM products"); - my @products; - push(@products, FetchSQLData()) while MoreSQLData(); - - grep($_ eq $::FORM{'product'}, @products) - || DisplayError("You must select an existing product for the status.") - && exit; -} - -################################################################################ -# Functions -################################################################################ - -sub list -{ - # Administer attachment status flags, which is the set of status flags - # that can be applied to an attachment. - - # If the user is seeing this screen as a result of doing something to - # an attachment status flag, display a message about what happened - # to that flag (i.e. "The attachment status flag was updated."). - my ($message) = (@_); - - # Retrieve a list of attachment status flags and create an array of hashes - # in which each hash contains the data for one flag. - SendSQL("SELECT attachstatusdefs.id, attachstatusdefs.name, " . - "attachstatusdefs.description, attachstatusdefs.sortkey, products.name, " . - "count(attachstatusdefs.id) " . - "FROM attachstatusdefs, products " . - "WHERE products.id = attachstatusdefs.product_id " . - "GROUP BY id " . - "ORDER BY attachstatusdefs.sortkey"); - my @statusdefs; - while ( MoreSQLData() ) - { - my ($id, $name, $description, $sortkey, $product, $attachcount) = FetchSQLData(); - push @statusdefs, { 'id' => $id , 'name' => $name , 'description' => $description , - 'sortkey' => $sortkey , 'product' => $product, - 'attachcount' => $attachcount }; - } - - # Define the variables and functions that will be passed to the UI template. - $vars->{'message'} = $message; - $vars->{'statusdefs'} = \@statusdefs; - - # 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/attachstatus/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); -} - - -sub create -{ - # Display a form for creating a new attachment status flag. - - # Retrieve a list of products to which the attachment status may apply. - SendSQL("SELECT name FROM products"); - my @products; - push(@products, FetchSQLData()) while MoreSQLData(); - - # Define the variables and functions that will be passed to the UI template. - $vars->{'products'} = \@products; - - # 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/attachstatus/create.html.tmpl", $vars) - || ThrowTemplateError($template->error()); -} - - -sub insert -{ - # Insert a new attachment status flag into the database. - - # Quote the flag's name and description as appropriate for inclusion - # in a SQL statement. - my $name = SqlQuote($::FORM{'name'}); - my $desc = SqlQuote($::FORM{'desc'}); - my $product_id = get_product_id($::FORM{'product'}); - - SendSQL("LOCK TABLES attachstatusdefs WRITE"); - SendSQL("SELECT MAX(id) FROM attachstatusdefs"); - my $id = FetchSQLData() + 1; - SendSQL("INSERT INTO attachstatusdefs (id, name, description, sortkey, product_id) - VALUES ($id, $name, $desc, $::FORM{'sortkey'}, $product_id)"); - SendSQL("UNLOCK TABLES"); - - # Display the "administer attachment status flags" page - # along with a message that the flag has been created. - list("The attachment status has been created."); -} - - -sub edit -{ - # Display a form for editing an existing attachment status flag. - - # Retrieve the definition from the database. - SendSQL("SELECT attachstatusdefs.name, attachstatusdefs.description, " . - " attachstatusdefs.sortkey, products.name " . - "FROM attachstatusdefs, products " . - "WHERE attachstatusdefs.product_id = products.id " . - " AND attachstatusdefs.id = $::FORM{'id'}"); - my ($name, $desc, $sortkey, $product) = FetchSQLData(); - - # Define the variables and functions that will be passed to the UI template. - $vars->{'id'} = $::FORM{'id'}; - $vars->{'name'} = $name; - $vars->{'desc'} = $desc; - $vars->{'sortkey'} = $sortkey; - $vars->{'product'} = $product; - - # 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/attachstatus/edit.html.tmpl", $vars) - || ThrowTemplateError($template->error()); -} - - -sub update -{ - # Update an attachment status flag in the database. - - # Quote the flag's name and description as appropriate for inclusion - # in a SQL statement. - my $name = SqlQuote($::FORM{'name'}); - my $desc = SqlQuote($::FORM{'desc'}); - - SendSQL("LOCK TABLES attachstatusdefs WRITE"); - SendSQL(" - UPDATE attachstatusdefs - SET name = $name , - description = $desc , - sortkey = $::FORM{'sortkey'} - WHERE id = $::FORM{'id'} - "); - SendSQL("UNLOCK TABLES"); - - # Display the "administer attachment status flags" page - # along with a message that the flag has been updated. - list("The attachment status has been updated."); -} - -sub confirmDelete -{ - # check if we need confirmation to delete: - - SendSQL("SELECT COUNT(attach_id), name - FROM attachstatusdefs LEFT JOIN attachstatuses - ON attachstatuses.statusid=attachstatusdefs.id - WHERE statusid = $::FORM{'id'} - GROUP BY attachstatuses.statusid;"); - - my ($attachcount, $name) = FetchSQLData(); - - if ($attachcount > 0) { - - $vars->{'id'} = $::FORM{'id'}; - $vars->{'attachcount'} = $attachcount; - $vars->{'name'} = $name; - - print "Content-type: text/html\n\n"; - - $template->process("admin/attachstatus/delete.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - } - else { - deleteStatus(); - } -} - -sub deleteStatus -{ - # Delete an attachment status flag from the database. - - SendSQL("LOCK TABLES attachstatusdefs WRITE, attachstatuses WRITE"); - SendSQL("DELETE FROM attachstatuses WHERE statusid = $::FORM{'id'}"); - SendSQL("DELETE FROM attachstatusdefs WHERE id = $::FORM{'id'}"); - SendSQL("UNLOCK TABLES"); - - # Display the "administer attachment status flags" page - # along with a message that the flag has been deleted. - list("The attachment status has been deleted."); -} diff --git a/editcomponents.cgi b/editcomponents.cgi index 7ad81ddfa..fc45b52c8 100755 --- a/editcomponents.cgi +++ b/editcomponents.cgi @@ -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"; diff --git a/editflagtypes.cgi b/editflagtypes.cgi new file mode 100755 index 000000000..aed73f284 --- /dev/null +++ b/editflagtypes.cgi @@ -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; +} + diff --git a/editproducts.cgi b/editproducts.cgi index 18ad4216d..3db7c6f84 100755 --- a/editproducts.cgi +++ b/editproducts.cgi @@ -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"; diff --git a/globals.pl b/globals.pl index a6a751562..9a625a842 100644 --- a/globals.pl +++ b/globals.pl @@ -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)); diff --git a/process_bug.cgi b/process_bug.cgi index 4ddfcca2c..076e014fc 100755 --- a/process_bug.cgi +++ b/process_bug.cgi @@ -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"); } diff --git a/productmenu.js b/productmenu.js new file mode 100644 index 000000000..d917d325c --- /dev/null +++ b/productmenu.js @@ -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 ); +} diff --git a/request.cgi b/request.cgi new file mode 100755 index 000000000..eb365559e --- /dev/null +++ b/request.cgi @@ -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'} }); +} + diff --git a/sanitycheck.cgi b/sanitycheck.cgi index da71163cc..2798dfd2f 100755 --- a/sanitycheck.cgi +++ b/sanitycheck.cgi @@ -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"); diff --git a/template/en/default/account/prefs/email.html.tmpl b/template/en/default/account/prefs/email.html.tmpl index e14ea9910..5d73a357b 100644 --- a/template/en/default/account/prefs/email.html.tmpl +++ b/template/en/default/account/prefs/email.html.tmpl @@ -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> diff --git a/template/en/default/admin/flag-type/confirm-delete.html.tmpl b/template/en/default/admin/flag-type/confirm-delete.html.tmpl new file mode 100644 index 000000000..b022e621e --- /dev/null +++ b/template/en/default/admin/flag-type/confirm-delete.html.tmpl @@ -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 %] diff --git a/template/en/default/admin/flag-type/edit.html.tmpl b/template/en/default/admin/flag-type/edit.html.tmpl new file mode 100644 index 000000000..ca01f6365 --- /dev/null +++ b/template/en/default/admin/flag-type/edit.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> </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> </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> </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> </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 %] diff --git a/template/en/default/admin/flag-type/list.html.tmpl b/template/en/default/admin/flag-type/list.html.tmpl new file mode 100644 index 000000000..76a835639 --- /dev/null +++ b/template/en/default/admin/flag-type/list.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 %] diff --git a/template/en/default/attachment/edit.html.tmpl b/template/en/default/attachment/edit.html.tmpl index ec2616bf9..32449f041 100644 --- a/template/en/default/attachment/edit.html.tmpl +++ b/template/en/default/attachment/edit.html.tmpl @@ -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> diff --git a/template/en/default/attachment/list.html.tmpl b/template/en/default/attachment/list.html.tmpl index e7aa8b0ef..59f749695 100644 --- a/template/en/default/attachment/list.html.tmpl +++ b/template/en/default/attachment/list.html.tmpl @@ -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', ' ') %]<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 %]&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 %]&action=enter">Create a New Attachment</a> (proposed patch, testcase, etc.) </td> <td colspan="1"> diff --git a/template/en/default/bug/edit.html.tmpl b/template/en/default/bug/edit.html.tmpl index 9cf33b8b5..ef9ec2d7f 100644 --- a/template/en/default/bug/edit.html.tmpl +++ b/template/en/default/bug/edit.html.tmpl @@ -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 *** %] diff --git a/template/en/default/flag/list.html.tmpl b/template/en/default/flag/list.html.tmpl new file mode 100644 index 000000000..951f248db --- /dev/null +++ b/template/en/default/flag/list.html.tmpl @@ -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> </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> diff --git a/template/en/default/global/code-error.html.tmpl b/template/en/default/global/code-error.html.tmpl index bf93977ad..1981364f1 100644 --- a/template/en/default/global/code-error.html.tmpl +++ b/template/en/default/global/code-error.html.tmpl @@ -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. #%] diff --git a/template/en/default/global/messages.html.tmpl b/template/en/default/global/messages.html.tmpl index 584c4a93e..85c678fdc 100644 --- a/template/en/default/global/messages.html.tmpl +++ b/template/en/default/global/messages.html.tmpl @@ -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") %] diff --git a/template/en/default/global/select-menu.html.tmpl b/template/en/default/global/select-menu.html.tmpl index c27f60e8b..7b7fddb29 100644 --- a/template/en/default/global/select-menu.html.tmpl +++ b/template/en/default/global/select-menu.html.tmpl @@ -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 %]> diff --git a/template/en/default/global/useful-links.html.tmpl b/template/en/default/global/useful-links.html.tmpl index 1e5b09df1..785a9d75e 100644 --- a/template/en/default/global/useful-links.html.tmpl +++ b/template/en/default/global/useful-links.html.tmpl @@ -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 statuses</a>' + [% ', <a href="editflagtypes.cgi">flags</a>' IF user.groups.editcomponents %] [% ', <a href="editgroups.cgi">groups</a>' IF user.groups.creategroups %] diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl index c6f970df3..c9dca30d4 100644 --- a/template/en/default/global/user-error.html.tmpl +++ b/template/en/default/global/user-error.html.tmpl @@ -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. diff --git a/template/en/default/request/created-email.txt.tmpl b/template/en/default/request/created-email.txt.tmpl new file mode 100644 index 000000000..3edf10786 --- /dev/null +++ b/template/en/default/request/created-email.txt.tmpl @@ -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 %] diff --git a/template/en/default/request/fulfilled-email.txt.tmpl b/template/en/default/request/fulfilled-email.txt.tmpl new file mode 100644 index 000000000..84608c546 --- /dev/null +++ b/template/en/default/request/fulfilled-email.txt.tmpl @@ -0,0 +1,42 @@ +[%# 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> + #%] +[% statuses = { '+' => "approved" , '-' => 'denied' , 'X' => "cancelled" } %] +From: bugzilla-request-daemon +To: [% flag.setter.email IF flag.setter.email_prefs.FlagRequester %] +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) -%] +[% user.realname %] <[% user.login %]> has [% statuses.${flag.status} %] your request 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 %] diff --git a/template/en/default/request/queue.html.tmpl b/template/en/default/request/queue.html.tmpl new file mode 100644 index 000000000..14f244ab3 --- /dev/null +++ b/template/en/default/request/queue.html.tmpl @@ -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 %] + diff --git a/template/en/default/request/verify.html.tmpl b/template/en/default/request/verify.html.tmpl new file mode 100644 index 000000000..ad4c07d2c --- /dev/null +++ b/template/en/default/request/verify.html.tmpl @@ -0,0 +1,108 @@ +<!-- 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> + #%] + +[%# INTERFACE: + # form, mform: hashes; the form values submitted to the script, used by + # hidden-fields to generate hidden form fields replicating + # the original form + # flags: array; the flags the user made, including information about + # potential requestees for those flags (based on + # the string the user typed into the requestee fields) + # target: record; the bug/attachment for which the flags are being made + #%] + +[% UNLESS header_done %] + [% title = BLOCK %] + Verify Requests for Bug #[% target.bug.id %] + [% IF target.attachment %], Attachment #[% target.attachment.id %][% END %] + [% END %] + + [% h1 = BLOCK %] + Verify Requests for <a href="show_bug.cgi?id=[% target.bug.id %]">Bug #[% target.bug.id %]</a> + [% IF target.attachment.exists %], + <a href="attachment.cgi?id=[% target.attachment.id %]&action=edit">Attachment #[% target.attachment.id %]</a> + [% END %] + [% END %] + + [% h2 = BLOCK %] + [% target.bug.summary FILTER html %] + [% IF target.attachment.exists %] + : [% target.attachment.summary FILTER html %] + [% END %] + [% END %] + + [% PROCESS global/header.html.tmpl %] +[% END %] + +<form method="post"> + +[% PROCESS "global/hidden-fields.html.tmpl" + exclude=("^(flag_type|requestee)-") %] + +[% FOREACH flag = flags %] + [% IF flag.requestees.size == 0 %] + <p> + Sorry, I can't find a user whose name or email address contains + the string <em>[% flag.requestee_str FILTER html %]</em>. + Double-check that the user's name or email address contains that + string, or try entering a shorter string. + </p> + <p> + Ask <input type="text" size="20" maxlength="255" + name="requestee-[% flag.type.id %]" + value="[% flag.requestee_str FILTER html %]"> + for [% flag.type.name FILTER html %] + <input type="hidden" name="flag_type-[% flag.type.id %]" value="?"> + </p> + + [% ELSIF flag.requestees.size == 1 %] + <input type="hidden" + name="requestee-[% flag.type.id %]" + value="[% flag.requestee.email FILTER html %]"> + <input type="hidden" name="flag_type-[% flag.type.id %]" value="?"> + + [% ELSE %] + <p> + More than one user's name or email address contains the string + <em>[% flag.requestee_str FILTER html %]</em>. Choose the user + you meant from the following menu or click the back button and try + again with a more specific string. + </p> + <p> + Ask <select name="requestee-[% flag.type.id %]"> + [% FOREACH requestee = flag.requestees %] + <option value="[% requestee.email FILTER html %]"> + [% requestee.identity FILTER html%]</option> + [% END %] + </select> + for [% flag.type.name %] + <input type="hidden" name="flag_type-[% flag.type.id %]" value="?"> + </p> + + [% END %] +[% END %] + +<input type="submit" value="Commit"> + +</form> + +[% PROCESS global/footer.html.tmpl %] + diff --git a/userprefs.cgi b/userprefs.cgi index 369c681ca..1d4be2a78 100755 --- a/userprefs.cgi +++ b/userprefs.cgi @@ -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, |