diff options
-rw-r--r-- | Bugzilla/RelationSet.pm | 211 | ||||
-rw-r--r-- | README | 4 | ||||
-rw-r--r-- | RelationSet.pm | 211 | ||||
-rw-r--r-- | bug_form.pl | 10 | ||||
-rwxr-xr-x | checksetup.pl | 85 | ||||
-rw-r--r-- | defparams.pl | 6 | ||||
-rw-r--r-- | globals.pl | 27 | ||||
-rwxr-xr-x | process_bug.cgi | 67 | ||||
-rwxr-xr-x | processmail | 266 | ||||
-rwxr-xr-x | userprefs.cgi | 67 |
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)); +} @@ -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"); + + } + } } |