summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Bugzilla/RelationSet.pm211
-rw-r--r--README4
-rw-r--r--RelationSet.pm211
-rw-r--r--bug_form.pl10
-rwxr-xr-xchecksetup.pl85
-rw-r--r--defparams.pl6
-rw-r--r--globals.pl27
-rwxr-xr-xprocess_bug.cgi67
-rwxr-xr-xprocessmail266
-rwxr-xr-xuserprefs.cgi67
10 files changed, 763 insertions, 191 deletions
diff --git a/Bugzilla/RelationSet.pm b/Bugzilla/RelationSet.pm
new file mode 100644
index 000000000..ee402e7a4
--- /dev/null
+++ b/Bugzilla/RelationSet.pm
@@ -0,0 +1,211 @@
+#
+# 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) 2000 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Dan Mosedale <dmose@mozilla.org>
+# Terry Weissman <terry@mozilla.org>
+
+# This object models a set of relations between one item and a group
+# of other items. An example is the set of relations between one bug
+# and the users CCed on that bug. Currently, the relation objects are
+# expected to be bugzilla userids. However, this could and perhaps
+# should be generalized to work with non userid objects, such as
+# keywords associated with a bug. That shouldn't be hard to do; it
+# might involve turning this into a virtual base class, and having
+# UserSet and KeywordSet types that inherit from it.
+
+use diagnostics;
+use strict;
+
+require "globals.pl";
+
+package RelationSet;
+use CGI::Carp qw(fatalsToBrowser);
+
+# create a new empty RelationSet
+#
+sub new {
+ my $type = shift();
+
+ # create a ref to an empty hash and bless it
+ #
+ my $self = {};
+ bless $self, $type;
+
+ # construct from a comma-delimited string
+ #
+ if ($#_ == 0) {
+ $self->mergeFromString($_[0]);
+ }
+ # unless this was a constructor for an empty list, somebody screwed up.
+ #
+ elsif ( $#_ != -1 ) {
+ confess("invalid number of arguments");
+ }
+
+ # bless as a RelationSet
+ #
+ return $self;
+}
+
+# Assumes that the set of relations "FROM $table WHERE $constantSql and
+# $column = $value" is currently represented by $self, and this set should
+# be updated to look like $other.
+#
+# Returns an array of two strings, one INSERT and one DELETE, which will
+# make this change. Either or both strings may be the empty string,
+# meaning that no INSERT or DELETE or both (respectively) need to be done.
+#
+# THE CALLER IS RESPONSIBLE FOR ANY DESIRED LOCKING AND/OR CONSISTENCY
+# CHECKS (not to mention doing the SendSQL() calls).
+#
+sub generateSqlDeltas {
+ ($#_ == 5) || confess("invalid number of arguments");
+ my ( $self, # instance ptr to set representing the existing state
+ $endState, # instance ptr to set representing the desired state
+ $table, # table where these relations are kept
+ $invariantName, # column held const for a RelationSet (often "bug_id")
+ $invariantValue, # what to hold the above column constant at
+ $columnName # the column which varies (often a userid)
+ ) = @_;
+
+ # construct the insert list by finding relations which exist in the
+ # end state but not the current state.
+ #
+ my @endStateRelations = keys(%$endState);
+ my @insertList = ();
+ foreach ( @endStateRelations ) {
+ push ( @insertList, $_ ) if ( ! exists $$self{"$_"} );
+ }
+
+ # we've built the list. If it's non-null, add required sql chrome.
+ #
+ my $sqlInsert="";
+ if ( $#insertList > -1 ) {
+ $sqlInsert = "INSERT INTO $table ($invariantName, $columnName) VALUES " .
+ join (",",
+ map ( "($invariantValue, $_)" , @insertList )
+ );
+ }
+
+ # construct the delete list by seeing which relations exist in the
+ # current state but not the end state
+ #
+ my @selfRelations = keys(%$self);
+ my @deleteList = ();
+ foreach ( @selfRelations ) {
+ push (@deleteList, $_) if ( ! exists $$endState{"$_"} );
+ }
+
+ # we've built the list. if it's non-empty, add required sql chrome.
+ #
+ my $sqlDelete = "";
+ if ( $#deleteList > -1 ) {
+ $sqlDelete = "DELETE FROM $table WHERE $invariantName = $invariantValue " .
+ "AND $columnName IN ( " . join (",", @deleteList) . " )";
+ }
+
+ return ($sqlInsert, $sqlDelete);
+}
+
+# compare the current object with another.
+#
+sub isEqual {
+ ($#_ == 1) || confess("invalid number of arguments");
+ my $self = shift();
+ my $other = shift();
+
+ # get arrays of the keys for faster processing
+ #
+ my @selfRelations = keys(%$self);
+ my @otherRelations = keys(%$other);
+
+ # make sure the arrays are the same size
+ #
+ return 0 if ( $#selfRelations != $#otherRelations );
+
+ # bail out if any of the elements are different
+ #
+ foreach my $relation ( @selfRelations ) {
+ return 0 if ( !exists $$other{$relation})
+ }
+
+ # we made it!
+ #
+ return 1;
+
+}
+
+# merge the results of a SQL command into this set
+#
+sub mergeFromDB {
+ ( $#_ == 1 ) || confess("invalid number of arguments");
+ my $self = shift();
+
+ &::SendSQL(shift());
+ while (my @row = &::FetchSQLData()) {
+ $$self{$row[0]} = 1;
+ }
+
+ return;
+}
+
+# merge a set in string form into this set
+#
+sub mergeFromString {
+ ($#_ == 1) || confess("invalid number of arguments");
+ my $self = shift();
+
+ # do the merge
+ #
+ foreach my $person (split(/[ ,]/, shift())) {
+ if ($person ne "") {
+ $$self{&::DBNameToIdAndCheck($person)} = 1;
+ }
+ }
+}
+
+# return the number of elements in this set
+#
+sub size {
+ my $self = shift();
+
+ my @k = keys(%$self);
+ return $#k++;
+}
+
+# return this set in array form
+#
+sub toArray {
+ my $self= shift();
+
+ return keys(%$self);
+}
+
+# return this set in string form (comma-separated and sorted)
+#
+sub toString {
+ ($#_ == 0) || confess("invalid number of arguments");
+ my $self = shift();
+
+ my @result = ();
+ foreach my $i ( keys %$self ) {
+ push @result, &::DBID_to_name($i);
+ }
+
+ return join(',', sort(@result));
+}
diff --git a/README b/README
index 65dfd0d77..017da0686 100644
--- a/README
+++ b/README
@@ -26,7 +26,7 @@ other necessary ingredient is a web server set up to run cgi scripts.
The software packages necessary for the proper running of bugzilla are:
- 1. MySQL database server and the mysql client
+ 1. MySQL database server and the mysql client (3.22.5 or greater)
2. Perl (5.004 or greater)
3. DBI Perl module
4. Data::Dumper Perl module
@@ -39,7 +39,7 @@ other necessary ingredient is a web server set up to run cgi scripts.
Bugzilla has quite a few prerequisites, but none of them are TCL.
Previous versions required TCL, but it no longer needed (or used).
-1.1. Getting and setting up MySQL database
+1.1. Getting and setting up MySQL database (3.22.5 or greater)
Visit MySQL homepage at http://www.mysql.org and grab the latest stable
release of the server. Both binaries and source are available and which
diff --git a/RelationSet.pm b/RelationSet.pm
new file mode 100644
index 000000000..ee402e7a4
--- /dev/null
+++ b/RelationSet.pm
@@ -0,0 +1,211 @@
+#
+# 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) 2000 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Dan Mosedale <dmose@mozilla.org>
+# Terry Weissman <terry@mozilla.org>
+
+# This object models a set of relations between one item and a group
+# of other items. An example is the set of relations between one bug
+# and the users CCed on that bug. Currently, the relation objects are
+# expected to be bugzilla userids. However, this could and perhaps
+# should be generalized to work with non userid objects, such as
+# keywords associated with a bug. That shouldn't be hard to do; it
+# might involve turning this into a virtual base class, and having
+# UserSet and KeywordSet types that inherit from it.
+
+use diagnostics;
+use strict;
+
+require "globals.pl";
+
+package RelationSet;
+use CGI::Carp qw(fatalsToBrowser);
+
+# create a new empty RelationSet
+#
+sub new {
+ my $type = shift();
+
+ # create a ref to an empty hash and bless it
+ #
+ my $self = {};
+ bless $self, $type;
+
+ # construct from a comma-delimited string
+ #
+ if ($#_ == 0) {
+ $self->mergeFromString($_[0]);
+ }
+ # unless this was a constructor for an empty list, somebody screwed up.
+ #
+ elsif ( $#_ != -1 ) {
+ confess("invalid number of arguments");
+ }
+
+ # bless as a RelationSet
+ #
+ return $self;
+}
+
+# Assumes that the set of relations "FROM $table WHERE $constantSql and
+# $column = $value" is currently represented by $self, and this set should
+# be updated to look like $other.
+#
+# Returns an array of two strings, one INSERT and one DELETE, which will
+# make this change. Either or both strings may be the empty string,
+# meaning that no INSERT or DELETE or both (respectively) need to be done.
+#
+# THE CALLER IS RESPONSIBLE FOR ANY DESIRED LOCKING AND/OR CONSISTENCY
+# CHECKS (not to mention doing the SendSQL() calls).
+#
+sub generateSqlDeltas {
+ ($#_ == 5) || confess("invalid number of arguments");
+ my ( $self, # instance ptr to set representing the existing state
+ $endState, # instance ptr to set representing the desired state
+ $table, # table where these relations are kept
+ $invariantName, # column held const for a RelationSet (often "bug_id")
+ $invariantValue, # what to hold the above column constant at
+ $columnName # the column which varies (often a userid)
+ ) = @_;
+
+ # construct the insert list by finding relations which exist in the
+ # end state but not the current state.
+ #
+ my @endStateRelations = keys(%$endState);
+ my @insertList = ();
+ foreach ( @endStateRelations ) {
+ push ( @insertList, $_ ) if ( ! exists $$self{"$_"} );
+ }
+
+ # we've built the list. If it's non-null, add required sql chrome.
+ #
+ my $sqlInsert="";
+ if ( $#insertList > -1 ) {
+ $sqlInsert = "INSERT INTO $table ($invariantName, $columnName) VALUES " .
+ join (",",
+ map ( "($invariantValue, $_)" , @insertList )
+ );
+ }
+
+ # construct the delete list by seeing which relations exist in the
+ # current state but not the end state
+ #
+ my @selfRelations = keys(%$self);
+ my @deleteList = ();
+ foreach ( @selfRelations ) {
+ push (@deleteList, $_) if ( ! exists $$endState{"$_"} );
+ }
+
+ # we've built the list. if it's non-empty, add required sql chrome.
+ #
+ my $sqlDelete = "";
+ if ( $#deleteList > -1 ) {
+ $sqlDelete = "DELETE FROM $table WHERE $invariantName = $invariantValue " .
+ "AND $columnName IN ( " . join (",", @deleteList) . " )";
+ }
+
+ return ($sqlInsert, $sqlDelete);
+}
+
+# compare the current object with another.
+#
+sub isEqual {
+ ($#_ == 1) || confess("invalid number of arguments");
+ my $self = shift();
+ my $other = shift();
+
+ # get arrays of the keys for faster processing
+ #
+ my @selfRelations = keys(%$self);
+ my @otherRelations = keys(%$other);
+
+ # make sure the arrays are the same size
+ #
+ return 0 if ( $#selfRelations != $#otherRelations );
+
+ # bail out if any of the elements are different
+ #
+ foreach my $relation ( @selfRelations ) {
+ return 0 if ( !exists $$other{$relation})
+ }
+
+ # we made it!
+ #
+ return 1;
+
+}
+
+# merge the results of a SQL command into this set
+#
+sub mergeFromDB {
+ ( $#_ == 1 ) || confess("invalid number of arguments");
+ my $self = shift();
+
+ &::SendSQL(shift());
+ while (my @row = &::FetchSQLData()) {
+ $$self{$row[0]} = 1;
+ }
+
+ return;
+}
+
+# merge a set in string form into this set
+#
+sub mergeFromString {
+ ($#_ == 1) || confess("invalid number of arguments");
+ my $self = shift();
+
+ # do the merge
+ #
+ foreach my $person (split(/[ ,]/, shift())) {
+ if ($person ne "") {
+ $$self{&::DBNameToIdAndCheck($person)} = 1;
+ }
+ }
+}
+
+# return the number of elements in this set
+#
+sub size {
+ my $self = shift();
+
+ my @k = keys(%$self);
+ return $#k++;
+}
+
+# return this set in array form
+#
+sub toArray {
+ my $self= shift();
+
+ return keys(%$self);
+}
+
+# return this set in string form (comma-separated and sorted)
+#
+sub toString {
+ ($#_ == 0) || confess("invalid number of arguments");
+ my $self = shift();
+
+ my @result = ();
+ foreach my $i ( keys %$self ) {
+ push @result, &::DBID_to_name($i);
+ }
+
+ return join(',', sort(@result));
+}
diff --git a/bug_form.pl b/bug_form.pl
index 4453d7ff1..9b459d66c 100644
--- a/bug_form.pl
+++ b/bug_form.pl
@@ -22,6 +22,8 @@
use diagnostics;
use strict;
+use RelationSet;
+
# Shut up misguided -w warnings about "used only once". For some reason,
# "use vars" chokes on me when I try it here.
@@ -147,8 +149,10 @@ my $sev_popup = make_options(\@::legal_severity, $bug{'bug_severity'});
my $component_popup = make_options($::components{$bug{'product'}},
$bug{'component'});
+my $ccSet = new RelationSet;
+$ccSet->mergeFromDB("select who from cc where bug_id=$id");
my $cc_element = '<INPUT NAME=cc SIZE=30 VALUE="' .
- ShowCcList($id) . '">';
+ $ccSet->toString() . '">';
my $URL = $bug{'bug_file_loc'};
@@ -208,7 +212,9 @@ if (Param("usetargetmilestone")) {
if ($url eq "") {
$url = "notargetmilestone.html";
}
-
+ if ($bug{'target_milestone'} eq "") {
+ $bug{'target_milestone'} = " ";
+ }
print "
<TD ALIGN=RIGHT><A href=\"$url\"><B>Target Milestone:</B></A></TD>
<TD><SELECT NAME=target_milestone>" .
diff --git a/checksetup.pl b/checksetup.pl
index 9a11dc588..17e510940 100755
--- a/checksetup.pl
+++ b/checksetup.pl
@@ -20,6 +20,7 @@
#
# Contributor(s): Holger Schurig <holgerschurig@nikocity.de>
# Terry Weissman <terry@mozilla.org>
+# Dan Mosedale <dmose@mozilla.org>
#
#
# Direct any questions on this source code to
@@ -591,8 +592,15 @@ $table{cc} =
'bug_id mediumint not null,
who mediumint not null,
- index(bug_id),
- index(who)';
+ index(who),
+ unique(bug_id,who)';
+
+$table{watch} =
+ 'watcher mediumint not null,
+ watched mediumint not null,
+
+ index(watched),
+ unique(watcher,watched)';
$table{longdescs} =
@@ -742,8 +750,8 @@ $table{keywords} =
'bug_id mediumint not null,
keywordid smallint not null,
- index(bug_id),
- index(keywordid)';
+ index(keywordid),
+ unique(bug_id,keywordid)';
$table{keyworddefs} =
'id smallint not null primary key,
@@ -994,6 +1002,49 @@ sub GetIndexDef ($$)
}
}
+sub CountIndexes ($)
+{
+ my ($table) = @_;
+
+ my $sth = $dbh->prepare("SHOW INDEX FROM $table");
+ $sth->execute;
+
+ if ( $sth->rows == -1 ) {
+ die ("Unexpected response while counting indexes in $table:" .
+ " \$sth->rows == -1");
+ }
+
+ return ($sth->rows);
+}
+
+sub DropIndexes ($)
+{
+ my ($table) = @_;
+ my %SEEN;
+
+ # get the list of indexes
+ #
+ my $sth = $dbh->prepare("SHOW INDEX FROM $table");
+ $sth->execute;
+
+ # drop each index
+ #
+ while ( my $ref = $sth->fetchrow_arrayref) {
+
+ # note that some indexes are described by multiple rows in the
+ # index table, so we may have already dropped the index described
+ # in the current row.
+ #
+ next if exists $SEEN{$$ref[2]};
+
+ my $dropSth = $dbh->prepare("ALTER TABLE $table DROP INDEX $$ref[2]");
+ $dropSth->execute;
+ $dropSth->finish;
+ $SEEN{$$ref[2]} = 1;
+
+ }
+
+}
#
# Check if the enums in the bugs table return the same values that are defined
# in the various locally changeable variables. If this is true, then alter the
@@ -1506,7 +1557,6 @@ AddField('products', 'maxvotesperbug', 'smallint not null default 10000');
AddField('products', 'votestoconfirm', 'smallint not null');
AddField('profiles', 'blessgroupset', 'bigint not null');
-
# 2000-03-21 Adding a table for target milestones to
# database - matthew@zeroknowledge.com
@@ -1577,6 +1627,29 @@ if (!GetFieldDef('products', 'defaultmilestone')) {
}
}
+# 2000-03-24 Added unique indexes into the cc and keyword tables. This
+# prevents certain database inconsistencies, and, moreover, is required for
+# new generalized list code to work.
+
+if ( CountIndexes('cc') != 3 ) {
+
+ # XXX should eliminate duplicate entries before altering
+ #
+ print "Recreating indexes on cc table.\n";
+ DropIndexes('cc');
+ $dbh->do("ALTER TABLE cc ADD UNIQUE (bug_id,who)");
+ $dbh->do("ALTER TABLE cc ADD INDEX (who)");
+}
+
+if ( CountIndexes('keywords') != 3 ) {
+
+ # XXX should eliminate duplicate entries before altering
+ #
+ print "Recreating indexes on keywords table.\n";
+ DropIndexes('keywords');
+ $dbh->do("ALTER TABLE keywords ADD INDEX (keywordid)");
+ $dbh->do("ALTER TABLE keywords ADD UNIQUE (bug_id,keywordid)");
+}
#
# If you had to change the --TABLE-- definition in any way, then add your
@@ -1594,4 +1667,6 @@ if ($regenerateshadow) {
print "Now regenerating the shadow database for all bugs.\n";
system("./processmail regenerate");
}
+
unlink "data/versioncache";
+print "Reminder: Bugzilla now requires version 3.22.5 or later of MySQL.\n";
diff --git a/defparams.pl b/defparams.pl
index 04e90ad0d..41a40faf0 100644
--- a/defparams.pl
+++ b/defparams.pl
@@ -550,6 +550,10 @@ DefParam("commentonclose",
DefParam("commentonduplicate",
"If this option is on, the user needs to enter a short comment if the bug is marked as duplicate",
"b", 0 );
-
+DefParam("supportwatchers",
+ "Support one user watching (ie getting copies of all related email" .
+ " about) another's bugs. Useful for people going on vacation, and" .
+ " QA folks watching particular developers' bugs",
+ "b", 0 );
1;
diff --git a/globals.pl b/globals.pl
index c870082c6..58eb7738c 100644
--- a/globals.pl
+++ b/globals.pl
@@ -18,6 +18,7 @@
# Rights Reserved.
#
# Contributor(s): Terry Weissman <terry@mozilla.org>
+# Dan Mosedale <dmose@mozilla.org>
# Contains some global variables and routines used throughout bugzilla.
@@ -65,6 +66,7 @@ use Mysql;
use Date::Format; # For time2str().
use Date::Parse; # For str2time().
# use Carp; # for confess
+use RelationSet;
# Contains the version string for the current running Bugzilla.
$::param{'version'} = '2.9';
@@ -118,9 +120,6 @@ sub SqlLog {
}
}
-
-
-
sub SendSQL {
my ($str, $dontshadow) = (@_);
my $iswrite = ($str =~ /^(INSERT|REPLACE|UPDATE|DELETE)/i);
@@ -756,23 +755,13 @@ sub GetLongDescriptionAsHTML {
}
sub ShowCcList {
- my ($num) = (@_);
- my @ccids;
- my @row;
- SendSQL("select who from cc where bug_id = $num");
- while (@row = FetchSQLData()) {
- push(@ccids, $row[0]);
- }
- my @result = ();
- foreach my $i (@ccids) {
- push @result, DBID_to_name($i);
- }
-
- return join(',', @result);
+ my ($num) = (@_);
+
+ my $ccSet = new RelationSet();
+ $ccSet->mergeFromDB("select who from cc where bug_id=$num");
+ return $ccSet->toString();
}
-
-
# Fills in a hashtable with info about the columns for the given table in the
# database. The hashtable has the following entries:
# -list- the list of column names
@@ -903,7 +892,7 @@ sub RemoveVotes {
}
-sub Param {
+sub Param ($) {
my ($value) = (@_);
if (defined $::param{$value}) {
return $::param{$value};
diff --git a/process_bug.cgi b/process_bug.cgi
index 4559af8b3..913ff8f18 100755
--- a/process_bug.cgi
+++ b/process_bug.cgi
@@ -28,6 +28,7 @@ my $UserInEditGroupSet = -1;
my $UserInCanConfirmGroupSet = -1;
require "CGI.pl";
+use RelationSet;
# Shut up misguided -w warnings about "used only once":
@@ -373,24 +374,21 @@ if (defined $::FORM{'qa_contact'}) {
ConnectToDatabase();
-my %ccids;
-my $origcclist = "";
+my $formCcSet = new RelationSet;
+my $origCcSet = new RelationSet;
+my $origCcString;
# We make sure to check out the CC list before we actually start touching any
-# bugs.
+# bugs. mergeFromString() ultimately searches the database using a quoted
+# form of the data it gets from $::FORM{'cc'}, so anything bogus from a
+# security standpoint should trigger an abort there.
+#
if (defined $::FORM{'cc'} && defined $::FORM{'id'}) {
- $origcclist = ShowCcList($::FORM{'id'});
- if ($origcclist ne $::FORM{'cc'}) {
- foreach my $person (split(/[ ,]/, $::FORM{'cc'})) {
- if ($person ne "") {
- my $cid = DBNameToIdAndCheck($person);
- $ccids{$cid} = 1;
- }
- }
- }
+ $origCcSet->mergeFromDB("select who from cc where bug_id = $::FORM{'id'}");
+ $origCcString = $origCcSet->toString(); # cache a copy of the string vers
+ $formCcSet->mergeFromString($::FORM{'cc'});
}
-
if ( Param('strictvaluechecks') ) {
CheckFormFieldDefined(\%::FORM, 'knob');
}
@@ -759,22 +757,25 @@ The changes made were:
AppendComment($id, $::FORM{'who'}, $::FORM{'comment'});
}
- if (defined $::FORM{'cc'} && $origcclist ne $::FORM{'cc'}) {
- SendSQL("delete from cc where bug_id = $id");
- foreach my $ccid (keys %ccids) {
- SendSQL("insert into cc (bug_id, who) values ($id, $ccid)");
- }
- my $newcclist = ShowCcList($id);
- if ($newcclist ne $origcclist) {
- my $col = GetFieldID('cc');
- my $origq = SqlQuote($origcclist);
- my $newq = SqlQuote($newcclist);
- SendSQL("INSERT INTO bugs_activity " .
- "(bug_id,who,bug_when,fieldid,oldvalue,newvalue) VALUES " .
- "($id,$whoid,'$timestamp',$col,$origq,$newq)");
- }
- }
-
+ if (defined $::FORM{'cc'} && defined $::FORM{'id'}
+ && ! $origCcSet->isEqual($formCcSet) ) {
+
+ # update the database to look like the form
+ #
+ my @CCDELTAS = $origCcSet->generateSqlDeltas($formCcSet, "cc",
+ "bug_id", $::FORM{'id'},
+ "who");
+ $CCDELTAS[0] eq "" || SendSQL($CCDELTAS[0]);
+ $CCDELTAS[1] eq "" || SendSQL($CCDELTAS[1]);
+
+ my $col = GetFieldID('cc');
+ my $origq = SqlQuote($origCcString);
+ my $newq = SqlQuote($::FORM{'cc'});
+ SendSQL("INSERT INTO bugs_activity " .
+ "(bug_id,who,bug_when,fieldid,oldvalue,newvalue) VALUES " .
+ "($id,$whoid,'$timestamp',$col,$origq,$newq)");
+ }
+
if (defined $::FORM{'dependson'}) {
my $me = "blocked";
@@ -850,9 +851,9 @@ The changes made were:
if ($col eq 'assigned_to' || $col eq 'qa_contact') {
$old = DBID_to_name($old) if $old != 0;
$new = DBID_to_name($new) if $new != 0;
- $origcclist .= ",$old"; # make sure to send mail to people
- # if they are going to no longer get
- # updates about this bug.
+ $origCcString .= ",$old"; # make sure to send mail to people
+ # if they are going to no longer get
+ # updates about this bug.
}
if ($col eq 'product') {
RemoveVotes($id, 0,
@@ -869,7 +870,7 @@ The changes made were:
print "<TABLE BORDER=1><TD><H2>Changes to bug $id submitted</H2>\n";
SendSQL("unlock tables");
- system("./processmail", "-forcecc", $origcclist, $id, $::FORM{'who'});
+ system("./processmail", "-forcecc", $origCcString, $id, $::FORM{'who'});
print "<TD><A HREF=\"show_bug.cgi?id=$id\">Back To BUG# $id</A></TABLE>\n";
foreach my $k (keys(%dependencychanged)) {
diff --git a/processmail b/processmail
index 9b9baa4dd..8c8ad8719 100755
--- a/processmail
+++ b/processmail
@@ -19,7 +19,8 @@
# Rights Reserved.
#
# Contributor(s): Terry Weissman <terry@mozilla.org>,
-# Bryce Nesbitt <bryce-mozilla@nextbus.com>
+# Bryce Nesbitt <bryce-mozilla@nextbus.com>
+# Dan Mosedale <dmose@mozilla.org>
# To recreate the shadow database, run "processmail regenerate" .
@@ -28,6 +29,8 @@ use strict;
require "globals.pl";
+use RelationSet;
+
$| = 1;
umask(0);
@@ -100,11 +103,11 @@ sub Different {
sub DescCC {
- my ($cclist) = (@_);
- if (scalar(@$cclist) <= 0) {
- return "";
- }
- return "Cc: " . join(", ", @$cclist) . "\n";
+ my $cclist = shift();
+
+ return "" if ( $cclist->size() == 0 );
+
+ return "Cc: " . $cclist->toString() . "\n";
}
@@ -201,20 +204,20 @@ sub GetBugText {
$::bug{'long_desc'} = GetLongDescriptionAsText($id);
- my @cclist;
- @cclist = split(/,/, ShowCcList($id));
+ my $cclist = new RelationSet();
+ $cclist->mergeFromDB("select who from cc where bug_id = $id");
my @voterlist;
SendSQL("select profiles.login_name from votes, profiles where votes.bug_id = $id and profiles.userid = votes.who");
while (MoreSQLData()) {
my $v = FetchOneColumn();
push(@voterlist, $v);
}
- $::bug{'cclist'} = join(',', @cclist);
+ $::bug{'cclist'} = $cclist->toString();
$::bug{'voterlist'} = join(',', @voterlist);
if (Param("prettyasciimail")) {
$^A = "";
- my $temp = formline <<'END',$::bug{'short_desc'},$id,$::bug{'product'},$::bug{'bug_status'},$::bug{'version'},$::bug{'resolution'},$::bug{'rep_platform'},$::bug{'bug_severity'},$::bug{'op_sys'},$::bug{'priority'},$::bug{'component'},$::bug{'assigned_to'},$::bug{'reporter'},$qa_contact,DescCC(\@cclist),$target_milestone,${status_whiteboard},$::bug{'bug_file_loc'},DescDependencies($id);
+ my $temp = formline <<'END',$::bug{'short_desc'},$id,$::bug{'product'},$::bug{'bug_status'},$::bug{'version'},$::bug{'resolution'},$::bug{'rep_platform'},$::bug{'bug_severity'},$::bug{'op_sys'},$::bug{'priority'},$::bug{'component'},$::bug{'assigned_to'},$::bug{'reporter'},$qa_contact,DescCC($cclist),$target_milestone,${status_whiteboard},$::bug{'bug_file_loc'},DescDependencies($id);
+============================================================================+
| @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |
+----------------------------------------------------------------------------+
@@ -255,7 +258,7 @@ Component: $::bug{'component'}
AssignedTo: $::bug{'assigned_to'}
ReportedBy: $::bug{'reporter'}
$qa_contact$target_milestone${status_whiteboard}URL: $::bug{'bug_file_loc'}
-" . DescCC(\@cclist) . "Summary: $::bug{'short_desc'}
+" . DescCC($cclist) . "Summary: $::bug{'short_desc'}
" . DescDependencies($id) . "
$::bug{'long_desc'}
";
@@ -357,7 +360,9 @@ sub NewProcessOneBug {
$values{$i} = shift(@row);
}
my ($start, $end) = (@row);
- $values{'cc'} = ShowCcList($id);
+ my $ccSet = new RelationSet();
+ $ccSet->mergeFromDB("SELECT who FROM cc WHERE bug_id = $id");
+ $values{'cc'} = $ccSet->toString();
my @voterlist;
SendSQL("SELECT profiles.login_name FROM votes, profiles " .
@@ -463,137 +468,140 @@ sub NewProcessOneBug {
@voterlist,
@forcecc) {
$count++;
- if ($seen{$person}) {
- next;
- }
-
- SendSQL("SELECT userid, emailnotification, newemailtech," .
- " groupset & $values{'groupset'} " .
- "FROM profiles WHERE login_name = " . SqlQuote($person));
- my ($userid, $emailnotification, $newemailtech,
- $groupset) = (FetchSQLData());
- if (!$newemailtech || !Param('newemailtech')) {
- next;
- }
- $seen{$person} = 1;
- if ($groupset ne $values{'groupset'}) {
- next;
- }
- if ($emailnotification eq "ExcludeSelfChanges" &&
- lc($person) eq $nametoexclude) {
- $didexclude = 1;
- next;
- }
- if ($emailnotification eq "CCOnly" && $count < 3) {
- next;
- }
- my %mailhead = %defmailhead;
+ NewProcessOnePerson($person, $count, \@headerlist, \%values,
+ \%defmailhead, \%fielddescription, $difftext,
+ $newcomments, $start, $id, 1);
+ }
-# SendSQL("SELECT name, mailhead, maildiffs FROM diffprefs, fielddefs WHERE fielddefs.fieldid = diffprefs.fieldid AND userid = $userid");
-# while (MoreSQLData()) {
-# my ($field, $h, $d) = (FetchSQLData());
-# $mailhead{$field} = $h;
-# $maildiffs{$field} = $d;
-# }
+ SendSQL("UPDATE bugs SET lastdiffed = '$end', delta_ts = delta_ts " .
+ "WHERE bug_id = $id");
+}
-# my $maxlen = 0;
-# foreach my $f (keys %mailhead) {
-# if ($mailhead{$f}) {
-# my $l = length($fielddescription{$f});
-# if ($maxlen < $l) {
-# $maxlen = $l;
-# }
-# }
-# }
+sub NewProcessOnePerson ($$\@\%\%\%$$$$) {
+ my ($person, $count, $hlRef, $valueRef, $dmhRef, $fdRef, $difftext,
+ $newcomments, $start, $id, $checkWatchers) = @_;
+ my %values = %$valueRef;
+ my @headerlist = @$hlRef;
+ my %defmailhead = %$dmhRef;
+ my %fielddescription = %$fdRef;
- my $head = "";
-
- foreach my $f (@headerlist) {
- if ($mailhead{$f}) {
- my $value = $values{$f};
- if (!defined $value) {
- # Probaby ought to whine or something. ###
- next;
- }
- my $desc = $fielddescription{$f};
- $head .= FormatDouble($desc, $value);
-
-# my $extra = $maxlen - length($desc);
-# $head .= ($extra x " ");
-# $head .= $desc . ": ";
-# while (1) {
-# if (length($value) < 70) {
-# $head .= $value . "\n";
-# last;
-# }
-# my $pos = rindex($value, " ", 70);
-# if ($pos < 0) {
-# $pos = rindex($value, ",", 70);
-# if ($pos < 0) {
-# $pos = 70;
-# }
-# }
-# $head .= substr($value, 0, 70) . "\n";
-# $head .= (($extra + 2) x " ");
-# $value = substr($value, 70);
-# }
- }
- }
-
+ if ($seen{$person}) {
+ return;
+ }
-
- if ($difftext eq "" && $newcomments eq "") {
- # Whoops, no differences!
- next;
- }
-
- my $isnew = ($start !~ m/[1-9]/);
-
- my %substs;
- $substs{"neworchanged"} = $isnew ? "New" : "Changed";
- $substs{"to"} = $person;
- $substs{"cc"} = '';
- $substs{"bugid"} = $id;
- if ($isnew) {
- $substs{"diffs"} = $head . "\n\n" . $newcomments;
- } else {
- $substs{"diffs"} = $difftext . "\n\n" . $newcomments;
- }
- $substs{"summary"} = $values{'short_desc'};
+ SendSQL("SELECT userid, emailnotification, newemailtech," .
+ " groupset & $values{'groupset'} " .
+ "FROM profiles WHERE login_name = " . SqlQuote($person));
+ my ($userid, $emailnotification, $newemailtech,
+ $groupset) = (FetchSQLData());
+
+ # check for watchers, and recurse if we find any, but tell the
+ # recursive call not to check for watchers
+ #
+ if (Param("supportwatchers") && $checkWatchers) {
+ my $personId = DBname_to_id($person);
+ my $watcherSet = new RelationSet();
+ $watcherSet->mergeFromDB("SELECT watcher FROM watch WHERE" .
+ " watched = $personId");
+
+ foreach my $watcher ( $watcherSet->toArray() ) {
- my $template = Param("newchangedmail");
+ NewProcessOnePerson(DBID_to_name($watcher),
+ $count, \@headerlist, \%values,
+ \%defmailhead, \%fielddescription, $difftext,
+ $newcomments, $start, $id, 0);
+ }
- my $msg = PerformSubsts($template, \%substs);
- open(SENDMAIL, "|/usr/lib/sendmail -t") ||
- die "Can't open sendmail";
-
- print SENDMAIL trim($msg) . "\n";
- close SENDMAIL;
- push(@sentlist, $person);
}
-
-
+ if (!$newemailtech || !Param('newemailtech')) {
+ return;
+ }
+ $seen{$person} = 1;
- SendSQL("UPDATE bugs SET lastdiffed = '$end', delta_ts = delta_ts " .
- "WHERE bug_id = $id");
+ # if this person doesn't have permission to see info on this bug,
+ # return.
+ #
+ # XXX - I _think_ this currently means that if a bug is suddenly given
+ # more restrictive permissions, people without those permissions won't
+ # see the action of restricting the bug itself; the bug will just
+ # quietly disappear from their radar.
+ #
+ if ($groupset ne $values{'groupset'}) {
+ return;
+ }
+ if ($emailnotification eq "ExcludeSelfChanges" &&
+ lc($person) eq $nametoexclude) {
+ $didexclude = 1;
+ return;
+ }
+ # "$count < 3" means "this person is either assigned_to or reporter"
+ #
+ if ($emailnotification eq "CCOnly" && $count < 3) {
+ return;
+ }
+
+ my %mailhead = %defmailhead;
+
+ my $head = "";
+
+ foreach my $f (@headerlist) {
+ if ($mailhead{$f}) {
+ my $value = $values{$f};
+ if (!defined $value) {
+ # Probaby ought to whine or something. ###
+ next;
+ }
+ my $desc = $fielddescription{$f};
+ $head .= FormatDouble($desc, $value);
+ }
+ }
+
+ if ($difftext eq "" && $newcomments eq "") {
+ # Whoops, no differences!
+ return;
+ }
+
+ my $isnew = ($start !~ m/[1-9]/);
+
+ my %substs;
+ $substs{"neworchanged"} = $isnew ? "New" : "Changed";
+ $substs{"to"} = $person;
+ $substs{"cc"} = '';
+ $substs{"bugid"} = $id;
+ if ($isnew) {
+ $substs{"diffs"} = $head . "\n\n" . $newcomments;
+ } else {
+ $substs{"diffs"} = $difftext . "\n\n" . $newcomments;
+ }
+ $substs{"summary"} = $values{'short_desc'};
+
+ my $template = Param("newchangedmail");
+
+ my $msg = PerformSubsts($template, \%substs);
+ open(SENDMAIL, "|/usr/lib/sendmail -t") ||
+ die "Can't open sendmail";
+
+ print SENDMAIL trim($msg) . "\n";
+ close SENDMAIL;
+ push(@sentlist, $person);
+
}
-
sub ProcessOneBug {
- my $i = $_[0];
- NewProcessOneBug($i);
- my $old = "shadow/$i";
- my $new = "shadow/$i.tmp.$$";
- my $diffs = "shadow/$i.diffs.$$";
- my $verb = "Changed";
- if (!stat($old)) {
- mkdir "shadow", 0777;
- chmod 0777, "shadow";
+ my $i = $_[0];
+ NewProcessOneBug($i);
+ my $old = "shadow/$i";
+ my $new = "shadow/$i.tmp.$$";
+ my $diffs = "shadow/$i.diffs.$$";
+ my $verb = "Changed";
+ if (!stat($old)) {
+ mkdir "shadow", 0777;
+ chmod 0777, "shadow";
open(OLD, ">$old") || die "Couldn't create null $old";
close OLD;
$verb = "New";
@@ -683,7 +691,7 @@ if (open(FID, "<data/nomail")) {
}
# To recreate the shadow database, run "processmail regenerate" .
-if ($ARGV[0] eq "regenerate") {
+if ($#ARGV >= 0 && $ARGV[0] eq "regenerate") {
$regenerate = 1;
shift @ARGV;
SendSQL("select bug_id from bugs order by bug_id");
@@ -700,7 +708,7 @@ if ($ARGV[0] eq "regenerate") {
exit;
}
-if ($ARGV[0] eq "-forcecc") {
+if ($#ARGV >= 0 && $ARGV[0] eq "-forcecc") {
shift(@ARGV);
foreach my $i (split(/,/, shift(@ARGV))) {
push(@forcecc, trim($i));
diff --git a/userprefs.cgi b/userprefs.cgi
index cc25fc69f..055270f0c 100755
--- a/userprefs.cgi
+++ b/userprefs.cgi
@@ -14,12 +14,15 @@
# The Original Code is the Bugzilla Bug Tracking System.
#
# Contributor(s): Terry Weissman <terry@mozilla.org>
+# Dan Mosedale <dmose@mozilla.org>
use diagnostics;
use strict;
require "CGI.pl";
+use RelationSet;
+
# Shut up misguided -w warnings about "used only once". "use vars" just
# doesn't work for me.
sub sillyness {
@@ -134,6 +137,29 @@ risk any bugs), check here.
EmitEntry("Check here to sign up (and risk any bugs)",
qq{<INPUT TYPE="checkbox" NAME="newemailtech" $checkedpart>New email tech});
}
+
+ if (Param("supportwatchers")) {
+ my $watcheduserSet = new RelationSet;
+ $watcheduserSet->mergeFromDB("SELECT watched FROM watch WHERE" .
+ " watcher=$userid");
+ my $watchedusers = $watcheduserSet->toString();
+
+ print qq{
+<TR><TD COLSPAN="2"><HR></TD></TR>
+<TR><TD COLSPAN="2"><FONT COLOR="red">New!</FONT>
+If you want to help cover for someone when they're on vacation, or if
+you need to do the QA related to all of their bugs, you can tell bugzilla
+to send mail related to their bugs to you also. List the email addresses
+of any users you wish to watch here, separated by commas.
+<FONT COLOR="red">Note that you MUST have the above "New email tech"
+button selected in order to use this feature.</FONT>
+</TD></TR>
+};
+ EmitEntry("Users to watch",
+ qq{<INPUT SIZE=35 NAME="watchedusers" VALUE="$watchedusers">});
+
+ }
+
}
sub SaveDiffs {
@@ -144,6 +170,47 @@ sub SaveDiffs {
SendSQL("UPDATE profiles " .
"SET emailnotification = " . SqlQuote($::FORM{'emailnotification'})
. ", newemailtech = $newemailtech WHERE userid = $userid");
+
+ # deal with any watchers
+ #
+ if (Param("supportwatchers") ) {
+
+ if (exists $::FORM{'watchedusers'}) {
+
+ Error ('You must have "New email tech" set to watch someone')
+ if ( $::FORM{'watchedusers'} ne "" && $newemailtech == 0);
+
+ # Just in case. Note that this much locking is actually overkill:
+ # we don't really care if anyone reads the watch table. So
+ # some small amount of contention could be gotten rid of by
+ # using user-defined locks rather than table locking.
+ #
+ SendSQL("LOCK TABLES watch WRITE, profiles READ");
+
+ # what the db looks like now
+ #
+ my $origWatchedUsers = new RelationSet;
+ $origWatchedUsers->mergeFromDB("SELECT watched FROM watch WHERE" .
+ " watcher=$userid");
+
+ # update the database to look like the form
+ #
+ my $newWatchedUsers = new RelationSet($::FORM{'watchedusers'});
+ my @CCDELTAS = $origWatchedUsers->generateSqlDeltas(
+ $newWatchedUsers,
+ "watch",
+ "watcher",
+ $userid,
+ "watched");
+ $CCDELTAS[0] eq "" || SendSQL($CCDELTAS[0]);
+ $CCDELTAS[1] eq "" || SendSQL($CCDELTAS[1]);
+
+ # all done
+ #
+ SendSQL("UNLOCK TABLES");
+
+ }
+ }
}