]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
allow users to watch the bugs of other users
authordmose%mozilla.org <>
Wed, 29 Mar 2000 05:30:23 +0000 (05:30 +0000)
committerdmose%mozilla.org <>
Wed, 29 Mar 2000 05:30:23 +0000 (05:30 +0000)
Bugzilla/RelationSet.pm [new file with mode: 0644]
README
RelationSet.pm [new file with mode: 0644]
bug_form.pl
checksetup.pl
defparams.pl
globals.pl
process_bug.cgi
processmail
userprefs.cgi

diff --git a/Bugzilla/RelationSet.pm b/Bugzilla/RelationSet.pm
new file mode 100644 (file)
index 0000000..ee402e7
--- /dev/null
@@ -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 65dfd0d771a8525461c7606d58f4f84a447c5ee4..017da0686a01656f3529fd3fbe15adc1caeef3db 100644 (file)
--- 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 (file)
index 0000000..ee402e7
--- /dev/null
@@ -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));
+}  
index 4453d7ff1c1871370cee9346b74c425a215a90b3..9b459d66cbf1f1d3fe51fe40948f891061f51de0 100644 (file)
@@ -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>" .
index 9a11dc5880748093d8ad03db1231e709cc55e7cb..17e5109402156679f3e218521d30d4c7f07eada6 100755 (executable)
@@ -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";
index 04e90ad0da29658fc16aa26d3014db1a1f976849..41a40faf01cfd16a68c93ed8598975f6e2bad685 100644 (file)
@@ -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;
 
index c870082c6963026bf55b9cd3944ad53c50604c87..58eb7738cdcf5587912bfcabd8f6b55fa2bc4ed7 100644 (file)
@@ -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};
index 4559af8b3888a937dabb2fe8305b4c4a525442b3..913ff8f18d38c94be0a07efda7416060342804fc 100755 (executable)
@@ -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)) {
index 9b9baa4ddf7332c026896c3cd6a55d4aa9d6e5f8..8c8ad8719e05c7ccdd51ec496568b5fc6c1643c9 100755 (executable)
@@ -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));
index cc25fc69fc59e007216ea7a7458a2a0e78d78041..055270f0cfdc653eca5c4bda35ace3e66740f726 100755 (executable)
 # 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");
+        
+        }
+    }
 }