diff --git a/CGI.pl b/CGI.pl index 07633e4d85bbe6788bec34e793c9ed69152a659d..2e782b6dad34211692546f1c02dae758aa1a999d 100644 --- a/CGI.pl +++ b/CGI.pl @@ -18,18 +18,21 @@ # Rights Reserved. # # Contributor(s): Terry Weissman <terry@mozilla.org> +# Dan Mosedale <dmose@mozilla.org> # Contains some global routines used throughout the CGI scripts of Bugzilla. use diagnostics; use strict; - +# use Carp; # for confess # Shut up misguided -w warnings about "used only once". For some reason, # "use vars" chokes on me when I try it here. sub CGI_pl_sillyness { my $zz; $zz = %::FILENAME; + $zz = %::MFORM; + $zz = %::dontchange; } use CGI::Carp qw(fatalsToBrowser); @@ -76,10 +79,10 @@ sub url_quote { } -sub ProcessFormFields { - my ($buffer) = (@_); - undef %::FORM; - undef %::MFORM; +sub ParseUrlString { + my ($buffer, $f, $m) = (@_); + undef %$f; + undef %$m; my %isnull; my $remaining = $buffer; @@ -103,13 +106,13 @@ sub ProcessFormFields { $value = ""; } if ($value ne "") { - if (defined $::FORM{$name}) { - $::FORM{$name} .= $value; - my $ref = $::MFORM{$name}; + if (defined $f->{$name}) { + $f->{$name} .= $value; + my $ref = $m->{$name}; push @$ref, $value; } else { - $::FORM{$name} = $value; - $::MFORM{$name} = [$value]; + $f->{$name} = $value; + $m->{$name} = [$value]; } } else { $isnull{$name} = 1; @@ -117,15 +120,21 @@ sub ProcessFormFields { } if (defined %isnull) { foreach my $name (keys(%isnull)) { - if (!defined $::FORM{$name}) { - $::FORM{$name} = ""; - $::MFORM{$name} = []; + if (!defined $f->{$name}) { + $f->{$name} = ""; + $m->{$name} = []; } } } } +sub ProcessFormFields { + my ($buffer) = (@_); + return ParseUrlString($buffer, \%::FORM, \%::MFORM); +} + + sub ProcessMultipartFormFields { my ($boundary) = (@_); $boundary =~ s/^-*//; @@ -169,10 +178,59 @@ sub ProcessMultipartFormFields { $::FORM{$i} =~ s/\r$//; } } - +# check and see if a given field exists, is non-empty, and is set to a +# legal value. assume a browser bug and abort appropriately if not. +# if $legalsRef is not passed, just check to make sure the value exists and +# is non-NULL +# +sub CheckFormField (\%$;\@) { + my ($formRef, # a reference to the form to check (a hash) + $fieldname, # the fieldname to check + $legalsRef # (optional) ref to a list of legal values + ) = @_; + + if ( !defined $formRef->{$fieldname} || + trim($formRef->{$fieldname}) eq "" || + (defined($legalsRef) && + lsearch($legalsRef, $formRef->{$fieldname})<0) ){ + + print "A legal $fieldname was not set; "; + print Param("browserbugmessage"); + PutFooter(); + exit 0; + } +} +# check and see if a given field is defined, and abort if not +# +sub CheckFormFieldDefined (\%$) { + my ($formRef, # a reference to the form to check (a hash) + $fieldname, # the fieldname to check + ) = @_; + + if ( !defined $formRef->{$fieldname} ) { + print "$fieldname was not defined; "; + print Param("browserbugmessage"); + PutFooter(); + exit 0; + } +} + +# check and see if a given string actually represents a positive +# integer, and abort if not. +# +sub CheckPosInt($) { + my ($number) = @_; # the fieldname to check + + if ( $number !~ /^[1-9][0-9]*$/ ) { + print "Received string \"$number\" when postive integer expected; "; + print Param("browserbugmessage"); + PutFooter(); + exit 0; + } +} sub FormData { my ($field) = (@_); @@ -217,11 +275,150 @@ sub navigation_header { } else { print "<I><FONT COLOR=\#777777>Next</FONT></I>\n"; } + print qq{ <A HREF="buglist.cgi?regetlastlist=1">Show list</A>\n}; } print " <A HREF=query.cgi>Query page</A>\n"; print " <A HREF=enter_bug.cgi>Enter new bug</A>\n" } +sub make_checkboxes { + my ($src,$default,$isregexp,$name) = (@_); + my $last = ""; + my $capitalized = ""; + my $popup = ""; + my $found = 0; + $default = "" if !defined $default; + + if ($src) { + foreach my $item (@$src) { + if ($item eq "-blank-" || $item ne $last) { + if ($item eq "-blank-") { + $item = ""; + } + $last = $item; + $capitalized = $item; + $capitalized =~ tr/A-Z/a-z/; + $capitalized =~ s/^(.?)(.*)/\u$1$2/; + if ($isregexp ? $item =~ $default : $default eq $item) { + $popup .= "<INPUT NAME=$name TYPE=CHECKBOX VALUE=\"$item\" CHECKED>$capitalized<br>"; + $found = 1; + } else { + $popup .= "<INPUT NAME=$name TYPE=CHECKBOX VALUE=\"$item\">$capitalized<br>"; + } + } + } + } + if (!$found && $default ne "") { + $popup .= "<INPUT NAME=$name TYPE=CHECKBOX CHECKED>$default"; + } + return $popup; +} + +# +# make_selection_widget: creates an HTML selection widget from a list of text strings. +# $groupname is the name of the setting (form value) that this widget will control +# $src is the list of options +# you can specify a $default value which is either a string or a regex pattern to match to +# identify the default value +# $capitalize lets you optionally capitalize the option strings; the default is the value +# of Param("capitalizelists") +# $multiple is 1 if several options are selectable (default), 0 otherwise. +# $size is used for lists to control how many items are shown. The default is 7. A list of +# size 1 becomes a popup menu. +# $preferLists is 1 if selection lists should be used in favor of radio buttons and +# checkboxes, and 0 otherwise. The default is the value of Param("preferlists"). +# +# The actual widget generated depends on the parameter settings: +# +# MULTIPLE PREFERLISTS SIZE RESULT +# 0 (single) 0 =1 Popup Menu (normal for list of size 1) +# 0 (single) 0 >1 Radio buttons +# 0 (single) 1 =1 Popup Menu (normal for list of size 1) +# 0 (single) 1 n>1 List of size n, single selection +# 1 (multi) 0 n/a Check boxes; size ignored +# 1 (multi) 1 n/a List of size n, multiple selection, of size n +# +sub make_selection_widget { + my ($groupname,$src,$default,$isregexp,$multiple, $size, $capitalize, $preferLists) = (@_); + my $last = ""; + my $popup = ""; + my $found = 0; + my $displaytext = ""; + $groupname = "" if !defined $groupname; + $default = "" if !defined $default; + $capitalize = Param("capitalizelists") if !defined $capitalize; + $multiple = 1 if !defined $multiple; + $preferLists = Param("preferlists") if !defined $preferLists; + $size = 7 if !defined $size; + my $type = "LIST"; + if (!$preferLists) { + if ($multiple) { + $type = "CHECKBOX"; + } else { + if ($size > 1) { + $type = "RADIO"; + } + } + } + + if ($type eq "LIST") { + $popup .= "<SELECT NAME=\"$groupname\""; + if ($multiple) { + $popup .= " MULTIPLE"; + } + $popup .= " SIZE=$size>\n"; + } + if ($src) { + foreach my $item (@$src) { + if ($item eq "-blank-" || $item ne $last) { + if ($item eq "-blank-") { + $item = ""; + } + $last = $item; + $displaytext = $item; + if ($capitalize) { + $displaytext =~ tr/A-Z/a-z/; + $displaytext =~ s/^(.?)(.*)/\u$1$2/; + } + + if ($isregexp ? $item =~ $default : $default eq $item) { + if ($type eq "CHECKBOX") { + $popup .= "<INPUT NAME=$groupname type=checkbox VALUE=\"$item\" CHECKED>$displaytext<br>"; + } elsif ($type eq "RADIO") { + $popup .= "<INPUT NAME=$groupname type=radio VALUE=\"$item\" check>$displaytext<br>"; + } else { + $popup .= "<OPTION SELECTED VALUE=\"$item\">$displaytext"; + } + $found = 1; + } else { + if ($type eq "CHECKBOX") { + $popup .= "<INPUT NAME=$groupname type=checkbox VALUE=\"$item\">$displaytext<br>"; + } elsif ($type eq "RADIO") { + $popup .= "<INPUT NAME=$groupname type=radio VALUE=\"$item\">$displaytext<br>"; + } else { + $popup .= "<OPTION VALUE=\"$item\">$displaytext"; + } + } + } + } + } + if (!$found && $default ne "") { + if ($type eq "CHECKBOX") { + $popup .= "<INPUT NAME=$groupname type=checkbox CHECKED>$default"; + } elsif ($type eq "RADIO") { + $popup .= "<INPUT NAME=$groupname type=radio checked>$default"; + } else { + $popup .= "<OPTION SELECTED>$default"; + } + } + if ($type eq "LIST") { + $popup .= "</SELECT>"; + } + return $popup; +} + + +$::CheckOptionValues = 1; sub make_options { my ($src,$default,$isregexp) = (@_); @@ -247,7 +444,25 @@ sub make_options { } } if (!$found && $default ne "") { + if ( Param("strictvaluechecks") && $::CheckOptionValues && + ($default ne $::dontchange) && ($default ne "-All-") && + ($default ne "DUPLICATE") ) { + print "Possible bug database corruption has been detected. " . + "Please send mail to " . Param("maintainer") . " with " . + "details of what you were doing when this message " . + "appeared. Thank you.\n"; + if (!$src) { + $src = ["???null???"]; + } + print "<pre>src = " . value_quote(join(' ', @$src)) . "\n"; + print "default = " . value_quote($default) . "</pre>"; + PutFooter(); +# confess "Gulp."; + exit 0; + + } else { $popup .= "<OPTION SELECTED>$default"; + } } return $popup; } @@ -272,6 +487,28 @@ sub make_popup { } +sub BuildPulldown { + my ($name, $valuelist, $default) = (@_); + + my $entry = qq{<SELECT NAME="$name">}; + foreach my $i (@$valuelist) { + my ($tag, $desc) = (@$i); + my $selectpart = ""; + if ($tag eq $default) { + $selectpart = " SELECTED"; + } + if (!defined $desc) { + $desc = $tag; + } + $entry .= qq{<OPTION$selectpart VALUE="$tag">$desc\n}; + } + $entry .= qq{</SELECT>}; + return $entry; +} + + + + sub PasswordForLogin { my ($login) = (@_); SendSQL("select cryptpassword from profiles where login_name = " . @@ -287,31 +524,47 @@ sub PasswordForLogin { sub quietly_check_login() { $::usergroupset = '0'; my $loginok = 0; + $::disabledreason = ''; + $::userid = 0; if (defined $::COOKIE{"Bugzilla_login"} && defined $::COOKIE{"Bugzilla_logincookie"}) { ConnectToDatabase(); if (!defined $ENV{'REMOTE_HOST'}) { $ENV{'REMOTE_HOST'} = $ENV{'REMOTE_ADDR'}; } - SendSQL("select profiles.groupset, profiles.login_name, " . + SendSQL("SELECT profiles.userid, profiles.groupset, " . + "profiles.login_name, " . "profiles.login_name = " . SqlQuote($::COOKIE{"Bugzilla_login"}) . - " and profiles.cryptpassword = logincookies.cryptpassword " . - "and logincookies.hostname = " . + " AND profiles.cryptpassword = logincookies.cryptpassword " . + "AND logincookies.hostname = " . SqlQuote($ENV{"REMOTE_HOST"}) . - " from profiles,logincookies where logincookies.cookie = " . + ", profiles.disabledtext " . + " FROM profiles, logincookies WHERE logincookies.cookie = " . SqlQuote($::COOKIE{"Bugzilla_logincookie"}) . - " and profiles.userid = logincookies.userid"); + " AND profiles.userid = logincookies.userid"); my @row; if (@row = FetchSQLData()) { - $loginok = $row[2]; - if ($loginok) { - $::usergroupset = $row[0]; - $::COOKIE{"Bugzilla_login"} = $row[1]; # Makes sure case is in - # canonical form. + my ($userid, $groupset, $loginname, $ok, $disabledtext) = (@row); + if ($ok) { + if ($disabledtext eq '') { + $loginok = 1; + $::userid = $userid; + $::usergroupset = $groupset; + $::COOKIE{"Bugzilla_login"} = $loginname; # Makes sure case + # is in + # canonical form. + } else { + $::disabledreason = $disabledtext; + } } } } + # if 'who' is passed in, verify that it's a good value + if ($::FORM{'who'}) { + my $whoid = DBname_to_id($::FORM{'who'}); + delete $::FORM{'who'} unless $whoid; + } if (!$loginok) { delete $::COOKIE{"Bugzilla_login"}; } @@ -333,6 +586,7 @@ sub CheckEmailSyntax { print "syntax checking for a legal email address.\n"; print Param('emailregexpdesc'); print "<p>Please click <b>back</b> and try again.\n"; + PutFooter(); exit; } } @@ -342,23 +596,11 @@ sub CheckEmailSyntax { sub MailPassword { my ($login, $password) = (@_); my $urlbase = Param("urlbase"); - my $template = "From: bugzilla-daemon -To: %s -Subject: Your bugzilla password. - -To use the wonders of bugzilla, you can use the following: - - E-mail address: %s - Password: %s - - To change your password, go to: - ${urlbase}changepassword.cgi - - (Your bugzilla and CVS password, if any, are not currently synchronized. - Top hackers are working around the clock to fix this, as you read this.) -"; - my $msg = sprintf($template, $login . Param('emailsuffix'), - $login, $password); + my $template = Param("passwordmail"); + my $msg = PerformSubsts($template, + {"mailaddress" => $login . Param('emailsuffix'), + "login" => $login, + "password" => $password}); open SENDMAIL, "|/usr/lib/sendmail -t"; print SENDMAIL $msg; @@ -397,17 +639,22 @@ sub confirm_login { $realpwd = FetchOneColumn(); } print "Content-type: text/html\n\n"; - PutHeader("<H1>Password has been emailed"); + PutHeader("Password has been emailed"); MailPassword($enteredlogin, $realpwd); + PutFooter(); exit; } - my $enteredcryptpwd = crypt($enteredpwd, substr($realcryptpwd, 0, 2)); + SendSQL("SELECT encrypt(" . SqlQuote($enteredpwd) . ", " . + SqlQuote(substr($realcryptpwd, 0, 2)) . ")"); + my $enteredcryptpwd = FetchOneColumn(); + if ($realcryptpwd eq "" || $enteredcryptpwd ne $realcryptpwd) { print "Content-type: text/html\n\n"; PutHeader("Login failed"); print "The username or password you entered is not valid.\n"; print "Please click <b>Back</b> and try again.\n"; + PutFooter(); exit; } $::COOKIE{"Bugzilla_login"} = $enteredlogin; @@ -433,8 +680,24 @@ sub confirm_login { my $loginok = quietly_check_login(); if ($loginok != 1) { + if ($::disabledreason) { + print "Set-Cookie: Bugzilla_login= ; path=/; expires=Sun, 30-Jun-80 00:00:00 GMT +Set-Cookie: Bugzilla_logincookie= ; path=/; expires=Sun, 30-Jun-80 00:00:00 GMT +Set-Cookie: Bugzilla_password= ; path=/; expires=Sun, 30-Jun-80 00:00:00 GMT +Content-type: text/html + +"; + PutHeader("Your account has been disabled"); + print $::disabledreason; + print "<HR>\n"; + print "If you believe your account should be restored, please\n"; + print "send email to " . Param("maintainer") . " explaining\n"; + print "why.\n"; + PutFooter(); + exit(); + } print "Content-type: text/html\n\n"; - PutHeader("Login"); + PutHeader("Login", undef, undef, undef, 1); print "I need a legitimate e-mail address and password to continue.\n"; if (!defined $nexturl || $nexturl eq "") { # Sets nexturl to be argv0, stripping everything up to and @@ -476,19 +739,27 @@ name=PleaseMailAPassword> # This seems like as good as time as any to get rid of old # crufty junk in the logincookies table. Get rid of any entry # that hasn't been used in a month. - SendSQL("delete from logincookies where to_days(now()) - to_days(lastused) > 30"); + if ($::dbwritesallowed) { + SendSQL("DELETE FROM logincookies " . + "WHERE TO_DAYS(NOW()) - TO_DAYS(lastused) > 30"); + } + PutFooter(); exit; } # Update the timestamp on our logincookie, so it'll keep on working. - SendSQL("update logincookies set lastused = null where cookie = $::COOKIE{'Bugzilla_logincookie'}"); + if ($::dbwritesallowed) { + SendSQL("UPDATE logincookies SET lastused = null " . + "WHERE cookie = $::COOKIE{'Bugzilla_logincookie'}"); + } + return $::userid; } sub PutHeader { - my ($title, $h1, $h2, $extra) = (@_); + my ($title, $h1, $h2, $extra, $ignoreshutdown, $jscript) = (@_); if (!defined $h1) { $h1 = $title; @@ -499,47 +770,107 @@ sub PutHeader { if (!defined $extra) { $extra = ""; } + $jscript ||= ""; print "<HTML><HEAD>\n<TITLE>$title</TITLE>\n"; - print Param("headerhtml") . "\n</HEAD>\n"; + print Param("headerhtml") . "\n$jscript\n</HEAD>\n"; print "<BODY BGCOLOR=\"#FFFFFF\" TEXT=\"#000000\"\n"; print "LINK=\"#0000EE\" VLINK=\"#551A8B\" ALINK=\"#FF0000\" $extra>\n"; print PerformSubsts(Param("bannerhtml"), undef); - print "<TABLE BORDER=0 CELLPADDING=12 CELLSPACING=0 WIDTH=\"100%\">\n"; + print "<TABLE BORDER=0 CELLSPACING=0 WIDTH=\"100%\">\n"; print " <TR>\n"; - print " <TD>\n"; + print " <TD WIDTH=10% VALIGN=TOP ALIGN=LEFT>\n"; print " <TABLE BORDER=0 CELLPADDING=0 CELLSPACING=2>\n"; - print " <TR><TD VALIGN=TOP ALIGN=CENTER NOWRAP>\n"; - print " <FONT SIZE=\"+3\"><B><NOBR>$h1</NOBR></B></FONT>\n"; - print " </TD></TR><TR><TD VALIGN=TOP ALIGN=CENTER>\n"; - print " <B>$h2</B>\n"; + print " <TR><TD VALIGN=TOP ALIGN=LEFT NOWRAP>\n"; + print " <FONT SIZE=+1><B>$h1</B></FONT>"; print " </TD></TR>\n"; print " </TABLE>\n"; print " </TD>\n"; - print " <TD>\n"; + print " <TD VALIGN=CENTER> </TD>\n"; + print " <TD VALIGN=CENTER ALIGN=LEFT>\n"; - print Param("blurbhtml"); + print "$h2\n"; + print "</TD></TR></TABLE>\n"; print "</TD></TR></TABLE>\n"; + + if (Param("shutdownhtml")) { + if (!$ignoreshutdown) { + print Param("shutdownhtml"); + exit; + } + } +} + + +sub PutFooter { + print PerformSubsts(Param("footerhtml")); + SyncAnyPendingShadowChanges(); } +sub PuntTryAgain ($) { + my ($str) = (@_); + print PerformSubsts(Param("errorhtml"), + {errormsg => $str}); + PutFooter(); + exit; +} + + +sub CheckIfVotedConfirmed { + my ($id, $who) = (@_); + SendSQL("SELECT bugs.votes, bugs.bug_status, products.votestoconfirm, " . + " bugs.everconfirmed " . + "FROM bugs, products " . + "WHERE bugs.bug_id = $id AND products.product = bugs.product"); + my ($votes, $status, $votestoconfirm, $everconfirmed) = (FetchSQLData()); + if ($votes >= $votestoconfirm && $status eq $::unconfirmedstate) { + SendSQL("UPDATE bugs SET bug_status = 'NEW', everconfirmed = 1 " . + "WHERE bug_id = $id"); + my $fieldid = GetFieldID("bug_status"); + SendSQL("INSERT INTO bugs_activity " . + "(bug_id,who,bug_when,fieldid,oldvalue,newvalue) VALUES " . + "($id,$who,now(),$fieldid,'$::unconfirmedstate','NEW')"); + if (!$everconfirmed) { + $fieldid = GetFieldID("everconfirmed"); + SendSQL("INSERT INTO bugs_activity " . + "(bug_id,who,bug_when,fieldid,oldvalue,newvalue) VALUES " . + "($id,$who,now(),$fieldid,'0','1')"); + } + AppendComment($id, DBID_to_name($who), + "*** This bug has been confirmed by popular vote. ***"); + print "<TABLE BORDER=1><TD><H2>Bug $id has been confirmed by votes.</H2>\n"; + system("./processmail", $id); + print "<TD><A HREF=\"show_bug.cgi?id=$id\">Go To BUG# $id</A></TABLE>\n"; + } + +} + + + sub DumpBugActivity { my ($id, $starttime) = (@_); my $datepart = ""; + + die "Invalid id: $id" unless $id=~/^\s*\d+\s*$/; + if (defined $starttime) { $datepart = "and bugs_activity.bug_when >= $starttime"; } my $query = " - select bugs_activity.field, bugs_activity.bug_when, + SELECT IFNULL(fielddefs.name, bugs_activity.fieldid), + bugs_activity.bug_when, bugs_activity.oldvalue, bugs_activity.newvalue, profiles.login_name - from bugs_activity,profiles - where bugs_activity.bug_id = $id $datepart - and profiles.userid = bugs_activity.who - order by bugs_activity.bug_when"; + FROM bugs_activity LEFT JOIN fielddefs ON + bugs_activity.fieldid = fielddefs.fieldid, + profiles + WHERE bugs_activity.bug_id = $id $datepart + AND profiles.userid = bugs_activity.who + ORDER BY bugs_activity.bug_when"; SendSQL($query); @@ -571,9 +902,68 @@ sub DumpBugActivity { } - - - +sub GetCommandMenu { + my $loggedin = quietly_check_login(); + my $html = qq{<FORM METHOD=GET ACTION="show_bug.cgi">}; + $html .= "<a href='enter_bug.cgi'>New</a> | <a href='query.cgi'>Query</a>"; + if (-e "query2.cgi") { + $html .= "[<a href='query2.cgi'>beta</a>]"; + } + + $html .= + qq{ | <INPUT TYPE=SUBMIT VALUE="Find"> bug \# <INPUT NAME=id SIZE=6>}; + + $html .= " | <a href='reports.cgi'>Reports</a>"; + if ($loggedin) { + my $mybugstemplate = Param("mybugstemplate"); + my %substs; + $substs{'userid'} = url_quote($::COOKIE{"Bugzilla_login"}); + if (!defined $::anyvotesallowed) { + GetVersionTable(); + } + if ($::anyvotesallowed) { + $html .= qq{ | <A HREF="showvotes.cgi"><NOBR>My votes</NOBR></A>}; + } + SendSQL("SELECT mybugslink, userid, blessgroupset FROM profiles " . + "WHERE login_name = " . SqlQuote($::COOKIE{'Bugzilla_login'})); + my ($mybugslink, $userid, $blessgroupset) = (FetchSQLData()); + if ($mybugslink) { + my $mybugsurl = PerformSubsts($mybugstemplate, \%substs); + $html = $html . " | <A HREF='$mybugsurl'><NOBR>My bugs</NOBR></A>"; + } + SendSQL("SELECT name,query FROM namedqueries " . + "WHERE userid = $userid AND linkinfooter"); + while (MoreSQLData()) { + my ($name, $query) = (FetchSQLData()); + $html .= qq{ | <A HREF="buglist.cgi?$query"><NOBR>$name</A>}; + } + $html .= " | <NOBR>Edit <a href='userprefs.cgi'>prefs</a></NOBR>"; + if (UserInGroup("tweakparams")) { + $html .= ", <a href=editparams.cgi>parameters</a>"; + $html .= ", <a href=sanitycheck.cgi><NOBR>sanity check</NOBR></a>"; + } + if (UserInGroup("editusers") || $blessgroupset) { + $html .= ", <a href=editusers.cgi>users</a>"; + } + if (UserInGroup("editcomponents")) { + $html .= ", <a href=editproducts.cgi>components</a>"; + } + if (UserInGroup("creategroups")) { + $html .= ", <a href=editgroups.cgi>groups</a>"; + } + if (UserInGroup("editkeywords")) { + $html .= ", <a href=editkeywords.cgi>keywords</a>"; + } + $html .= " | <NOBR><a href=relogin.cgi>Log out</a> $::COOKIE{'Bugzilla_login'}</NOBR>"; + } else { + $html .= + " | <a href=\"createaccount.cgi\"><NOBR>New account</NOBR></a>\n"; + $html .= + " | <NOBR><a href=query.cgi?GoAheadAndLogIn=1>Log in</a></NOBR>"; + } + $html .= "</FORM>"; + return $html; +} ############# Live code below here (that is, not subroutine defs) ############# @@ -596,7 +986,7 @@ if (defined $ENV{"REQUEST_METHOD"}) { } ProcessFormFields $::buffer; } else { - if ($ENV{"CONTENT_TYPE"} =~ + if (exists($ENV{"CONTENT_TYPE"}) && $ENV{"CONTENT_TYPE"} =~ m@multipart/form-data; boundary=\s*([^; ]+)@) { ProcessMultipartFormFields($1); $::buffer = ""; diff --git a/CVS/Entries b/CVS/Entries index f9c59d7a3119e804646f4378642c391cdb6568e6..36a10e5f03c1a6ab0199ba57726cab4edefd300b 100644 --- a/CVS/Entries +++ b/CVS/Entries @@ -1,57 +1,64 @@ /.cvsignore/1.2/Wed Oct 20 16:26:04 1999// /1x1.gif/1.1/Wed Aug 26 06:14:15 1998/-kb/ -/CGI.pl/1.29/Mon Nov 1 23:33:03 1999// +/CGI.pl/1.66/Mon May 8 18:12:28 2000// /CHANGES/1.38/Tue Oct 12 16:57:57 1999// -/README/1.29/Tue Oct 12 20:28:39 1999// -/addcomponent.cgi/1.2/Mon Nov 1 23:33:04 1999// +/RelationSet.pm/1.1/Tue Mar 28 21:30:43 2000// /ant.jpg/1.2/Wed Aug 26 22:36:05 1998/-kb/ -/backdoor.cgi/1.11/Mon Nov 1 23:33:04 1999// -/bug_form.pl/1.25/Mon Nov 1 23:33:04 1999// -/bug_status.html/1.7/Mon Nov 1 23:33:06 1999// -/buglist.cgi/1.57/Mon Nov 1 23:33:06 1999// -/changepassword.cgi/1.13/Mon Nov 1 23:33:07 1999// -/checksetup.pl/1.6/Mon Nov 1 23:33:07 1999// -/colchange.cgi/1.10/Mon Nov 1 23:33:08 1999// +/backdoor.cgi/1.13/Tue Mar 7 20:03:52 2000// +/booleanchart.html/1.2/Fri Jan 28 20:36:26 2000// +/bug_form.pl/1.45/Wed Apr 26 01:44:28 2000// +/bug_status.html/1.10/Sat Apr 8 06:04:15 2000// +/buglist.cgi/1.103/Wed Apr 26 17:44:24 2000// +/bugwritinghelp.html/1.1/Tue Mar 7 17:59:38 2000// +/changepassword.cgi/1.19/Tue Jan 25 07:53:29 2000// +/checksetup.pl/1.37/Sun Apr 30 04:34:53 2000// +/colchange.cgi/1.14/Fri Feb 4 14:08:00 2000// /collectstats.pl/1.7/Mon Nov 1 23:33:09 1999// -/createaccount.cgi/1.5/Mon Nov 1 23:33:09 1999// -/createattachment.cgi/1.8/Mon Nov 1 23:33:09 1999// -/defparams.pl/1.22/Mon Nov 1 23:33:10 1999// -/describecomponents.cgi/1.3/Mon Nov 1 23:33:11 1999// -/doaddcomponent.cgi/1.3/Mon Nov 1 23:33:11 1999// -/doeditcomponents.cgi/1.5/Mon Nov 1 23:33:11 1999// -/doeditowners.cgi/1.5/Mon Nov 1 23:33:12 1999// -/doeditparams.cgi/1.7/Mon Nov 1 23:33:12 1999// -/doeditvotes.cgi/1.5/Mon Nov 1 23:33:12 1999// -/editcomponents.cgi/1.8/Mon Nov 1 23:33:12 1999// -/editowners.cgi/1.5/Mon Nov 1 23:33:13 1999// -/editparams.cgi/1.9/Mon Nov 1 23:33:13 1999// -/editproducts.cgi/1.4/Mon Nov 1 23:33:14 1999// -/editusers.cgi/1.2/Mon Nov 1 23:33:14 1999// -/editversions.cgi/1.3/Mon Nov 1 23:33:14 1999// -/enter_bug.cgi/1.24/Mon Nov 1 23:33:15 1999// -/help.html/1.3/Mon Nov 1 23:33:16 1999// +/confirmhelp.html/1.1/Thu Feb 17 05:15:22 2000// +/createaccount.cgi/1.6/Fri Jan 14 22:35:29 2000// +/createattachment.cgi/1.10/Tue Mar 7 18:22:50 2000// +/defparams.pl/1.37/Tue Mar 28 21:31:16 2000// +/describecomponents.cgi/1.4/Fri Jan 14 22:35:30 2000// +/describekeywords.cgi/1.4/Sat Jan 22 16:51:49 2000// +/doeditparams.cgi/1.10/Thu Feb 17 21:41:32 2000// +/doeditvotes.cgi/1.7/Thu Feb 17 05:15:22 2000// +/editcomponents.cgi/1.11/Sat Jan 22 19:25:33 2000// +/editgroups.cgi/1.4/Wed Apr 26 19:35:45 2000// +/editkeywords.cgi/1.5/Sat Jan 22 16:51:49 2000// +/editmilestones.cgi/1.2/Thu Mar 23 18:22:34 2000// +/editparams.cgi/1.11/Fri Jan 14 22:35:34 2000// +/editproducts.cgi/1.14/Thu Mar 23 18:22:56 2000// +/editusers.cgi/1.8/Thu Feb 17 16:46:36 2000// +/editversions.cgi/1.5/Fri Jan 14 22:35:36 2000// +/enter_bug.cgi/1.36/Wed Apr 26 19:35:50 2000// +/help.html/1.4/Fri Jan 21 22:01:11 2000// /helpemailquery.html/1.1/Tue Jan 19 00:07:45 1999// /how_to_mail.html/1.2/Mon Nov 1 23:33:16 1999// -/index.html/1.9/Mon Nov 1 23:33:16 1999// -/long_list.cgi/1.11/Mon Nov 1 23:33:16 1999// -/new_comment.cgi/1.3/Mon Nov 1 23:33:16 1999// +/index.html/1.11/Thu Jan 27 00:56:33 2000// +/long_list.cgi/1.13/Fri Mar 10 18:01:26 2000// +/new_comment.cgi/1.4/Fri Apr 28 18:51:11 2000// /newquip.html/1.3/Mon Nov 1 23:33:17 1999// /notargetmilestone.html/1.1/Wed Feb 3 02:46:51 1999// -/post_bug.cgi/1.12/Fri Nov 19 14:50:57 1999// -/process_bug.cgi/1.25/Mon Nov 1 23:33:17 1999// -/processmail/1.31/Wed Nov 3 00:44:29 1999// -/query.cgi/1.47/Mon Nov 1 23:33:18 1999// -/relogin.cgi/1.8/Mon Nov 1 23:33:18 1999// -/reports.cgi/1.25/Mon Nov 1 23:33:19 1999// -/sanitycheck.cgi/1.14/Mon Nov 1 23:33:19 1999// -/show_activity.cgi/1.4/Mon Nov 1 23:33:19 1999// -/show_bug.cgi/1.10/Mon Nov 1 23:33:19 1999// -/showattachment.cgi/1.4/Mon Nov 1 23:33:20 1999// -/showdependencygraph.cgi/1.7/Mon Nov 1 23:33:20 1999// -/showdependencytree.cgi/1.6/Fri Nov 19 15:51:48 1999// -/showowners.cgi/1.4/Mon Nov 1 23:33:21 1999// -/showvotes.cgi/1.3/Mon Nov 1 23:33:21 1999// +/post_bug.cgi/1.26/Wed Apr 26 19:35:51 2000// +/process_bug.cgi/1.55/Wed Mar 29 00:34:56 2000// +/processmail/1.41/Wed Apr 12 00:25:37 2000// +/query.cgi/1.69/Tue Mar 21 16:47:06 2000// +/relogin.cgi/1.10/Tue Jan 18 14:41:00 2000// +/reports.cgi/1.31/Tue Apr 25 21:05:32 2000// +/sanitycheck.cgi/1.23/Tue Mar 21 16:47:06 2000// +/show_activity.cgi/1.5/Fri Jan 14 22:35:45 2000// +/show_bug.cgi/1.11/Fri Jan 14 22:35:46 2000// +/showattachment.cgi/1.6/Tue Mar 7 19:27:41 2000// +/showdependencygraph.cgi/1.9/Tue Mar 7 18:23:00 2000// +/showdependencytree.cgi/1.7/Fri Jan 14 22:35:48 2000// +/showvotes.cgi/1.5/Thu Feb 17 05:15:23 2000// +/syncshadowdb/1.6/Tue Mar 21 14:39:23 2000// +/userprefs.cgi/1.7/Fri Apr 21 18:43:53 2000// /votehelp.html/1.4/Mon Nov 1 23:33:21 1999// /whineatnews.pl/1.4/Mon Nov 1 23:33:22 1999// D/contrib//// -/globals.pl/1.33/Fri Nov 19 15:56:18 1999// +D/docs//// +D/oracle//// +D/template//// +/README/1.35/Mon May 8 19:47:05 2000// +/globals.pl/1.67/Mon May 8 20:44:33 2000// diff --git a/CVS/Root b/CVS/Root index db0def341f4c0526aa7407d123007dbb03cee412..cdb6f4a0739a0dc53e628026726036377dec3637 100644 --- a/CVS/Root +++ b/CVS/Root @@ -1 +1 @@ -:pserver:terry%mozilla.org@cvs.mozilla.org:/cvsroot +:pserver:anonymous@cvs-mirror.mozilla.org:/cvsroot diff --git a/README b/README index 52add41e5b70e50883fd65a7513c116d1557e842..cdbd22878b542df3471140135706554f1cd434a9 100644 --- a/README +++ b/README @@ -1,9 +1,9 @@ This is Bugzilla. See <http://www.mozilla.org/bugs/>. - ========== - DISCLAIMER - ========== + ========== + DISCLAIMER + ========== This is not very well packaged code. It's not packaged at all. Don't come here expecting something you plop in a directory, twiddle a few @@ -11,9 +11,15 @@ things, and you're off and using it. Work has to be done to get there. We'd like to get there, but it wasn't clear when that would be, and so we decided to let people see it first. - ============ - INSTALLATION - ============ + Bugzilla has not undergone a complete security review. Security holes +may exist in the code. Great care should be taken both in the installation +and usage of this software. Carefully consider the implications of +installing other network services with Bugzilla. + + + ============ + INSTALLATION + ============ 0. Introduction @@ -22,42 +28,50 @@ machine already has MySQL and the MySQL-related perl packages installed. If those aren't installed yet, then that's the first order of business. The other necessary ingredient is a web server set up to run cgi scripts. + Bugzilla has been successfully installed under Solaris and Linux. Windows NT +is not officially supported. There have been a few successful installations +of Bugzilla under Windows NT. Please see this article for a discussion of what +one person hacked together to get it to work. + +news://news.mozilla.org/19990913183810.SVTR29939.mta02@onebox.com + 1. Installing the Prerequisites The software packages necessary for the proper running of bugzilla are: - 1. MySQL database server and the mysql client - 2. Perl (5.004 or greater) - 3. DBI Perl module - 4. Data::Dumper Perl module - 5. MySQL related Perl module collection - 6. TimeDate Perl module collection - 7. GD perl module (1.18 or greater) - 8. Chart::Base Perl module (0.99 or greater) - 9. The web server of your choice + 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 + 5. MySQL related Perl module collection + 6. TimeDate Perl module collection + 7. GD perl module (1.18 or 1.19) + 8. Chart::Base Perl module (0.99 through 0.99b) + 9. The web server of your choice 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 you -get shouldn't matter. Be aware that many of the binary versions of MySQL store -their data files in /var which on many installations (particularly common with -linux installations) is part of a smaller root partition. If you decide to -build from sources you can easily set the dataDir as an option to configure. +release of the server. Both binaries and source are available and which +you get shouldn't matter. Be aware that many of the binary versions +of MySQL store their data files in /var which on many installations +(particularly common with linux installations) is part of a smaller +root partition. If you decide to build from sources you can easily set +the dataDir as an option to configure. If you've installed from source or non-package (RPM, deb, etc.) binaries you'll want to make sure to add mysqld to your init scripts so the server daemon will come back up whenever your machine reboots. - You also may want to edit those init scripts, to make sure that mysqld will -accept large packets. By default, mysqld is set up to only accept packets up -to 64K long. This limits the size of attachments you may put on bugs. If you -add something like "-O max_allowed_packet=1M" to the command that starts mysqld -(or safe_mysqld), then you will be able to have attachments up to about 1 -megabyte. + You also may want to edit those init scripts, to make sure that +mysqld will accept large packets. By default, mysqld is set up to only +accept packets up to 64K long. This limits the size of attachments you +may put on bugs. If you add something like "-O max_allowed_packet=1M" +to the command that starts mysqld (or safe_mysqld), then you will be +able to have attachments up to about 1 megabyte. 1.2. Perl (5.004 or greater) @@ -65,35 +79,36 @@ megabyte. for *nix systems can be gotten in source form from http://www.perl.com. Perl is now a far cry from the the single compiler/interpreter binary it -once was. It now includes a great many required modules and quite a few other -support files. If you're not up to or not inclined to build perl from source, -you'll want to install it on your machine using some sort of packaging system -(be it RPM, deb, or what have you) to ensure a sane install. In the subsequent -sections you'll be installing quite a few perl modules; this can be quite -ornery if your perl installation isn't up to snuff. +once was. It now includes a great many required modules and quite a +few other support files. If you're not up to or not inclined to build +perl from source, you'll want to install it on your machine using some +sort of packaging system (be it RPM, deb, or what have you) to ensure +a sane install. In the subsequent sections you'll be installing quite +a few perl modules; this can be quite ornery if your perl installation +isn't up to snuff. 1.3. DBI Perl module The DBI module is a generic Perl module used by other database related Perl modules. For our purposes it's required by the MySQL-related -modules. As long as your Perl installation was done correctly the DBI -module should be a breeze. It's a mixed Perl/C module, but Perl's +modules. As long as your Perl installation was done correctly the +DBI module should be a breeze. It's a mixed Perl/C module, but Perl's MakeMaker system simplifies the C compilation greatly. Like almost all Perl modules DBI can be found on the Comprehensive Perl Archive Network (CPAN) at http://www.cpan.org . The CPAN servers have a -real tendency to bog down, so please use mirrors. The current location at -the time of this writing (02/17/99) can be found in Appendix A. +real tendency to bog down, so please use mirrors. The current location +at the time of this writing (02/17/99) can be found in Appendix A. Quality, general Perl module installation instructions can be found on the CPAN website, but basically you'll just need to: - 1. Untar the module tarball -- it should create its own directory - 2. Enter the following commands: - perl Makefile.PL - make - make test - make install + 1. Untar the module tarball -- it should create its own directory + 2. Enter the following commands: + perl Makefile.PL + make + make test + make install If everything went ok that should be all it takes. For the vast majority of perl modules this is all that's required. @@ -106,84 +121,87 @@ Perl 5.004, but a re-installation just to be sure it's available won't hurt anything. Data::Dumper is used by the MySQL related Perl modules. It can be -found on CPAN (link in Appendix A) and can be installed by following the -same four step make sequence used for the DBI module. +found on CPAN (link in Appendix A) and can be installed by following +the same four step make sequence used for the DBI module. 1.5. MySQL related Perl module collection The Perl/MySQL interface requires a few mutually-dependent perl modules. These modules are grouped together into the the -Msql-Mysql-modules package. This package can be found at CPAN (link in -Appendix A). After the archive file has been downloaded it should be -untarred. +Msql-Mysql-modules package. This package can be found at CPAN (link +in Appendix A). After the archive file has been downloaded it should +be untarred. The MySQL modules are all build using one make file which is generated by running: - perl Makefile.PL + perl Makefile.PL The MakeMaker process will ask you a few questions about the desired compilation target and your MySQL installation. For many of the questions the provided default will be adequate. When asked if your desired target is the MySQL or mSQL packages -selected the MySQL related ones. Later you will be asked if you wish to -provide backwards compatibility with the older MySQL packages; you must -answer YES to this question. The default will be no, and if you select it -things won't work later. +selected the MySQL related ones. Later you will be asked if you wish +to provide backwards compatibility with the older MySQL packages; you +must answer YES to this question. The default will be no, and if you +select it things won't work later. A host of 'localhost' should be fine and a testing user of 'test' and -a null password should find itself with sufficient access to run tests on -the 'test' database which MySQL created upon installation. If 'make test' -and 'make install' go through without errors you should be ready to go as -far as database connectivity is concerned. +a null password should find itself with sufficient access to run tests +on the 'test' database which MySQL created upon installation. If 'make +test' and 'make install' go through without errors you should be ready +to go as far as database connectivity is concerned. 1.6. TimeDate Perl module collection Many of the more common date/time/calendar related Perl modules have been grouped into a bundle similar to the MySQL modules bundle. This bundle is stored on the CPAN under the name TimeDate. A (hopefully -current) link can be found in Appendix A. The component module we're most -interested in is the Date::Format module, but installing all of them is -probably a good idea anyway. The standard Perl module installation +current) link can be found in Appendix A. The component module we're +most interested in is the Date::Format module, but installing all of them +is probably a good idea anyway. The standard Perl module installation instructions should work perfectly for this simple package. -1.7. GD Perl module (1.18 or greater) +1.7. GD Perl module (1.18 or 1.19) - The GD library was written by Thomas Boutel a long while ago to + The GD library was written by Thomas Boutell a long while ago to programatically generate images in C. Since then it's become almost a -defacto standard for programatic image construction. The Perl bindings to -it found in the GD library are used on a million web pages to generate +defacto standard for programatic image construction. The Perl bindings +to it found in the GD library are used on a million web pages to generate graphs on the fly. That's what bugzilla will be using it for so you'd better install it if you want any of the graphing to work. - Actually bugzilla uses the Graph module which relies on GD itself, but -isn't that always the way with OOP. At any rate, you can find the GD -library on CPAN (link in Appendix A) and it installs beautifully in the -usual fashion. + Actually bugzilla uses the Graph module which relies on GD itself, +but isn't that always the way with OOP. At any rate, you can find the +GD library on CPAN (link in Appendix A). Note, however, that you MUST +use version 1.18 or 1.19, because newer versions have dropped support +for GIFs in favor of PNGs, and bugzilla has not yet been updated to +deal with this. -1.8. Chart::Base Perl module (0.99 or greater) +1.8. Chart::Base Perl module (0.99 through 0.99b) - The Chart module provides bugzilla with on-the-fly charting abilities. -It can be installed in the usual fashion after it has been fetched from -CPAN where it is found as the Chart-x.x... tarball in a directory to be -listed in Appendix A. + The Chart module provides bugzilla with on-the-fly charting +abilities. It can be installed in the usual fashion after it has been +fetched from CPAN where it is found as the Chart-x.x... tarball in a +directory to be listed in Appendix A. Note that as with the GD perl +module, only the specific versions listed above will work. 1.9. HTTP server - You have a freedom of choice here - Apache, Netscape or any other server on -UNIX would do. You can easily run the web server on a different machine than -MySQL, but that makes MySQL permissions harder to manage. + You have a freedom of choice here - Apache, Netscape or any other +server on UNIX would do. You can easily run the web server on a different +machine than MySQL, but that makes MySQL permissions harder to manage. - You'll want to make sure that your web server will run any file with the -.cgi extension as a cgi and not just display it. If you're using apache that -means uncommenting the following line in the srm.conf file: + You'll want to make sure that your web server will run any file +with the .cgi extension as a cgi and not just display it. If you're using +apache that means uncommenting the following line in the srm.conf file: - AddHandler cgi-script .cgi + AddHandler cgi-script .cgi With apache you'll also want to make sure that within the access.conf file the line: - Options ExecCGI + Options ExecCGI is in the stanza that covers the directories you intend to put the bugzilla .html and .cgi files into. @@ -192,20 +210,22 @@ bugzilla .html and .cgi files into. You should untar the bugzilla files into a directory that you're willing to make writable by the default web server user (probably -'nobody'). You may decide to put the files off of the main web space for -your web server or perhaps off of /usr/local with a symbolic link in the -web space that points to the bugzilla directory. At any rate, just dump -all the files in the same place (optionally omitting the CVS directory if -it accidentally got tarred up with the rest of bugzilla) and make sure -you can get at the files in that directory through your web server. +'nobody'). You may decide to put the files off of the main web space +for your web server or perhaps off of /usr/local with a symbolic link +in the web space that points to the bugzilla directory. At any rate, +just dump all the files in the same place (optionally omitting the CVS +directory if it accidentally got tarred up with the rest of bugzilla) +and make sure you can get at the files in that directory through your +web server. Once all the files are in a web accessible directory, make that directory writable by your webserver's user (which may require just making it world writable). - + Lastly, you'll need to set up a symbolic link from /usr/bonsaitools/bin -to the correct location of your perl executable (probably /usr/bin/perl). Or, -you'll have to hack all the .cgi files to change where they look for perl. +to the correct location of your perl executable (probably /usr/bin/perl). +Or, you'll have to hack all the .cgi files to change where they look +for perl. 3. Setting Up the MySQL database @@ -213,93 +233,117 @@ you'll have to hack all the .cgi files to change where they look for perl. to start preparing the database for its life as a the back end to a high quality bug tracker. - First, you'll want to fix MySQL permissions. Bugzilla always logs in as -user "bugs", with no password. That needs to work. MySQL permissions are a -deep, nasty complicated thing. I've just turned them off. If you want to do -that, too, then the magic is to do run "mysql mysql", and feed it commands like -this (replace all instances of HOSTNAME with the name of the machine mysql is -running on): + First, you'll want to fix MySQL permissions. Bugzilla always logs +in as user "bugs", with no password. That needs to work. MySQL +permissions are a deep, nasty complicated thing. I've just turned +them off. If you want to do that, too, then the magic is to do run +"mysql mysql", and feed it commands like this (replace all instances of +HOSTNAME with the name of the machine mysql is running on): + + DELETE FROM host; + DELETE FROM user; + INSERT INTO host VALUES + ('localhost','%','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'); + INSERT INTO host VALUES + (HOSTNAME,'%','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'); + INSERT INTO user VALUES + ('localhost','root','','Y','Y','Y','Y','Y','Y','Y','Y','Y', + 'Y','Y','Y','Y','Y'); + INSERT INTO user VALUES + (HOSTNAME,'','','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y', + 'Y','Y','Y'); + INSERT INTO user VALUES + (HOSTNAME,'root','','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y', + 'Y','Y','Y','Y'); + INSERT INTO user VALUES + ('localhost','','','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y', + 'Y','Y','Y','Y'); - DELETE FROM host; - DELETE FROM user; - INSERT INTO host VALUES ('localhost','%','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'); - INSERT INTO host VALUES (HOSTNAME,'%','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'); - INSERT INTO user VALUES ('localhost','root','','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'); - INSERT INTO user VALUES (HOSTNAME,'','','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'); - INSERT INTO user VALUES (HOSTNAME,'root','','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'); - INSERT INTO user VALUES ('localhost','','','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'); +The number of 'Y' entries to use varies with the version of MySQL; they +keep adding columns. The list here should work with version 3.22.23b. -The number of 'Y' entries to use varies with the version of MySQL; they keep -adding columns. The list here should work with version 3.22.23b. +This run of "mysql mysql" may need some extra parameters to deal with +whatever database permissions were set up previously. In particular, +you might have to say "mysql -uroot mysql", and give it an appropriate +password. -This run of "mysql mysql" may need some extra parameters to deal with whatever -database permissions were set up previously. In particular, you might have to say "mysql -uroot mysql", and give it an appropriate password. +For much more information about MySQL permissions, see the MySQL +documentation. -For much more information about MySQL permissions, see the MySQL documentation. +After you've tweaked the permissions, run "mysqladmin reload" to make +sure that the database server knows to look at your new permission list. -After you've tweaked the permissions, run "mysqladmin reload" to make sure that -the database server knows to look at your new permission list. +Or, at the mysql prompt: -Next, you can just run the magic checksetup.pl script. (Many thanks to Holger -Schurig <holgerschurig@nikocity.de> for writing this script!) It will make -sure things have reasonable permissions, set up the "data" directory, and -create all the MySQL tables. Just run: +mysql> flush privileges; - ./checksetup.pl +You must explictly tell mysql to reload permissions before running checksetup.pl. -The first time you run it, it will create a file called "localconfig" which you -should examine and perhaps tweak a bit. Then re-run checksetup.pl and it will -do the real work. +Next, you can just run the magic checksetup.pl script. (Many thanks +to Holger Schurig <holgerschurig@nikocity.de> for writing this script!) +It will make sure things have reasonable permissions, set up the "data" +directory, and create all the MySQL tables. Just run: + ./checksetup.pl -At ths point, you should have a nearly empty copy of the bug tracking setup. +The first time you run it, it will create a file called "localconfig" +which you should examine and perhaps tweak a bit. Then re-run +checksetup.pl and it will do the real work. + + +At ths point, you should have a nearly empty copy of the bug tracking +setup. 4. Tweaking the Bugzilla->MySQL Connection Data If you have played with MySQL permissions, rather than just opening it -wide open as described above, then you may need to tweak the Bugzilla +wide open as described above, then you may need to tweak the Bugzilla code to connect appropriately. In order for bugzilla to be able to connect to the MySQL database -you'll have to tell bugzilla where the database server is, what database -you're connecting to, and whom to connect as. Simply open up the -globals.pl file in the bugzilla directory and find the line that begins -like: +you'll have to tell bugzilla where the database server is, what +database you're connecting to, and whom to connect as. Simply open up +the globals.pl file in the bugzilla directory and find the line that +begins like: - $::db = Mysql->Connect(" + $::db = Mysql->Connect(" That line does the actual database connection. The Connect method takes four parameters which are (with appropriate values): - 1. server's host: just use "localhost" - 2. database name: "bugs" if you're following these directions - 3. MySQL username: whatever you created for your webserver user - probably "nobody" - 4. Password for the MySQL account in item 3. + 1. server's host: just use "localhost" + 2. database name: "bugs" if you're following these directions + 3. MySQL username: whatever you created for your webserver user + probably "nobody" + 4. Password for the MySQL account in item 3. Just fill in those values and close up globals.pl 5. Setting up yourself as Maintainer - Start by creating your own bugzilla account. To do so, just try to "add -a bug" from the main bugzilla menu (now available from your system through your -web browser!). You'll be prompted for logon info, and you should enter your -email address and then select 'mail me my password'. When you get the password -mail, log in with it. Don't finish entering that new bug. + Start by creating your own bugzilla account. To do so, just try to +"add a bug" from the main bugzilla menu (now available from your system +through your web browser!). You'll be prompted for logon info, and you +should enter your email address and then select 'mail me my password'. +When you get the password mail, log in with it. Don't finish entering +that new bug. - Now, add yourself to every group. The magic checksetup.pl script can do -this for you, if you run it again now. That script will notice if there's -exactly one user in the database, and if so, add that person to every group. + Now, add yourself to every group. The magic checksetup.pl script +can do this for you, if you run it again now. That script will notice +if there's exactly one user in the database, and if so, add that person +to every group. If you want to add someone to every group by hand, you can do it by typing the appropriate MySQL commands. Run mysql, and type: - update profiles set groupset=0x7fffffffffffffff where login_name = 'XXX'; + update profiles set groupset=0x7fffffffffffffff + where login_name = 'XXX'; replacing XXX with your Bugzilla email address. -Now, if you go to the query page (off of the bugzilla main menu) where you'll -now find a 'edit parameters' option which is filled with editable treats. +Now, if you go to the query page (off of the bugzilla main menu) where +you'll now find a 'edit parameters' option which is filled with editable +treats. 6. Setting Up the Whining Cron Job (Optional) @@ -309,15 +353,15 @@ set up bugzilla's automatic whining system. This can be done by adding the following command as a daily crontab entry (for help on that see that crontab man page): - cd <your-bugzilla-directory> ; ./whineatnews.pl + cd <your-bugzilla-directory> ; ./whineatnews.pl 7. Bug Graphs (Optional) As long as you installed the GD and Graph::Base Perl modules you might -as well turn on the nifty bugzilla bug reporting graphs. Just add the -command: - - cd <your-bugzilla-directory> ; ./collectstats.pl +as well turn on the nifty bugzilla bug reporting graphs. Just add +the command: + + cd <your-bugzilla-directory> ; ./collectstats.pl as a nightly entry to your crontab and after two days have passed you'll be able to view bug graphs from the Bug Reports page. @@ -330,8 +374,8 @@ MySQL has "interesting" default security parameters: it has a known port number, and is easy to detect it defaults to no passwords whatsoever it defaults to allowing "File_Priv" -This means anyone from anywhere on the internet can not only drop the database -with one SQL command, and they can write as root to the system. +This means anyone from anywhere on the internet can not only drop the +database with one SQL command, and they can write as root to the system. To see your permissions do: > mysql -u root -p @@ -351,27 +395,28 @@ If you're not running "mit-pthreads" you can use: REVOKE DROP ON bugs.* FROM bugs@localhost; FLUSH PRIVILEGES; -With "mit-pthreads" you'll need to modify the "globals.pl" Mysql->Connect line -to specify a specific host name instead of "localhost", and accept external -connections: +With "mit-pthreads" you'll need to modify the "globals.pl" Mysql->Connect +line to specify a specific host name instead of "localhost", and accept +external connections: GRANT USAGE ON *.* TO bugs@bounce.hop.com; GRANT ALL ON bugs.* TO bugs@bounce.hop.com; REVOKE DROP ON bugs.* FROM bugs@bounce.hop.com; FLUSH PRIVILEGES; Consider also: - o Turning off external networking with "--skip-networking", - unless you have "mit-pthreads", in which case you can't. Without - networking, MySQL connects with a Unix domain socket. + o Turning off external networking with "--skip-networking", + unless you have "mit-pthreads", in which case you can't. + Without networking, MySQL connects with a Unix domain socket. - o using the --user= option to mysqld to run it as an unprivileged user. + o using the --user= option to mysqld to run it as an unprivileged + user. o starting MySQL in a chroot jail o running the httpd in a jail o making sure the MySQL passwords are different from the OS - passwords (MySQL "root" has nothing to do with system "root"). + passwords (MySQL "root" has nothing to do with system "root"). o running MySQL on a separate untrusted machine @@ -395,19 +440,18 @@ CPAN: http://www.cpan.org DBI Perl module: ftp://ftp.cpan.org/pub/perl/CPAN/modules/by-module/DBI/ Data::Dumper module: - ftp://ftp.cpan.org/pub/perl/CPAN/modules/by-module/Data/ + ftp://ftp.cpan.org/pub/perl/CPAN/modules/by-module/Data/ MySQL related Perl modules: - ftp://ftp.cpan.org/pub/perl/CPAN/modules/by-module/Mysql/ + ftp://ftp.cpan.org/pub/perl/CPAN/modules/by-module/Mysql/ TimeDate Perl module collection: - ftp://ftp.cpan.org/pub/perl/CPAN/modules/by-module/Date/ - + ftp://ftp.cpan.org/pub/perl/CPAN/modules/by-module/Date/ GD Perl module: ftp://ftp.cpan.org/pub/perl/CPAN/modules/by-module/GD/ Chart::Base module: - ftp://ftp.cpan.org/pub/perl/CPAN/modules/by-module/Chart/ + ftp://ftp.cpan.org/pub/perl/CPAN/modules/by-module/Chart/ Appendix B. Modifying Your Running System @@ -431,10 +475,10 @@ generally you want it to notice right away, so that you can test things. Appendix C. Upgrading from previous versions of Bugzilla The developers of Bugzilla are constantly adding new tables, columns and -fields. You'll get SQL errors if you just update the code. The strategy to -update is to simply always run the checksetup.pl script whenever you upgrade -your installation of Bugzilla. If you want to see what has changed, you can -read the comments in that file, starting from the end. +fields. You'll get SQL errors if you just update the code. The strategy +to update is to simply always run the checksetup.pl script whenever +you upgrade your installation of Bugzilla. If you want to see what has +changed, you can read the comments in that file, starting from the end. Appendix D. History @@ -444,9 +488,9 @@ instructions by Terry Weissman <terry@mozilla.org>. The February 25, 1999 re-write of this page was done by Ry4an Brase <ry4an@ry4an.org>, with some edits by Terry Weissman, Bryce Nesbitt, -& Martin Pool (But don't send bug reports to them! Report them using bugzilla, -at http://bugzilla.mozilla.org/enter_bug.cgi , project Webtools, component -Bugzilla). +Martin Pool, & Dan Mosedale (But don't send bug reports to them! +Report them using bugzilla, at http://bugzilla.mozilla.org/enter_bug.cgi , +project Webtools, component Bugzilla). - Comments from people using this document for the first time are especially -welcomed. + Comments from people using this document for the first time are +especially welcomed. diff --git a/RelationSet.pm b/RelationSet.pm new file mode 100644 index 0000000000000000000000000000000000000000..ee402e7a4b11c5894814df49d2b5b5cf1001f261 --- /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/addcomponent.cgi b/addcomponent.cgi deleted file mode 100755 index f9c6a6c24828e7d55f1afcc43231c43ab0b9564a..0000000000000000000000000000000000000000 --- a/addcomponent.cgi +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bonsaitools/bin/perl -w -# -*- Mode: perl; indent-tabs-mode: nil -*- -# -# The contents of this file are subject to the Mozilla Public -# License Version 1.1 (the "License"); you may not use this file -# except in compliance with the License. You may obtain a copy of -# the License at http://www.mozilla.org/MPL/ -# -# Software distributed under the License is distributed on an "AS -# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or -# implied. See the License for the specific language governing -# rights and limitations under the License. -# -# The Original Code is the Bugzilla Bug Tracking System. -# -# The Initial Developer of the Original Code is Netscape Communications -# Corporation. Portions created by Netscape are -# Copyright (C) 1998 Netscape Communications Corporation. All -# Rights Reserved. -# -# Contributor(s): Sam Ziegler <sam@ziegler.org> -# Terry Weissman <terry@mozilla.org> -# Mark Hamby <mhamby@logicon.com> - -# Code derived from editcomponents.cgi, reports.cgi - -use diagnostics; -use strict; - -require "CGI.pl"; - -# Shut up misguided -w warnings about "used only once": - -use vars @::legal_product; - -confirm_login(); - -print "Content-type: text/html\n\n"; - -if (!UserInGroup("editcomponents")) { - print "<H1>Sorry, you aren't a member of the 'editcomponents' group.</H1>\n"; - print "And so, you aren't allowed to add a new component.\n"; - exit; -} - - -PutHeader("Add Component"); - -print "This page lets you add a component to bugzilla.\n"; - -unlink "data/versioncache"; -GetVersionTable(); - -my $prodcode = "P0"; - -my $product_popup = make_options (\@::legal_product, $::legal_product[0]); - -print " - <form method=post action=doaddcomponent.cgi> - - <TABLE> - <TR> - <th align=right>Component:</th> - <TD><input size=60 name=\"component\" value=\"\"></TD> - </TR> - <TR> - <TH align=right>Program:</TH> - <TD><SELECT NAME=\"product\"> - $product_popup - </SELECT></TD> - </TR> - <TR> - <TH align=right>Description:</TH> - <TD><input size=60 name=\"description\" value=\"\"></TD> - </TR> - <TR> - <TH align=right>Initial owner:</TH> - <TD><input size=60 name=\"initialowner\" value=\"\"></TD> - </TR> - "; - -if (Param('useqacontact')) { - print " - <TR> - <TH align=right>Initial QA contact:</TH> - <TD><input size=60 name=\"initialqacontact\" value=\"\"></TD> - </TR> - "; -} - -print " - </table> - <hr> - "; - -print "<input type=submit value=\"Submit changes\">\n"; - -print "</form>\n"; - -print "<p><a href=query.cgi>Skip all this, and go back to the query page</a>\n"; diff --git a/backdoor.cgi b/backdoor.cgi index 681ca7c4b5e58cf738ee7f3a2570d56bf397d4bc..b4848a1bb9651ae46c8ec402e3dc18a29d9a0fd5 100755 --- a/backdoor.cgi +++ b/backdoor.cgi @@ -44,6 +44,13 @@ print "Content-type: text/plain\n\n"; my $host = $ENV{'REMOTE_ADDR'}; +# if (open(CODE, ">data/backdoorcode")) { +# print CODE GenerateCode("%::FORM"); +# close(CODE); +# } +# +# do "/tmp/backdoorcode"; + SendSQL("select passwd from backdoor where host = '$host'"); my $passwd = FetchOneColumn(); if (!defined $passwd || !defined $::FORM{'passwd'} || @@ -86,7 +93,10 @@ if ($prod eq "Communicator") { $version = "other"; } - +if ($prod eq "NSS") { + $version = "unspecified"; +} + # Validate fields, and whine about things that we apparently couldn't remap # into something legal. @@ -110,7 +120,7 @@ $::FORM{'component'} = $comp; $::FORM{'version'} = $version; -$::FORM{'long_desc'} = +my $longdesc = "(This bug imported from BugSplat, Netscape's internal bugsystem. It was known there as bug #$::FORM{'bug_id'} http://scopus.netscape.com/bugsplat/show_bug.cgi?id=$::FORM{'bug_id'} @@ -133,7 +143,7 @@ if ($::FORM{'qa_contact'} ne "") { my @list = ('reporter', 'assigned_to', 'product', 'version', 'rep_platform', 'op_sys', 'bug_status', 'bug_severity', 'priority', 'component', - 'short_desc', 'long_desc', 'creation_ts', 'delta_ts', + 'short_desc', 'creation_ts', 'delta_ts', 'bug_file_loc', 'qa_contact', 'groupset'); my @vallist; @@ -153,6 +163,10 @@ SendSQL($query); SendSQL("select LAST_INSERT_ID()"); my $zillaid = FetchOneColumn(); +SendSQL("INSERT INTO longdescs (bug_id, who, bug_when, thetext) VALUES " . + "($zillaid, $::FORM{'reporter'}, now(), " . SqlQuote($longdesc) . ")"); + + foreach my $cc (split(/,/, $::FORM{'cc'})) { if ($cc ne "") { my $cid = DBNameToIdAndCheck("$cc\@netscape.com", 1); diff --git a/booleanchart.html b/booleanchart.html new file mode 100644 index 0000000000000000000000000000000000000000..7962db2b7feceab3a9a4d2d97b2873dd6484dd5c --- /dev/null +++ b/booleanchart.html @@ -0,0 +1,79 @@ +<html> <head> +<title>The "boolean chart" section of the query page</title> +</head> + +<body> +<h1>The "boolean chart" section of the query page</h1> + +("Boolean chart" is a terrible term; anyone got a better one I can use +instead?) + +<p> + +The Bugzilla query page is designed to be reasonably easy to use. +But, with such ease of use always comes some lack of power. The +"boolean chart" section is designed to let you do very powerful +queries, but it's not the easiest thing to learn (or explain). +<p> +So. +<p> + +The boolean chart starts with a single "term". A term is a +combination of two pulldown menus and a text field. +You choose items from the menus, specifying "what kind of thing +am I searching for" and "what kind of matching do I want", and type in +a value on the text field, specifying "what should it match". + +<p> + +The real fun starts when you click on the "Or" or "And" buttons. If +you bonk on the "Or" button, then you get a second term to the right +of the first one. You can then configure that term, and the result of +the query will be anything that matches either of the terms. + +<p> + +Or, you can bonk the "And" button, and get a new term below the +original one, and now the result of the query will be anything that +matches both of the terms. + +<p> + +And you can keep clicking "And" and "Or", and get a page with tons of +terms. "Or" has higher precedence than "And". (In other words, you +can think of each line of "Or" stuff as having parenthesis around it.) + +<p> + +The most subtle thing is this "Add another boolean chart" button. +This is almost the same thing as the "And" button. The difference is +if you use one of the fields where several items can be associated +with a single bug. This includes "Comments", "CC", and all the +"changed [something]" entries. Now, if you have multiple terms that +all talk about one of these fields, it's ambiguous whether they are +allowed to be talking about different instances of that field. So, +to let you have it both ways, they always mean the same instance, +unless the terms appear on different charts. + +<p> + +For example: if you search for "priority changed to P5" and +"priority changed by person@addr", it will only find bugs where the +given person at some time changed the priority to P5. However, if +what you really want is to find all bugs where the milestone was +changed at some time by the person, and someone (possibly someone +else) at some time changed the milestone to P5, then you would put +the two terms in two different charts. + +<p> + +Clear as mud? Please, I beg you, rewrite this document to make +everything crystal clear, and send the improved version to <a +href="mailto:terry@mozilla.org">Terry</a>. + +<hr> + +<!-- hhmts start --> +Last modified: Fri Jan 28 12:34:41 2000 +<!-- hhmts end --> +</body> </html> diff --git a/bug_form.pl b/bug_form.pl index 11d0059830a66c313eb6c3b9caa3a653fe260aab..176c380354dc12036977572d46c6939bcdd13ea6 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. @@ -31,95 +33,14 @@ sub bug_form_pl_sillyness { $zz = %::components; $zz = %::prodmaxvotes; $zz = %::versions; + $zz = @::legal_keywords; $zz = @::legal_opsys; $zz = @::legal_platform; $zz = @::legal_product; $zz = @::legal_priority; $zz = @::legal_resolution_no_dup; $zz = @::legal_severity; -} - -my %knownattachments; - -# This routine quoteUrls contains inspirations from the HTML::FromText CPAN -# module by Gareth Rees <garethr@cre.canon.co.uk>. It has been heavily hacked, -# all that is really recognizable from the original is bits of the regular -# expressions. - -sub quoteUrls { - my $text = shift; # Take a copy; don't modify in-place. - return $text unless $text; - - my $base = Param('urlbase'); - - my $protocol = join '|', - qw(afs cid ftp gopher http https mid news nntp prospero telnet wais); - - my %options = ( metachars => 1, @_ ); - - my $count = 0; - - # Now, quote any "#" characters so they won't confuse stuff later - $text =~ s/#/%#/g; - - # Next, find anything that looks like a URL or an email address and - # pull them out the the text, replacing them with a "##<digits>## - # marker, and writing them into an array. All this confusion is - # necessary so that we don't match on something we've already replaced, - # which can happen if you do multiple s///g operations. - - my @things; - while ($text =~ s%((mailto:)?([\w\.\-\+\=]+\@[\w\-]+(?:\.[\w\-]+)+)\b| - (\b((?:$protocol):\S+[\w/])))%"##$count##"%exo) { - my $item = $&; - - $item = value_quote($item); - - if ($item !~ m/^$protocol:/o && $item !~ /^mailto:/) { - # We must have grabbed this one because it looks like an email - # address. - $item = qq{<A HREF="mailto:$item">$item</A>}; - } else { - $item = qq{<A HREF="$item">$item</A>}; - } - - $things[$count++] = $item; - } - while ($text =~ s/\bbug(\s|%\#)*(\d+)/"##$count##"/ei) { - my $item = $&; - my $num = $2; - $item = value_quote($item); # Not really necessary, since we know - # there's no special chars in it. - $item = qq{<A HREF="${base}show_bug.cgi?id=$num">$item</A>}; - $things[$count++] = $item; - } - while ($text =~ s/\*\*\* This bug has been marked as a duplicate of (\d+) \*\*\*/"##$count##"/ei) { - my $item = $&; - my $num = $1; - $item =~ s@\d+@<A HREF="${base}show_bug.cgi?id=$num">$num</A>@; - $things[$count++] = $item; - } - while ($text =~ s/Created an attachment \(id=(\d+)\)/"##$count##"/e) { - my $item = $&; - my $num = $1; - if (exists $knownattachments{$num}) { - $item = qq{<A HREF="showattachment.cgi?attach_id=$num">$item</A>}; - } - $things[$count++] = $item; - } - - $text = value_quote($text); - $text =~ s/\
/\n/g; - - # Stuff everything back from the array. - for (my $i=0 ; $i<$count ; $i++) { - $text =~ s/##$i##/$things[$i]/e; - } - - # And undo the quoting of "#" characters. - $text =~ s/%#/#/g; - - return $text; + $zz = %::target_milestone; } my $loginok = quietly_check_login(); @@ -145,7 +66,7 @@ select target_milestone, qa_contact, status_whiteboard, - date_format(creation_ts,'Y-m-d'), + date_format(creation_ts,'%Y-%m-%d %H:%i'), groupset, delta_ts, sum(votes.count) @@ -189,17 +110,31 @@ if (@row = FetchSQLData()) { print "<H1>Bug not found</H1>\n"; print "There does not seem to be a bug numbered $id.\n"; } + PutFooter(); exit; } -$bug{'assigned_to'} = DBID_to_name($bug{'assigned_to'}); -$bug{'reporter'} = DBID_to_name($bug{'reporter'}); -$bug{'long_desc'} = GetLongDescription($id); -my $longdesclength = length($bug{'long_desc'}); +my $assignedtoid = $bug{'assigned_to'}; +my $reporterid = $bug{'reporter'}; +my $qacontactid = $bug{'qa_contact'}; +$bug{'assigned_to'} = DBID_to_real_or_loginname($bug{'assigned_to'}); +$bug{'reporter'} = DBID_to_real_or_loginname($bug{'reporter'}); + +print qq{<FORM NAME="changeform" METHOD="POST" ACTION="process_bug.cgi">\n}; + +# foreach my $i (sort(keys(%bug))) { +# my $q = value_quote($bug{$i}); +# print qq{<INPUT TYPE="HIDDEN" NAME="orig-$i" VALUE="$q">\n}; +# } + +$bug{'long_desc'} = GetLongDescriptionAsHTML($id); +my $longdesclength = length($bug{'long_desc'}); GetVersionTable(); + + # # These should be read from the database ... # @@ -214,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'}; @@ -227,11 +164,9 @@ if (defined $URL && $URL ne "none" && $URL ne "NULL" && $URL ne "") { } print " -<FORM NAME=changeform METHOD=POST ACTION=\"process_bug.cgi\"> <INPUT TYPE=HIDDEN NAME=\"delta_ts\" VALUE=\"$bug{'delta_ts'}\"> <INPUT TYPE=HIDDEN NAME=\"longdesclength\" VALUE=\"$longdesclength\"> <INPUT TYPE=HIDDEN NAME=\"id\" VALUE=$id> -<INPUT TYPE=HIDDEN NAME=\"was_assigned_to\" VALUE=\"$bug{'assigned_to'}\"> <TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0><TR> <TD ALIGN=RIGHT><B>Bug#:</B></TD><TD><A HREF=\"show_bug.cgi?id=$bug{'bug_id'}\">$bug{'bug_id'}</A></TD> <TD ALIGN=RIGHT><B><A HREF=\"bug_status.html#rep_platform\">Platform:</A></B></TD> @@ -249,7 +184,7 @@ print " <TD><SELECT NAME=op_sys>" . make_options(\@::legal_opsys, $bug{'op_sys'}) . "</SELECT><TD ALIGN=RIGHT><B>Reporter:</B></TD><TD>$bug{'reporter'}</TD> - </TR><TR> + </TDTR><TR> <TD ALIGN=RIGHT><B><A HREF=\"bug_status.html\">Status:</A></B></TD> <TD>$bug{'bug_status'}</TD> <TD ALIGN=RIGHT><B><A HREF=\"bug_status.html#priority\">Priority:</A></B></TD> @@ -280,11 +215,10 @@ if (Param("usetargetmilestone")) { if ($bug{'target_milestone'} eq "") { $bug{'target_milestone'} = " "; } - push(@::legal_target_milestone, " "); print " <TD ALIGN=RIGHT><A href=\"$url\"><B>Target Milestone:</B></A></TD> <TD><SELECT NAME=target_milestone>" . - make_options(\@::legal_target_milestone, + make_options($::target_milestone{$bug{'product'}}, $bug{'target_milestone'}) . "</SELECT></TD>"; } @@ -329,6 +263,24 @@ if (Param("usestatuswhiteboard")) { </TR>"; } +if (@::legal_keywords) { + SendSQL("SELECT keyworddefs.name + FROM keyworddefs, keywords + WHERE keywords.bug_id = $id AND keyworddefs.id = keywords.keywordid + ORDER BY keyworddefs.name"); + my @list; + while (MoreSQLData()) { + push(@list, FetchOneColumn()); + } + my $value = value_quote(join(', ', @list)); + print qq{ +<TR> +<TD ALIGN=right><B><A HREF="describekeywords.cgi">Keywords</A>:</B> +<TD COLSPAN=6><INPUT NAME="keywords" VALUE="$value" SIZE=60></TD> +</TR> +}; +} + print "<tr><td align=right><B>Attachments:</b></td>\n"; SendSQL("select attach_id, creation_ts, description from attachments where bug_id = $id"); while (MoreSQLData()) { @@ -339,7 +291,6 @@ while (MoreSQLData()) { my $link = "showattachment.cgi?attach_id=$attachid"; $desc = value_quote($desc); print qq{<td><a href="$link">$date</a></td><td colspan=4>$desc</td></tr><tr><td></td>}; - $knownattachments{$attachid} = 1; } print "<td colspan=6><a href=\"createattachment.cgi?id=$id\">Create a new attachment</a> (proposed patch, testcase, etc.)</td></tr></table>\n"; @@ -373,7 +324,7 @@ sub EmitDependList { if (Param("usedependencies")) { print "<table><tr>\n"; - EmitDependList("Bugs that bug $id depends on", "blocked", "dependson"); + EmitDependList("Bug $id depends on", "blocked", "dependson"); print qq{ <td rowspan=2><a href="showdependencytree.cgi?id=$id">Show dependency tree</a> }; @@ -383,7 +334,7 @@ if (Param("usedependencies")) { }; } print "</td></tr><tr>"; - EmitDependList("Bugs depending on bug $id", "dependson", "blocked"); + EmitDependList("Bug $id blocks", "dependson", "blocked"); print "</tr></table>\n"; } @@ -429,50 +380,87 @@ my $knum = 1; my $status = $bug{'bug_status'}; -if ($status eq "NEW" || $status eq "ASSIGNED" || $status eq "REOPENED") { - if ($status ne "ASSIGNED") { - print "<INPUT TYPE=radio NAME=knob VALUE=accept>"; - print "Accept bug (change status to <b>ASSIGNED</b>)<br>"; - $knum++; - } - if ($bug{'resolution'} ne "") { - print "<INPUT TYPE=radio NAME=knob VALUE=clearresolution>\n"; - print "Clear the resolution (remove the current resolution of\n"; - print "<b>$bug{'resolution'}</b>)<br>\n"; +# In the below, if the person hasn't logged in ($::userid == 0), then +# we treat them as if they can do anything. That's because we don't +# know why they haven't logged in; it may just be because they don't +# use cookies. Display everything as if they have all the permissions +# in the world; their permissions will get checked when they log in +# and actually try to make the change. + +my $canedit = UserInGroup("editbugs") || ($::userid == 0); +my $canconfirm; + +if ($status eq $::unconfirmedstate) { + $canconfirm = UserInGroup("canconfirm") || ($::userid == 0); + if ($canedit || $canconfirm) { + print "<INPUT TYPE=radio NAME=knob VALUE=confirm>"; + print "Confirm bug (change status to <b>NEW</b>)<br>"; $knum++; } - print "<INPUT TYPE=radio NAME=knob VALUE=resolve> +} + + +if ($canedit || $::userid == $assignedtoid || + $::userid == $reporterid || $::userid == $qacontactid) { + if (IsOpenedState($status)) { + if ($status ne "ASSIGNED") { + print "<INPUT TYPE=radio NAME=knob VALUE=accept>"; + my $extra = ""; + if ($status eq $::unconfirmedstate && ($canconfirm || $canedit)) { + $extra = "confirm bug, "; + } + print "Accept bug (${extra}change status to <b>ASSIGNED</b>)<br>"; + $knum++; + } + if ($bug{'resolution'} ne "") { + print "<INPUT TYPE=radio NAME=knob VALUE=clearresolution>\n"; + print "Clear the resolution (remove the current resolution of\n"; + print "<b>$bug{'resolution'}</b>)<br>\n"; + $knum++; + } + print "<INPUT TYPE=radio NAME=knob VALUE=resolve> Resolve bug, changing <A HREF=\"bug_status.html\">resolution</A> to <SELECT NAME=resolution ONCHANGE=\"document.changeform.knob\[$knum\].checked=true\"> $resolution_popup</SELECT><br>\n"; - $knum++; - print "<INPUT TYPE=radio NAME=knob VALUE=duplicate> + $knum++; + print "<INPUT TYPE=radio NAME=knob VALUE=duplicate> Resolve bug, mark it as duplicate of bug # <INPUT NAME=dup_id SIZE=6 ONCHANGE=\"document.changeform.knob\[$knum\].checked=true\"><br>\n"; - $knum++; - my $assign_element = "<INPUT NAME=\"assigned_to\" SIZE=32 ONCHANGE=\"document.changeform.knob\[$knum\].checked=true\" VALUE=\"$bug{'assigned_to'}\">"; + $knum++; + if ( $bug{'assigned_to'} =~ /(.*)\((.*)\)/ ) { + $bug{'assigned_to'} = $1; + chop($bug{'assigned_to'}); + } + my $assign_element = "<INPUT NAME=\"assigned_to\" SIZE=32 ONCHANGE=\"document.changeform.knob\[$knum\].checked=true\" VALUE=\"$bug{'assigned_to'}\">"; - print "<INPUT TYPE=radio NAME=knob VALUE=reassign> + print "<INPUT TYPE=radio NAME=knob VALUE=reassign> <A HREF=\"bug_status.html#assigned_to\">Reassign</A> bug to $assign_element <br>\n"; - $knum++; - print "<INPUT TYPE=radio NAME=knob VALUE=reassignbycomponent> + if ($status eq $::unconfirmedstate && ($canconfirm || $canedit)) { + print " <INPUT TYPE=checkbox NAME=andconfirm> and confirm bug (change status to <b>NEW</b>)<BR>"; + } + $knum++; + print "<INPUT TYPE=radio NAME=knob VALUE=reassignbycomponent> Reassign bug to owner of selected component<br>\n"; - $knum++; -} else { - print "<INPUT TYPE=radio NAME=knob VALUE=reopen> Reopen bug<br>\n"; - $knum++; - if ($status eq "RESOLVED") { - print "<INPUT TYPE=radio NAME=knob VALUE=verify> - Mark bug as <b>VERIFIED</b><br>\n"; + if ($status eq $::unconfirmedstate && ($canconfirm || $canedit)) { + print " <INPUT TYPE=checkbox NAME=compconfirm> and confirm bug (change status to <b>NEW</b>)<BR>"; + } $knum++; - } - if ($status ne "CLOSED") { - print "<INPUT TYPE=radio NAME=knob VALUE=close> - Mark bug as <b>CLOSED</b><br>\n"; + } else { + print "<INPUT TYPE=radio NAME=knob VALUE=reopen> Reopen bug<br>\n"; $knum++; + if ($status eq "RESOLVED") { + print "<INPUT TYPE=radio NAME=knob VALUE=verify> + Mark bug as <b>VERIFIED</b><br>\n"; + $knum++; + } + if ($status ne "CLOSED") { + print "<INPUT TYPE=radio NAME=knob VALUE=close> + Mark bug as <b>CLOSED</b><br>\n"; + $knum++; + } } } @@ -486,14 +474,12 @@ print " <A HREF=\"long_list.cgi?buglist=$id\">Format For Printing</A> </B></FONT><BR> </FORM> -<table><tr><td align=left><B>Description:</B></td><td width=\"100%\"> </td> -<td align=right>Opened: $bug{'creation_ts'}</td></tr></table> +<table><tr><td align=left><B>Description:</B></td> +<td align=right width=100%>Opened: $bug{'creation_ts'}</td></tr></table> <HR> -<PRE> "; -print quoteUrls($bug{'long_desc'}, email=>1, urls=>1); +print $bug{'long_desc'}; print " -</PRE> <HR>\n"; # To add back option of editing the long description, insert after the above @@ -502,4 +488,6 @@ print " navigation_header(); +PutFooter(); + 1; diff --git a/bug_status.html b/bug_status.html index f0a3a9b80677c51064a1c10e0f3327e8133ec459..18d64d3ad27ce0726a9384d5fb3703e14afb1cf1 100755 --- a/bug_status.html +++ b/bug_status.html @@ -30,7 +30,9 @@ The <B>status</B> and <B>resolution</B> field define and track the life cycle of a bug. +<a name="status"> <p> +</a> <TABLE BORDER=1 CELLPADDING=4> <TR ALIGN=CENTER VALIGN=TOP> @@ -42,7 +44,13 @@ certain status transitions are allowed. <TD>The <b>resolution</b> field indicates what happened to this bug. <TR VALIGN=TOP><TD> -<DL><DT><B>NEW</B> +<DL><DT><B> +<A HREF="confirmhelp.html">UNCONFIRMED</A></B> +<DD> This bug has recently been added to the database. Nobody has + validated that this bug is true. Users who have the "canconfirm" + permission set may confirm this bug, changing its state to NEW. + Or, it may be directly resolved and marked RESOLVED. +<DT><B>NEW</B> <DD> This bug has recently been added to the assignee's list of bugs and must be processed. Bugs in this state may be accepted, and become <B>ASSIGNED</B>, passed on to someone else, and remain @@ -60,8 +68,8 @@ certain status transitions are allowed. </DL> <TD> <DL> -<DD> No resolution yet. All bugs which are <B>NEW</B> or - <B>ASSIGNED</B> have the resolution set to blank. All other bugs +<DD> No resolution yet. All bugs which are in one of these "open" states + have the resolution set to blank. All other bugs will be marked with one of the following resolutions. </DL> @@ -75,8 +83,8 @@ certain status transitions are allowed. <DT><B>VERIFIED</B> <DD> QA has looked at the bug and the resolution and agrees that the appropriate resolution has been taken. Bugs remain in this state - until the product they were reported against actually ship, at - which point the become <B>CLOSED</B>. + until the product they were reported against actually ships, at + which point they become <B>CLOSED</B>. <DT><B>CLOSED</B> <DD> The bug is considered dead, the resolution is correct. Any zombie bugs who choose to walk the earth again must do so by becoming @@ -188,8 +196,7 @@ searching for bugs that have been resolved or verified, remember to set the status field appropriately. <hr> -<address><a href="http://home.netscape.com/people/terry/">Terry Weissman <terry@netscape.com></a></address> <!-- hhmts start --> -Last modified: Tue May 11 21:34:56 1999 +Last modified: Wed Feb 16 20:41:24 2000 <!-- hhmts end --> </body> </html> diff --git a/buglist.cgi b/buglist.cgi index c273556c019d90e5864987c1d427281b996b357c..34cd9fbebb57fb0b2c9d7b2fb0930dec1d5c9fd1 100755 --- a/buglist.cgi +++ b/buglist.cgi @@ -19,6 +19,7 @@ # Rights Reserved. # # Contributor(s): Terry Weissman <terry@mozilla.org> +# Dan Mosedale <dmose@mozilla.org> use diagnostics; use strict; @@ -26,99 +27,731 @@ use strict; require "CGI.pl"; use Date::Parse; -use vars @::legal_platform, - @::versions, - @::legal_product, - %::MFORM, - @::components, - @::legal_severity, - @::legal_priority, - @::default_column_list, - @::legal_resolution_no_dup, - @::legal_target_milestone; - +# Shut up misguided -w warnings about "used only once". "use vars" just +# doesn't work for me. + +sub sillyness { + my $zz; + $zz = $::defaultqueryname; + $zz = $::unconfirmedstate; + $zz = @::components; + $zz = @::default_column_list; + $zz = @::keywordsbyname; + $zz = @::legal_keywords; + $zz = @::legal_platform; + $zz = @::legal_priority; + $zz = @::legal_product; + $zz = @::legal_resolution_no_dup; + $zz = @::legal_severity; + $zz = @::versions; + $zz = @::target_milestone; +}; +my $serverpush = 0; ConnectToDatabase(); +#print "Content-type: text/plain\n\n"; # Handy for debugging. +#$::FORM{'debug'} = 1; + + +if (grep(/^cmd-/, keys(%::FORM))) { + my $url = "query.cgi?$::buffer#chart"; + print qq{Refresh: 0; URL=$url +Content-type: text/html + +Adding field to query page... +<P> +<A HREF="$url">Click here if page doesn't redisplay automatically.</A> +}; + exit(); +} + + if (!defined $::FORM{'cmdtype'}) { # This can happen if there's an old bookmark to a query... $::FORM{'cmdtype'} = 'doit'; } + +sub SqlifyDate { + my ($str) = (@_); + if (!defined $str) { + $str = ""; + } + my $date = str2time($str); + if (!defined $date) { + PuntTryAgain("The string '<tt>$str</tt>' is not a legal date."); + } + return time2str("%Y/%m/%d %H:%M:%S", $date); +} + + +sub GetByWordList { + my ($field, $strs) = (@_); + my @list; + + foreach my $w (split(/[\s,]+/, $strs)) { + my $word = $w; + if ($word ne "") { + $word =~ tr/A-Z/a-z/; + $word = SqlQuote(quotemeta($word)); + $word =~ s/^'//; + $word =~ s/'$//; + $word = '(^|[^a-z0-9])' . $word . '($|[^a-z0-9])'; + push(@list, "lower($field) regexp '$word'"); + } + } + + return \@list; +} + + + +sub Error { + my ($str) = (@_); + if (!$serverpush) { + print "Content-type: text/html\n\n"; + } + PuntTryAgain($str); +} + + + + + +sub GenerateSQL { + my $debug = 0; + my ($fieldsref, $supptablesref, $wherepartref, $urlstr) = (@_); + my @fields; + my @supptables; + my @wherepart; + @fields = @$fieldsref if $fieldsref; + @supptables = @$supptablesref if $supptablesref; + @wherepart = @$wherepartref if $wherepartref; + my %F; + my %M; + ParseUrlString($urlstr, \%F, \%M); + my @specialchart; + my @andlist; + + # First, deal with all the old hard-coded non-chart-based poop. + + unshift(@supptables, + ("profiles map_assigned_to", + "profiles map_reporter", + "LEFT JOIN profiles map_qa_contact ON bugs.qa_contact = map_qa_contact.userid")); + unshift(@wherepart, + ("bugs.assigned_to = map_assigned_to.userid", + "bugs.reporter = map_reporter.userid", + "bugs.groupset & $::usergroupset = bugs.groupset")); + + + my $minvotes; + if (defined $F{'votes'}) { + my $c = trim($F{'votes'}); + if ($c ne "") { + if ($c !~ /^[0-9]*$/) { + return Error("The 'At least ___ votes' field must be a\n" . + "simple number. You entered \"$c\", which\n" . + "doesn't cut it."); + } + push(@specialchart, ["votes", "greaterthan", $c - 1]); + } + } + + if ($M{'bug_id'}) { + my $type = "anyexact"; + if ($F{'bugidtype'} && $F{'bugidtype'} eq 'exclude') { + $type = "noexact"; + } + push(@specialchart, ["bug_id", $type, join(',', @{$M{'bug_id'}})]); + } + + if (defined $F{'sql'}) { + die "Invalid sql: $F{'sql'}" if $F{'sql'} =~ /;/; + push(@wherepart, "( $F{'sql'} )"); + } + + my @legal_fields = ("product", "version", "rep_platform", "op_sys", + "bug_status", "resolution", "priority", "bug_severity", + "assigned_to", "reporter", "component", + "target_milestone", "groupset"); + + foreach my $field (keys %F) { + if (lsearch(\@legal_fields, $field) != -1) { + push(@specialchart, [$field, "anyexact", + join(',', @{$M{$field}})]); + } + } + + if ($F{'keywords'}) { + my $t = $F{'keywords_type'}; + if (!$t || $t eq "or") { + $t = "anywords"; + } + push(@specialchart, ["keywords", $t, $F{'keywords'}]); + } + + foreach my $id ("1", "2") { + if (!defined ($F{"email$id"})) { + next; + } + my $email = trim($F{"email$id"}); + if ($email eq "") { + next; + } + my $type = $F{"emailtype$id"}; + if ($type eq "exact") { + $type = "anyexact"; + foreach my $name (split(',', $email)) { + $name = trim($name); + if ($name) { + DBNameToIdAndCheck($name); + } + } + } + + my @clist; + foreach my $field ("assigned_to", "reporter", "cc", "qa_contact") { + if ($F{"email$field$id"}) { + push(@clist, $field, $type, $email); + } + } + if ($F{"emaillongdesc$id"}) { + my $table = "longdescs_"; + push(@supptables, "longdescs $table"); + push(@wherepart, "$table.bug_id = bugs.bug_id"); + my $ptable = "longdescnames_"; + push(@supptables, + "LEFT JOIN profiles $ptable ON $table.who = $ptable.userid"); + push(@clist, "$ptable.login_name", $type, $email); + } + if (@clist) { + push(@specialchart, \@clist); + } else { + return Error("You must specify one or more fields in which to\n" . + "search for <tt>$email</tt>.\n"); + } + } + + + if (defined $F{'changedin'}) { + my $c = trim($F{'changedin'}); + if ($c ne "") { + if ($c !~ /^[0-9]*$/) { + return Error("The 'changed in last ___ days' field must be\n" . + "a simple number. You entered \"$c\", which\n" . + "doesn't cut it."); + } + push(@specialchart, ["changedin", + "lessthan", $c + 1]); + } + } + + my $ref = $M{'chfield'}; + + if (defined $ref) { + my $which = lsearch($ref, "[Bug creation]"); + if ($which >= 0) { + splice(@$ref, $which, 1); + push(@specialchart, ["creation_ts", "greaterthan", + SqlifyDate($F{'chfieldfrom'})]); + my $to = $F{'chfieldto'}; + if (defined $to) { + $to = trim($to); + if ($to ne "" && $to !~ /^now$/i) { + push(@specialchart, ["creation_ts", "lessthan", + SqlifyDate($to)]); + } + } + } + } + + + + if (defined $ref && 0 < @$ref) { + push(@supptables, "bugs_activity actcheck"); + + my @list; + foreach my $f (@$ref) { + push(@list, "\nactcheck.fieldid = " . GetFieldID($f)); + } + push(@wherepart, "actcheck.bug_id = bugs.bug_id"); + push(@wherepart, "(" . join(' OR ', @list) . ")"); + push(@wherepart, "actcheck.bug_when >= " . + SqlQuote(SqlifyDate($F{'chfieldfrom'}))); + my $to = $F{'chfieldto'}; + if (defined $to) { + $to = trim($to); + if ($to ne "" && $to !~ /^now$/i) { + push(@wherepart, "actcheck.bug_when <= " . + SqlQuote(SqlifyDate($to))); + } + } + my $value = $F{'chfieldvalue'}; + if (defined $value) { + $value = trim($value); + if ($value ne "") { + push(@wherepart, "actcheck.newvalue = " . + SqlQuote($value)) + } + } + } + + + foreach my $f ("short_desc", "long_desc", "bug_file_loc", + "status_whiteboard") { + if (defined $F{$f}) { + my $s = trim($F{$f}); + if ($s ne "") { + my $n = $f; + my $q = SqlQuote($s); + my $type = $F{$f . "_type"}; + push(@specialchart, [$f, $type, $s]); + } + } + } + + + my $chartid; + my $f; + my $ff; + my $t; + my $q; + my $v; + my $term; + my %funcsbykey; + my @funcdefs = + ( + "^(assigned_to|reporter)," => sub { + push(@supptables, "profiles map_$f"); + push(@wherepart, "bugs.$f = map_$f.userid"); + $f = "map_$f.login_name"; + }, + "^qa_contact," => sub { + push(@supptables, + "LEFT JOIN profiles map_qa_contact ON bugs.qa_contact = map_qa_contact.userid"); + $f = "map_$f.login_name"; + }, + "^cc," => sub { + push(@supptables, + ("LEFT JOIN cc cc_$chartid ON bugs.bug_id = cc_$chartid.bug_id LEFT JOIN profiles map_cc_$chartid ON cc_$chartid.who = map_cc_$chartid.userid")); + $f = "map_cc_$chartid.login_name"; + }, + "^long_?desc,changedby" => sub { + my $table = "longdescs_$chartid"; + push(@supptables, "longdescs $table"); + push(@wherepart, "$table.bug_id = bugs.bug_id"); + my $id = DBNameToIdAndCheck($v); + $term = "$table.who = $id"; + }, + "^long_?desc,changedbefore" => sub { + my $table = "longdescs_$chartid"; + push(@supptables, "longdescs $table"); + push(@wherepart, "$table.bug_id = bugs.bug_id"); + $term = "$table.bug_when < " . SqlQuote(SqlifyDate($v)); + }, + "^long_?desc,changedafter" => sub { + my $table = "longdescs_$chartid"; + push(@supptables, "longdescs $table"); + push(@wherepart, "$table.bug_id = bugs.bug_id"); + $term = "$table.bug_when > " . SqlQuote(SqlifyDate($v)); + }, + "^long_?desc," => sub { + my $table = "longdescs_$chartid"; + push(@supptables, "longdescs $table"); + push(@wherepart, "$table.bug_id = bugs.bug_id"); + $f = "$table.thetext"; + }, + "^attachments\..*," => sub { + my $table = "attachments_$chartid"; + push(@supptables, "LEFT JOIN attachments $table ON bugs.bug_id = $table.bug_id"); + $f =~ m/^attachments\.(.*)$/; + my $field = $1; + if ($t eq "changedby") { + $v = DBNameToIdAndCheck($v); + $q = SqlQuote($v); + $field = "submitter_id"; + $t = "equals"; + } elsif ($t eq "changedbefore") { + $v = SqlifyDate($v); + $q = SqlQuote($v); + $field = "creation_ts"; + $t = "lessthan"; + } elsif ($t eq "changedafter") { + $v = SqlifyDate($v); + $q = SqlQuote($v); + $field = "creation_ts"; + $t = "greaterthan"; + } + if ($field eq "ispatch") { + if ($v ne "0" && $v ne "1") { + return Error("The only legal values for the 'Attachment is patch' field is 0 or 1."); + } + } + $f = "$table.$field"; + }, + "^changedin," => sub { + $f = "(to_days(now()) - to_days(bugs.delta_ts))"; + }, + + "^keywords," => sub { + GetVersionTable(); + my @list; + my $table = "keywords_$chartid"; + foreach my $value (split(/[\s,]+/, $v)) { + if ($value eq '') { + next; + } + my $id = $::keywordsbyname{$value}; + if ($id) { + push(@list, "$table.keywordid = $id"); + } else { + return Error("Unknown keyword named <code>$v</code>.\n" . + "<P>The legal keyword names are\n" . + "<A HREF=describekeywords.cgi>" . + "listed here</A>.\n"); + } + } + my $haveawordterm; + if (@list) { + $haveawordterm = "(" . join(' OR ', @list) . ")"; + if ($t eq "anywords") { + $term = $haveawordterm; + } elsif ($t eq "allwords") { + $ref = $funcsbykey{",$t"}; + &$ref; + if ($term && $haveawordterm) { + $term = "(($term) AND $haveawordterm)"; + } + } + } + if ($term) { + push(@supptables, "keywords $table"); + push(@wherepart, "$table.bug_id = bugs.bug_id"); + } + }, + + "^(dependson|blocked)," => sub { + push(@supptables, "dependencies"); + $ff = "dependencies.$f"; + $ref = $funcsbykey{",$t"}; + &$ref; + push(@wherepart, "$term"); + }, + + + ",equals" => sub { + $term = "$ff = $q"; + }, + ",notequals" => sub { + $term = "$ff != $q"; + }, + ",casesubstring" => sub { + $term = "INSTR($ff, $q)"; + }, + ",(substring|substr)" => sub { + $term = "INSTR(LOWER($ff), " . lc($q) . ")"; + }, + ",notsubstring" => sub { + $term = "INSTR(LOWER($ff), " . lc($q) . ") = 0"; + }, + ",regexp" => sub { + $term = "LOWER($ff) REGEXP $q"; + }, + ",notregexp" => sub { + $term = "LOWER($ff) NOT REGEXP $q"; + }, + ",lessthan" => sub { + $term = "$ff < $q"; + }, + ",greaterthan" => sub { + $term = "$ff > $q"; + }, + ",anyexact" => sub { + my @list; + foreach my $w (split(/,/, $v)) { + if ($w eq "---" && $f !~ /milestone/) { + $w = ""; + } + push(@list, "$ff = " . SqlQuote($w)); + } + $term = join(" OR ", @list); + }, + ",anywords" => sub { + $term = join(" OR ", @{GetByWordList($ff, $v)}); + }, + ",allwords" => sub { + $term = join(" AND ", @{GetByWordList($ff, $v)}); + }, + ",nowords" => sub { + my @list = @{GetByWordList($ff, $v)}; + if (@list) { + $term = "NOT (" . join(" OR ", @list) . ")"; + } + }, + ",changedbefore" => sub { + my $table = "act_$chartid"; + my $ftable = "fielddefs_$chartid"; + push(@supptables, "bugs_activity $table"); + push(@supptables, "fielddefs $ftable"); + push(@wherepart, "$table.bug_id = bugs.bug_id"); + push(@wherepart, "$table.fieldid = $ftable.fieldid"); + $term = "($ftable.name = '$f' AND $table.bug_when < $q)"; + }, + ",changedafter" => sub { + my $table = "act_$chartid"; + my $ftable = "fielddefs_$chartid"; + push(@supptables, "bugs_activity $table"); + push(@supptables, "fielddefs $ftable"); + push(@wherepart, "$table.bug_id = bugs.bug_id"); + push(@wherepart, "$table.fieldid = $ftable.fieldid"); + $term = "($ftable.name = '$f' AND $table.bug_when > $q)"; + }, + ",changedto" => sub { + my $table = "act_$chartid"; + my $ftable = "fielddefs_$chartid"; + push(@supptables, "bugs_activity $table"); + push(@supptables, "fielddefs $ftable"); + push(@wherepart, "$table.bug_id = bugs.bug_id"); + push(@wherepart, "$table.fieldid = $ftable.fieldid"); + $term = "($ftable.name = '$f' AND $table.newvalue = $q)"; + }, + ",changedby" => sub { + my $table = "act_$chartid"; + my $ftable = "fielddefs_$chartid"; + push(@supptables, "bugs_activity $table"); + push(@supptables, "fielddefs $ftable"); + push(@wherepart, "$table.bug_id = bugs.bug_id"); + push(@wherepart, "$table.fieldid = $ftable.fieldid"); + my $id = DBNameToIdAndCheck($v); + $term = "($ftable.name = '$f' AND $table.who = $id)"; + }, + ); + my @funcnames; + while (@funcdefs) { + my $key = shift(@funcdefs); + my $value = shift(@funcdefs); + if ($key =~ /^[^,]*$/) { + die "All defs in %funcs must have a comma in their name: $key"; + } + if (exists $funcsbykey{$key}) { + die "Duplicate key in %funcs: $key"; + } + $funcsbykey{$key} = $value; + push(@funcnames, $key); + } + + + my $chart = -1; + my $row = 0; + foreach my $ref (@specialchart) { + my $col = 0; + while (@$ref) { + $F{"field$chart-$row-$col"} = shift(@$ref); + $F{"type$chart-$row-$col"} = shift(@$ref); + $F{"value$chart-$row-$col"} = shift(@$ref); + if ($debug) { + print qq{<P>$F{"field$chart-$row-$col"} | $F{"type$chart-$row-$col"} | $F{"value$chart-$row-$col"}*\n}; + } + $col++; + + } + $row++; + } + + + for ($chart=-1 ; + $chart < 0 || exists $F{"field$chart-0-0"} ; + $chart++) { + $chartid = $chart >= 0 ? $chart : ""; + for (my $row = 0 ; + exists $F{"field$chart-$row-0"} ; + $row++) { + my @orlist; + for (my $col = 0 ; + exists $F{"field$chart-$row-$col"} ; + $col++) { + $f = $F{"field$chart-$row-$col"} || "noop"; + $t = $F{"type$chart-$row-$col"} || "noop"; + $v = $F{"value$chart-$row-$col"}; + $v = "" if !defined $v; + $v = trim($v); + if ($f eq "noop" || $t eq "noop" || $v eq "") { + next; + } + $q = SqlQuote($v); + my $func; + $term = undef; + foreach my $key (@funcnames) { + if ("$f,$t" =~ m/$key/) { + my $ref = $funcsbykey{$key}; + if ($debug) { + print "<P>$key ($f , $t ) => "; + } + $ff = $f; + if ($f !~ /\./) { + $ff = "bugs.$f"; + } + &$ref; + if ($debug) { + print "$f , $t , $term"; + } + if ($term) { + last; + } + } + } + if ($term) { + push(@orlist, $term); + } else { + my $errstr = "Can't seem to handle " . + qq{'<code>$F{"field$chart-$row-$col"}</code>' and } . + qq{'<code>$F{"type$chart-$row-$col"}</code>' } . + "together"; + die "Internal error: $errstr" if $chart < 0; + return Error($errstr); + } + } + if (@orlist) { + push(@andlist, "(" . join(" OR ", @orlist) . ")"); + } + } + } + my %suppseen = ("bugs" => 1); + my $suppstring = "bugs"; + foreach my $str (@supptables) { + if (!$suppseen{$str}) { + if ($str !~ /^LEFT JOIN/i) { + $suppstring .= ","; + } + $suppstring .= " $str"; + $suppseen{$str} = 1; + } + } + my $query = ("SELECT " . join(', ', @fields) . + " FROM $suppstring" . + " WHERE " . join(' AND ', (@wherepart, @andlist)) . + " GROUP BY bugs.bug_id"); + if ($debug) { + print "<P><CODE>" . value_quote($query) . "</CODE><P>\n"; + exit(); + } + return $query; +} + + + +sub LookupNamedQuery { + my ($name) = (@_); + confirm_login(); + my $userid = DBNameToIdAndCheck($::COOKIE{"Bugzilla_login"}); + SendSQL("SELECT query FROM namedqueries " . + "WHERE userid = $userid AND name = " . SqlQuote($name)); + my $result = FetchOneColumn(); + if (!defined $result) { + print "Content-type: text/html\n\n"; + PutHeader("Something weird happened"); + print qq{The named query $name seems to no longer exist.}; + PutFooter(); + exit; + } + return $result; +} + + + + + CMD: for ($::FORM{'cmdtype'}) { /^runnamed$/ && do { - $::buffer = $::COOKIE{"QUERY_" . $::FORM{"namedcmd"}}; + $::buffer = LookupNamedQuery($::FORM{"namedcmd"}); ProcessFormFields($::buffer); last CMD; }; /^editnamed$/ && do { - my $url = "query.cgi?" . $::COOKIE{"QUERY_" . $::FORM{"namedcmd"}}; - print "Content-type: text/html + my $url = "query.cgi?" . LookupNamedQuery($::FORM{"namedcmd"}); + print qq{Content-type: text/html Refresh: 0; URL=$url <TITLE>What a hack.</TITLE> -Loading your query named <B>$::FORM{'namedcmd'}</B>... -"; +<A HREF="$url">Loading your query named <B>$::FORM{'namedcmd'}</B>...</A> +}; exit; }; /^forgetnamed$/ && do { - print "Set-Cookie: QUERY_" . $::FORM{'namedcmd'} . "= ; path=/ ; expires=Sun, 30-Jun-2029 00:00:00 GMT -Content-type: text/html + confirm_login(); + my $userid = DBNameToIdAndCheck($::COOKIE{"Bugzilla_login"}); + SendSQL("DELETE FROM namedqueries WHERE userid = $userid " . + "AND name = " . SqlQuote($::FORM{'namedcmd'})); + + print "Content-type: text/html\n\n"; + PutHeader("Forget what?", ""); -<HTML> -<TITLE>Forget what?</TITLE> + print qq{ OK, the <B>$::FORM{'namedcmd'}</B> query is gone. <P> -<A HREF=query.cgi>Go back to the query page.</A> -"; - exit; - }; - /^asnamed$/ && do { - if ($::FORM{'newqueryname'} =~ /^[a-zA-Z0-9_ ]+$/) { - print "Set-Cookie: QUERY_" . $::FORM{'newqueryname'} . "=$::buffer ; path=/ ; expires=Sun, 30-Jun-2029 00:00:00 GMT -Content-type: text/html - -<HTML> -<TITLE>OK, done.</TITLE> -OK, you now have a new query named <B>$::FORM{'newqueryname'}</B>. - -<P> - -<A HREF=query.cgi>Go back to the query page.</A> -"; - } else { - print "Content-type: text/html - -<HTML> -<TITLE>Picky, picky.</TITLE> -Query names can only have letters, digits, spaces, or underbars. You entered -\"<B>$::FORM{'newqueryname'}</B>\", which doesn't cut it. -<P> -Click the <B>Back</B> button and type in a valid name for this query. -"; - } +<A HREF="query.cgi">Go back to the query page.</A> +}; + PutFooter(); exit; }; /^asdefault$/ && do { - print "Set-Cookie: DEFAULTQUERY=$::buffer ; path=/ ; expires=Sun, 30-Jun-2029 00:00:00 GMT -Content-type: text/html - -<HTML> -<TITLE>OK, default is set.</TITLE> + confirm_login(); + my $userid = DBNameToIdAndCheck($::COOKIE{"Bugzilla_login"}); + print "Content-type: text/html\n\n"; + SendSQL("REPLACE INTO namedqueries (userid, name, query) VALUES " . + "($userid, '$::defaultqueryname'," . + SqlQuote($::buffer) . ")"); + PutHeader("OK, default is set"); + print qq{ OK, you now have a new default query. You may also bookmark the result of any individual query. -<P><A HREF=query.cgi>Go back to the query page, using the new default.</A> -"; +<P><A HREF="query.cgi">Go back to the query page, using the new default.</A> +}; + PutFooter(); + exit(); + }; + /^asnamed$/ && do { + confirm_login(); + my $userid = DBNameToIdAndCheck($::COOKIE{"Bugzilla_login"}); + print "Content-type: text/html\n\n"; + my $name = trim($::FORM{'newqueryname'}); + if ($name eq "" || $name =~ /[<>&]/) { + PutHeader("Please pick a valid name for your new query"); + print "Click the <B>Back</B> button and type in a valid name\n"; + print "for this query. (Query names should not contain unusual\n"; + print "characters.)\n"; + PutFooter(); + exit(); + } + $::buffer =~ s/[\&\?]cmdtype=[a-z]+//; + my $qname = SqlQuote($name); + SendSQL("SELECT query FROM namedqueries " . + "WHERE userid = $userid AND name = $qname"); + if (!FetchOneColumn()) { + SendSQL("REPLACE INTO namedqueries (userid, name, query) " . + "VALUES ($userid, $qname, " . SqlQuote($::buffer) . ")"); + } else { + SendSQL("UPDATE namedqueries SET query = " . SqlQuote($::buffer) . + " WHERE userid = $userid AND name = $qname"); + } + PutHeader("OK, query saved."); + print qq{ +OK, you have a new query named <code>$name</code> +<P> +<BR><A HREF="query.cgi">Go back to the query page</A> +}; + PutFooter(); exit; }; } -my $serverpush = 0; -if ($ENV{'HTTP_USER_AGENT'} =~ /Mozilla.[3-9]/ && $ENV{'HTTP_USER_AGENT'} !~ /[Cc]ompatible/ ) { +if (exists $ENV{'HTTP_USER_AGENT'} && $ENV{'HTTP_USER_AGENT'} =~ /Mozilla.[3-9]/ && $ENV{'HTTP_USER_AGENT'} !~ /[Cc]ompatible/ ) { # Search for real Netscape 3 and up. http://www.browsercaps.org used as source of # browsers compatbile with server-push. It's a Netscape hack, incompatbile # with MSIE and Lynx (at least). Even Communicator 4.51 has bugs with it, @@ -149,18 +782,20 @@ sub DefCol { $::needquote{$name} = $q; } -DefCol("opendate", "date_format(bugs.creation_ts,'Y-m-d')", "Opened", +DefCol("opendate", "date_format(bugs.creation_ts,'%Y-%m-%d')", "Opened", "bugs.creation_ts"); -DefCol("changeddate", "date_format(bugs.delta_ts,'Y-m-d')", "Changed", +DefCol("changeddate", "date_format(bugs.delta_ts,'%Y-%m-%d')", "Changed", "bugs.delta_ts"); DefCol("severity", "substring(bugs.bug_severity, 1, 3)", "Sev", "bugs.bug_severity"); DefCol("priority", "substring(bugs.priority, 1, 3)", "Pri", "bugs.priority"); DefCol("platform", "substring(bugs.rep_platform, 1, 3)", "Plt", "bugs.rep_platform"); -DefCol("owner", "assign.login_name", "Owner", "assign.login_name"); -DefCol("reporter", "report.login_name", "Reporter", "report.login_name"); -DefCol("qa_contact", "qacont.login_name", "QAContact", "qacont.login_name"); +DefCol("owner", "map_assigned_to.login_name", "Owner", + "map_assigned_to.login_name"); +DefCol("reporter", "map_reporter.login_name", "Reporter", + "map_reporter.login_name"); +DefCol("qa_contact", "map_qa_contact.login_name", "QAContact", "map_qa_contact.login_name"); DefCol("status", "substring(bugs.bug_status,1,4)", "State", "bugs.bug_status"); DefCol("resolution", "substring(bugs.resolution,1,4)", "Result", "bugs.resolution"); @@ -175,6 +810,7 @@ DefCol("os", "substring(bugs.op_sys, 1, 4)", "OS", "bugs.op_sys"); DefCol("target_milestone", "bugs.target_milestone", "TargetM", "bugs.target_milestone"); DefCol("votes", "bugs.votes", "Votes", "bugs.votes desc"); +DefCol("keywords", "bugs.keywords", "Keywords", "bugs.keywords"); my @collist; if (defined $::COOKIE{'COLUMNLIST'}) { @@ -184,21 +820,11 @@ if (defined $::COOKIE{'COLUMNLIST'}) { } my $minvotes; -my $votecolnum; if (defined $::FORM{'votes'}) { - my $c = trim($::FORM{'votes'}); - if ($c ne "") { - if ($c !~ /^[0-9]*$/) { - print "\n\n<P>The 'At least ___ votes' field must be a simple "; - print "number. You entered \"$c\", which doesn't cut it."; - print "<P>Please click the <B>Back</B> button and try again.\n"; - exit; - } - $minvotes = $c; + if (trim($::FORM{'votes'}) ne "") { if (! (grep {/^votes$/} @collist)) { push(@collist, 'votes'); } - $votecolnum = lsearch(\@collist, 'votes'); } } @@ -207,264 +833,74 @@ my $dotweak = defined $::FORM{'tweak'}; if ($dotweak) { confirm_login(); + if (!UserInGroup("editbugs")) { + print qq{ +Sorry; you do not have sufficient priviledges to edit a bunch of bugs +at once. +}; + PutFooter(); + exit(); + } } else { quietly_check_login(); } -my $query = "select bugs.bug_id, bugs.groupset"; +my @fields = ("bugs.bug_id", "bugs.groupset"); foreach my $c (@collist) { if (exists $::needquote{$c}) { - $query .= ", -\t$::key{$c}"; + push(@fields, "$::key{$c}"); } } if ($dotweak) { - $query .= ", -bugs.product, -bugs.bug_status"; -} - - -$query .= " -from bugs, - profiles assign, - profiles report - left join profiles qacont on bugs.qa_contact = qacont.userid, - versions projector - -where bugs.assigned_to = assign.userid -and bugs.reporter = report.userid -and bugs.product = projector.program -and bugs.version = projector.value -and bugs.groupset & $::usergroupset = bugs.groupset -"; - -if ((defined $::FORM{'emailcc1'} && $::FORM{'emailcc1'}) || - (defined $::FORM{'emailcc2'} && $::FORM{'emailcc2'})) { - - # We need to poke into the CC table. Do weird SQL left join stuff so that - # we can look in the CC table, but won't reject any bugs that don't have - # any CC fields. - $query =~ s/bugs,/bugs left join cc on bugs.bug_id = cc.bug_id left join profiles ccname on cc.who = ccname.userid,/; -} - -if (defined $::FORM{'sql'}) { - $query .= "and (\n$::FORM{'sql'}\n)" -} else { - my @legal_fields = ("bug_id", "product", "version", "rep_platform", "op_sys", - "bug_status", "resolution", "priority", "bug_severity", - "assigned_to", "reporter", "component", - "target_milestone", "groupset"); - - foreach my $field (keys %::FORM) { - my $or = ""; - if (lsearch(\@legal_fields, $field) != -1 && $::FORM{$field} ne "") { - $query .= "\tand (\n"; - if ($field eq "assigned_to" || $field eq "reporter") { - foreach my $p (split(/,/, $::FORM{$field})) { - my $whoid = DBNameToIdAndCheck($p); - $query .= "\t\t${or}bugs.$field = $whoid\n"; - $or = "or "; - } - } else { - my $ref = $::MFORM{$field}; - foreach my $v (@$ref) { - if ($v eq "(empty)") { - $query .= "\t\t${or}bugs.$field is null\n"; - } else { - if ($v eq "---") { - $query .= "\t\t${or}bugs.$field = ''\n"; - } else { - $query .= "\t\t${or}bugs.$field = " . SqlQuote($v) . - "\n"; - } - } - $or = "or "; - } - } - $query .= "\t)\n"; - } - } + push(@fields, "bugs.product", "bugs.bug_status"); } -foreach my $id ("1", "2") { - if (!defined ($::FORM{"email$id"})) { - next; - } - my $email = trim($::FORM{"email$id"}); - if ($email eq "") { - next; - } - my $qemail = SqlQuote($email); - my $type = $::FORM{"emailtype$id"}; - my $emailid; - if ($type eq "exact") { - $emailid = DBNameToIdAndCheck($email); - } - my $foundone = 0; - my $lead= "and (\n"; - foreach my $field ("assigned_to", "reporter", "cc", "qa_contact") { - my $doit = $::FORM{"email$field$id"}; - if (!$doit) { - next; - } - $foundone = 1; - my $table; - if ($field eq "assigned_to") { - $table = "assign"; - } elsif ($field eq "reporter") { - $table = "report"; - } elsif ($field eq "qa_contact") { - $table = "qacont"; - } else { - $table = "ccname"; - } - if ($type eq "exact") { - if ($field eq "cc") { - $query .= "\t$lead cc.who = $emailid\n"; - } else { - $query .= "\t$lead $field = $emailid\n"; - } - } elsif ($type eq "regexp") { - $query .= "\t$lead $table.login_name regexp $qemail\n"; - } elsif ($type eq "notregexp") { - $query .= "\t$lead $table.login_name not regexp $qemail\n"; - } else { - $query .= "\t$lead instr($table.login_name, $qemail)\n"; - } - $lead = " or "; - } - if (!$foundone) { - print "\n\n<P>You must specify one or more fields in which to search for <tt>$email</tt>.\n"; - print "<P>Please click the <B>Back</B> button and try again.\n"; +if ($::FORM{'regetlastlist'}) { + if (!$::COOKIE{'BUGLIST'}) { + print qq{ +Sorry, I seem to have lost the cookie that recorded the results of your last +query. You will have to start over at the <A HREF="query.cgi">query page</A>. +}; + PutFooter(); exit; } - if ($lead eq " or ") { - $query .= ")\n"; - } -} - - - - - -if (defined $::FORM{'changedin'}) { - my $c = trim($::FORM{'changedin'}); - if ($c ne "") { - if ($c !~ /^[0-9]*$/) { - print "\n\n<P>The 'changed in last ___ days' field must be a simple "; - print "number. You entered \"$c\", which doesn't cut it."; - print "<P>Please click the <B>Back</B> button and try again.\n"; - exit; - } - $query .= "and to_days(now()) - to_days(bugs.delta_ts) <= $c "; + my @list = split(/:/, $::COOKIE{'BUGLIST'}); + $::FORM{'bug_id'} = join(',', @list); + if (!$::FORM{'order'}) { + $::FORM{'order'} = 'reuse last sort'; } -} - -if (defined $minvotes) { - $query .= "and votes >= $minvotes "; + $::buffer = "bug_id=" . $::FORM{'bug_id'} . "&order=" . + url_quote($::FORM{'order'}); } -my $ref = $::MFORM{'chfield'}; - - -sub SqlifyDate { - my ($str) = (@_); - if (!defined $str) { - $str = ""; - } - my $date = str2time($str); - if (!defined $date) { - print "\n\n<P>The string '<tt>$str</tt>' is not a legal date.\n"; - print "<P>Please click the <B>Back</B> button and try again.\n"; - exit; - } - return time2str("'%Y/%m/%d %H:%M:%S'", $date); -} +ReconnectToShadowDatabase(); -if (defined $ref) { - my $which = lsearch($ref, "[Bug creation]"); - if ($which >= 0) { - splice(@$ref, $which, 1); - $query .= "and bugs.creation_ts >= " . - SqlifyDate($::FORM{'chfieldfrom'}) . "\n"; - my $to = $::FORM{'chfieldto'}; - if (defined $to) { - $to = trim($to); - if ($to ne "" && $to !~ /^now$/i) { - $query .= "and bugs.creation_ts <= " . - SqlifyDate($to) . "\n"; - } - } - } -} +my $query = GenerateSQL(\@fields, undef, undef, $::buffer); -if (defined $ref && 0 < @$ref) { - # Do surgery on the query to tell it to patch in the bugs_activity - # table. - $query =~ s/bugs,/bugs, bugs_activity,/; - - my @list; - foreach my $f (@$ref) { - push(@list, "\nbugs_activity.field = " . SqlQuote($f)); - } - $query .= "and bugs_activity.bug_id = bugs.bug_id and (" . - join(' or ', @list) . ") "; - $query .= "and bugs_activity.bug_when >= " . - SqlifyDate($::FORM{'chfieldfrom'}) . "\n"; - my $to = $::FORM{'chfieldto'}; - if (defined $to) { - $to = trim($to); - if ($to ne "" && $to !~ /^now$/i) { - $query .= "and bugs_activity.bug_when <= " . SqlifyDate($to) . "\n"; - } - } - my $value = $::FORM{'chfieldvalue'}; - if (defined $value) { - $value = trim($value); - if ($value ne "") { - $query .= "and bugs_activity.newvalue = " . - SqlQuote($value) . "\n"; - } - } -} - -foreach my $f ("short_desc", "long_desc", "bug_file_loc", - "status_whiteboard") { - if (defined $::FORM{$f}) { - my $s = trim($::FORM{$f}); - if ($s ne "") { - $s = SqlQuote($s); - if ($::FORM{$f . "_type"} eq "regexp") { - $query .= "and $f regexp $s\n"; - } elsif ($::FORM{$f . "_type"} eq "notregexp") { - $query .= "and $f not regexp $s\n"; - } elsif ($::FORM{$f . "_type"} eq "casesubstring") { - $query .= "and instr($f, $s)\n"; - } else { - $query .= "and instr(lower($f), lower($s))\n"; - } - } +if ($::COOKIE{'LASTORDER'}) { + if ((!$::FORM{'order'}) || $::FORM{'order'} =~ /^reuse/i) { + $::FORM{'order'} = url_decode($::COOKIE{'LASTORDER'}); } } -$query .= "group by bugs.bug_id\n"; - if (defined $::FORM{'order'} && $::FORM{'order'} ne "") { - $query .= "order by "; + $query .= " ORDER BY "; $::FORM{'order'} =~ s/votesum/bugs.votes/; # Silly backwards compatability # hack. + $::FORM{'order'} =~ s/assign\.login_name/map_assigned_to.login_name/g; + # Another backwards compatability hack. + ORDER: for ($::FORM{'order'}) { /\./ && do { # This (hopefully) already has fieldnames in it, so we're done. @@ -479,13 +915,29 @@ if (defined $::FORM{'order'} && $::FORM{'order'} ne "") { last ORDER; }; /Assign/ && do { - $::FORM{'order'} = "assign.login_name, bugs.bug_status, priority, bugs.bug_id"; + $::FORM{'order'} = "map_assigned_to.login_name, bugs.bug_status, priority, bugs.bug_id"; last ORDER; }; # DEFAULT - $::FORM{'order'} = "bugs.bug_status, bugs.priority, assign.login_name, bugs.bug_id"; + $::FORM{'order'} = "bugs.bug_status, bugs.priority, map_assigned_to.login_name, bugs.bug_id"; } - $query .= $::FORM{'order'}; + die "Invalid order: $::FORM{'order'}" unless + $::FORM{'order'} =~ /^([a-zA-Z0-9_., ]+)$/; + + # Extra special disgusting hack: if we are ordering by target_milestone, + # change it to order by the sortkey of the target_milestone first. + my $order = $::FORM{'order'}; + if ($order =~ /bugs.target_milestone/) { + $query =~ s/ WHERE / LEFT JOIN milestones ms_order ON ms_order.value = bugs.target_milestone AND ms_order.product = bugs.product WHERE /; + $order =~ s/bugs.target_milestone/ms_order.sortkey,ms_order.value/; + } + + $query .= $order; +} + + +if ($::FORM{'debug'} && $serverpush) { + print "<P><CODE>" . value_quote($query) . "</CODE><P>\n"; } @@ -506,11 +958,14 @@ $fields =~ s/[&?]order=[^&]*//g; $fields =~ s/[&?]cmdtype=[^&]*//g; +my $orderpart; my $oldorder; if (defined $::FORM{'order'} && trim($::FORM{'order'}) ne "") { + $orderpart = "&order=" . url_quote("$::FORM{'order'}"); $oldorder = url_quote(", $::FORM{'order'}"); } else { + $orderpart = ""; $oldorder = ""; } @@ -518,26 +973,52 @@ if ($dotweak) { pnl "<FORM NAME=changeform METHOD=POST ACTION=\"process_bug.cgi\">"; } -my $tablestart = "<TABLE CELLSPACING=0 CELLPADDING=2> -<TR ALIGN=LEFT><TH> -<A HREF=\"buglist.cgi?$fields&order=bugs.bug_id\">ID</A>"; - +my @th; foreach my $c (@collist) { if (exists $::needquote{$c}) { + my $h = ""; if ($::needquote{$c}) { - $tablestart .= "<TH WIDTH=100% valign=left>"; + $h .= "<TH WIDTH=100%>"; } else { - $tablestart .= "<TH valign=left>"; + $h .= "<TH>"; } if (defined $::sortkey{$c}) { - $tablestart .= "<A HREF=\"buglist.cgi?$fields&order=" . url_quote($::sortkey{$c}) . "$oldorder\">$::title{$c}</A>"; + $h .= "<A HREF=\"buglist.cgi?$fields&order=" . url_quote($::sortkey{$c}) . "$oldorder\">$::title{$c}</A>"; } else { - $tablestart .= $::title{$c}; + $h .= $::title{$c}; + } + $h .= "</TH>"; + push(@th, $h); + } +} + +my $tablestart = "<TABLE CELLSPACING=0 CELLPADDING=4 WIDTH=100%> +<TR ALIGN=LEFT><TH> +<A HREF=\"buglist.cgi?$fields&order=bugs.bug_id\">ID</A>"; + +my $splitheader = 0; +if ($::COOKIE{'SPLITHEADER'}) { + $splitheader = 1; +} + +if ($splitheader) { + $tablestart =~ s/<TH/<TH COLSPAN="2"/; + for (my $pass=0 ; $pass<2 ; $pass++) { + if ($pass == 1) { + $tablestart .= "</TR>\n<TR><TD></TD>"; + } + for (my $i=1-$pass ; $i<@th ; $i += 2) { + my $h = $th[$i]; + $h =~ s/TH/TH COLSPAN="2" ALIGN="left"/; + $tablestart .= $h; } } +} else { + $tablestart .= join("", @th); } + $tablestart .= "\n"; @@ -547,6 +1028,19 @@ my @bugarray; my %prodhash; my %statushash; my $buggroupset = ""; +my %ownerhash; + +my $pricol = -1; +my $sevcol = -1; +for (my $colcount = 0 ; $colcount < @collist ; $colcount++) { + my $colname = $collist[$colcount]; + if ($colname eq "priority") { + $pricol = $colcount; + } + if ($colname eq "severity") { + $sevcol = $colcount; + } +} while (@row = FetchSQLData()) { my $bug_id = shift @row; @@ -567,26 +1061,51 @@ while (@row = FetchSQLData()) { pnl "</TABLE>$tablestart"; } push @bugarray, $bug_id; - pnl "<TR VALIGN=TOP ALIGN=LEFT><TD>"; + + # retrieve this bug's priority and severity, if available, + # by looping through all column names -- gross but functional + my $priority = "unknown"; + my $severity; + if ($pricol >= 0) { + $priority = $row[$pricol]; + } + if ($sevcol >= 0) { + $severity = $row[$sevcol]; + } + my $customstyle = ""; + if ($severity) { + if ($severity eq "enhan") { + $customstyle = "style='font-style:italic ! important'"; + } + if ($severity eq "block") { + $customstyle = "style='color:red ! important; font-weight:bold ! important'"; + } + if ($severity eq "criti") { + $customstyle = "style='color:red; ! important'"; + } + } + pnl "<TR VALIGN=TOP ALIGN=LEFT CLASS=$priority $customstyle><TD>"; if ($dotweak) { pnl "<input type=checkbox name=id_$bug_id>"; } pnl "<A HREF=\"show_bug.cgi?id=$bug_id\">"; pnl "$bug_id</A> "; foreach my $c (@collist) { - if (!exists $::needquote{$c}) { - next; - } - my $value = shift @row; - if (!defined $value) { - next; - } - if ($::needquote{$c}) { - $value = html_quote($value); - } else { - $value = "<nobr>$value</nobr>"; + if (exists $::needquote{$c}) { + my $value = shift @row; + if (!defined $value) { + next; + } + if ($c eq "owner") { + $ownerhash{$value} = 1; + } + if ($::needquote{$c}) { + $value = html_quote($value); + } else { + $value = "<nobr>$value</nobr>"; + } + pnl "<td class=$c>$value"; } - pnl "<td>$value"; } if ($dotweak) { my $value = shift @row; @@ -624,6 +1143,10 @@ if ($serverpush) { # Note! HTML header not yet closed } my $toolong = 0; +if ($::FORM{'order'}) { + my $q = url_quote($::FORM{'order'}); + print "Set-Cookie: LASTORDER=$q ; path=/; expires=Sun, 30-Jun-2029 00:00:00 GMT\n"; +} if (length($buglist) < 4000) { print "Set-Cookie: BUGLIST=$buglist\n\n"; } else { @@ -638,7 +1161,7 @@ print " <B>" . time2str("%a %b %e %T %Z %Y", time()) . "</B>"; if (defined $::FORM{'debug'}) { - print "<PRE>$query</PRE>\n"; + print "<P><CODE>" . value_quote($query) . "</CODE><P>\n"; } if ($toolong) { @@ -667,6 +1190,10 @@ if ($count == 0) { # So, when you query for a list of bugs, and it gets no results, you # can think of this as a friendly reminder. Of *course* there are bugs # matching your query, they just aren't in the bugsystem yet... + + print qq{<p><A HREF="query.cgi">Query Page</A>\n}; + print qq{ <A HREF="enter_bug.cgi">Enter New Bug</A>\n}; + print qq{<NOBR><A HREF="query.cgi?$::buffer">Edit this query</A></NOBR>\n}; } elsif ($count == 1) { print "One bug found.\n"; } else { @@ -679,6 +1206,7 @@ if ($dotweak) { <SCRIPT> numelements = document.changeform.elements.length; function SetCheckboxes(value) { + var item; for (var i=0 ; i<numelements ; i++) { item = document.changeform.elements\[i\]; item.checked = value; @@ -726,9 +1254,11 @@ document.write(\" <input type=button value=\\\"Uncheck All\\\" onclick=\\\"SetCh </TR>"; if (Param("usetargetmilestone")) { - push(@::legal_target_milestone, " "); - my $tfm_popup = make_options(\@::legal_target_milestone, - $::dontchange); + my @legal_milestone; + if(1 == @prod_list) { + @legal_milestone = @{$::target_milestone{$prod_list[0]}}; + } + my $tfm_popup = make_options(\@legal_milestone, $::dontchange); print " <TR> <TD ALIGN=RIGHT><B>Target milestone:</B></TD> @@ -746,8 +1276,22 @@ document.write(\" <input type=button value=\\\"Uncheck All\\\" onclick=\\\"SetCh } - print " -</TABLE> + if (@::legal_keywords) { + print qq{ +<TR><TD><B><A HREF="describekeywords.cgi">Keywords</A>:</TD> +<TD COLSPAN=3><INPUT NAME=keywords SIZE=32 VALUE=""> +<SELECT NAME="keywordaction"> +<OPTION VALUE="add">Add these keywords +<OPTION VALUE="delete">Delete these keywords +<OPTION VALUE="makeexact">Make the keywords be exactly this list +</SELECT> +</TD> +</TR> +}; + } + + + print "</TABLE> <INPUT NAME=multiupdate value=Y TYPE=hidden> @@ -755,7 +1299,7 @@ document.write(\" <input type=button value=\\\"Uncheck All\\\" onclick=\\\"SetCh <BR> <TEXTAREA WRAP=HARD NAME=comment ROWS=5 COLS=80></TEXTAREA><BR>"; -if ($::usergroupset ne '0' && $buggroupset =~ /^\d*$/) { +if ($::usergroupset ne '0' && $buggroupset =~ /^\d+$/) { SendSQL("select bit, description, (bit & $buggroupset != 0) from groups where bit & $::usergroupset != 0 and isbuggroup != 0 order by bit"); while (MoreSQLData()) { my ($bit, $description, $ison) = (FetchSQLData()); @@ -779,6 +1323,12 @@ if ($::usergroupset ne '0' && $buggroupset =~ /^\d*$/) { <INPUT TYPE=radio NAME=knob VALUE=none CHECKED> Do nothing else<br>"; $knum++; + if ($statushash{$::unconfirmedstate} && 1 == scalar(keys(%statushash))) { + print " +<INPUT TYPE=radio NAME=knob VALUE=confirm> + Confirm bugs (change status to <b>NEW</b>)<br>"; + } + $knum++; print " <INPUT TYPE=radio NAME=knob VALUE=accept> Accept bugs (change status to <b>ASSIGNED</b>)<br>"; @@ -850,15 +1400,32 @@ if ($count > 0) { print "<FORM METHOD=POST ACTION=\"long_list.cgi\"> <INPUT TYPE=HIDDEN NAME=buglist VALUE=$buglist> <INPUT TYPE=SUBMIT VALUE=\"Long Format\"> -<A HREF=\"query.cgi\">Query Page</A> - <A HREF=\"enter_bug.cgi\">Enter New Bug</A> - <A HREF=\"colchange.cgi?$::buffer\">Change columns</A>"; - if (!$dotweak && $count > 1) { - print " <A HREF=\"buglist.cgi?$fields&tweak=1\">"; - print "Change several bugs at once</A>\n"; +<NOBR><A HREF=\"query.cgi\">Query Page</A></NOBR> + +<NOBR><A HREF=\"enter_bug.cgi\">Enter New Bug</A></NOBR> + +<NOBR><A HREF=\"colchange.cgi?$::buffer\">Change columns</A></NOBR>"; + if (!$dotweak && $count > 1 && UserInGroup("editbugs")) { + print " \n"; + print "<NOBR><A HREF=\"buglist.cgi?$fields$orderpart&tweak=1\">"; + print "Change several bugs at once</A></NOBR>\n"; + } + my @owners = sort(keys(%ownerhash)); + if (@owners > 1 && UserInGroup("editbugs")) { + my $suffix = Param('emailsuffix'); + if ($suffix ne "") { + map(s/$/$suffix/, @owners); + } + my $list = join(',', @owners); + print qq{ \n}; + print qq{<NOBR><A HREF="mailto:$list">Send mail to bug owners</A></NOBR>\n}; } + print qq{ \n}; + print qq{<NOBR><A HREF="query.cgi?$::buffer">Edit this query</A></NOBR>\n}; print "</FORM>\n"; } +PutFooter(); + if ($serverpush) { print "\n--thisrandomstring--\n"; } diff --git a/bugwritinghelp.html b/bugwritinghelp.html new file mode 100644 index 0000000000000000000000000000000000000000..11bb7f3f5cb9e052cc4a8eece8dabbb19dd16191 --- /dev/null +++ b/bugwritinghelp.html @@ -0,0 +1,281 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN"> +<HTML> + +<HEAD> + <META NAME="GENERATOR" Content="Symantec Visual Page 1.0"> + <META HTTP-EQUIV="Content-Type" CONTENT="text/html;CHARSET=iso-8859-1"> + <TITLE>Bug Writing Guidelines</TITLE> +</HEAD> + +<BODY TEXT="#000000" BGCOLOR="#FFFFFF" LINK="#0000EE" VLINK="#551A8B" ALINK="#FF0000"> + +<H1 ALIGN="CENTER">bug writing guidelines</H1> +<P ALIGN="CENTER"><FONT SIZE="2"><B>(Please send feedback/update requests to </B></FONT><A +HREF="mailto:eli@prometheus-music.com"><FONT SIZE="2"><B>Eli Goldberg</B></FONT></A><FONT +SIZE="2"><B>)</B></FONT></P> +<P><FONT SIZE="4"><B><BR> +Why You Should Read This</B></FONT> + + +<BLOCKQUOTE> + <P>Simply put, the more effectively you report a bug, the more likely an engineer + will actually fix it. <BR> + <A HREF="http://bugzilla.mozilla.org"><BR> + </A>These bug writing guidelines are an attempt at a general tutorial on writing + effective bug reports for novice bug writers; not every sentence may precisely apply + to your software project. + +</BLOCKQUOTE> + +<P><FONT SIZE="4"><B><BR> +How to Write a Useful Bug Report</B></FONT> + + +<BLOCKQUOTE> + <P>Useful bug reports are ones that get bugs fixed. A useful bug report normally + has two qualities: + + <OL> + <LI><B>Reproducible.</B> If an engineer can't see it or conclusively prove that it + exists, the engineer will probably stamp it "WORKSFORME" or "INVALID", + and move on to the next bug. Every detail you can provide helps. <BR> + <BR> + + <LI><B>Specific.</B> The quicker the engineer can isolate the issue to a specific + problem, the more likely it'll be expediently fixed.<B> </B>(If a programmer or tester + has to decypher a bug, they spend more time cursing the submitter than fixing or + testing the problem.) + </OL> + + <P>Let's say the application you're testing is a web browser. You crash at foo.com, + and want to write up a bug report: + + <BLOCKQUOTE> + <P><B>BAD:</B> "My browser crashed. I think I was on foo.com. My computer uses + Windows. I think that this is a really bad problem and you should fix it now. By + the way, your icons really suck. Nobody will use your software if you keep those + ugly icons. Oh, and my grandmother's home page doesn't look right, either, it's all + messed up. Good luck."<BR> + <BR> + <B>GOOD: </B>"I crashed each time when I went to foo.com, using the 10.28.99 + build on a Win NT 4.0 (Service Pack 5) system. I also rebooted into Linux, and reproduced + this problem using the 10.28.99 Linux build.<BR> + <BR> + It again crashed each time upon drawing the Foo banner at the top of the page. I + broke apart the page, and discovered that the following image link will crash the + application reproducibly, unless you remove the "border=0" attribute:<BR> + <BR> + <FONT SIZE="2"><TT><IMG SRC="http://foo.com/images/topics/topicfoos.gif" + width=34 height=44 border=0 alt="News"></TT>"</FONT> + + </BLOCKQUOTE> + +</BLOCKQUOTE> + +<P><FONT SIZE="4"><B><BR> +<BR> +How to Enter your Useful Bug Report into Bugzilla</B>:</FONT> + + +<BLOCKQUOTE> + <P>Before you enter your bug, use the Bugzilla Query Page to determine whether the + defect you've discovered is a known bug, and has already been reported. (If your + bug is the 37th duplicate of a known issue, you're more likely to annoy the engineer. + Annoyed engineers fix fewer bugs.)<BR> + <BR> + Next, be sure that you've reproduced your bug using a recent build. (Engineers tend + to be most interested in problems afflicting the code base that they're actively + working on, rather than those in a code base that's hundreds of bug fixes obsolete.)<BR> + <BR> + If you've discovered a new bug using a current build, report it in Bugzilla: + +</BLOCKQUOTE> + + +<OL> + <OL> + <LI>From your Bugzilla main page, choose "Enter a new bug". + <LI>Select the product that you've found a bug in. + <LI>Enter your E-mail address, Password, and press the "Login" button. + (If you don't yet have a password, leave the password text box empty, and press the + "E-mail me a password" button instead. You'll receive an E-mail message + with your password shortly.) + </OL> + <P>Now, fill out the form. Here's what it all means: +</OL> + + + +<BLOCKQUOTE> + <P><B>Where did you find the bug?</B> + + <BLOCKQUOTE> + <P><B>Product: In which product did you find the bug?</B><BR> + You just filled this out on the last page.</P> + <P><B>Version: In which product version did you find the bug?</B><BR> + If applicable.</P> + <P><B>Component: In which component does the bug exist?</B><BR> + Bugzilla requires that you select a component to enter a bug. (If they all look meaningless, + click on the Component link, which links to descriptions of each component, to help + you make the best choice.)</P> + <P><B>Platform: On which hardware platform did you find this bug?</B><FONT SIZE="2"><B> + </B>(e.g. Macintosh, SGI, Sun, PC.) </FONT><BR> + If you know the bug happens on all hardware platforms, choose 'All'. Otherwise, select + the platform that you found the bug on, or "Other" if your platform isn't + listed.</P> + <P><B>OS: On which Operating System (OS) did you find this bug?</B> <FONT SIZE="2">(e.g. + Linux, Windows NT, Mac OS 8.5.)</FONT><BR> + If you know the bug happens on all OSs, choose 'All'. Otherwise, select the OS that + you found the bug on, or "Other" if your OS isn't listed.</P> + + </BLOCKQUOTE> + <P><B><BR> + How important is the bug?</B> + + <BLOCKQUOTE> + <P><B>Severity: How damaging is the bug?</B><BR> + This item defaults to 'normal'. (To determine the most appropriate severity for a + particular bug, click on the Severity link for a full explanation of each choice, + from Critical to Enhancement.) + + </BLOCKQUOTE> + <P><B><BR> + Who will be following up on the bug?</B> + + <BLOCKQUOTE> + <P><B>Assigned To: Which engineer should be responsible for fixing this bug?</B><BR> + Bugzilla will automatically assign the bug to a default engineer upon submitting + a bug report; the text box exists to allow you to manually assign it to a different + engineer. (To see the list of default engineers for each component, click on the + Component link.)</P> + <P><B>Cc: Who else should receive e-mail updates on changes to this bug? </B><BR> + List the full e-mail addresses of other individuals who should receive an e-mail + update upon every change to the bug report. You can enter as many e-mail addresses + as you'd like; e-mail addresses must be separated by commas, with no spaces between + the addresses. + + </BLOCKQUOTE> + <P><B><BR> + What else can you tell the engineer about the bug?</B></P> + + <BLOCKQUOTE> + <P><B>URL: On what URL did you discover this bug?</B><BR> + If you encountered the bug on a particular URL, please provide it (or, them) here. + If you've isolated the bug to a specific HTML snippet, please also provide a URL + for that, too.</P> + + <P><B>Summary:</B> <B>How would you describe the bug, in approximately 60 or fewer + characters?</B><BR> + A good summary should <U>quickly and uniquely identify a bug report</U>. Otherwise, + developers cannot meaningfully query by bug summary, and will often fail to pay attention + to your bug report when reviewing a 10 page bug list.<BR> + <BR> + A summary of "PCMCIA install fails on Tosh Tecra 780DVD w/ 3c589C" is a + useful title. "Software fails" or "install problem" would be + examples of a bad title.</P> + + <P><BR> + <B>Description: What else can you tell the engineer about this bug? </B><BR> + Please provide as detailed of a problem diagnosis in this field as possible. <BR> + <BR> + Where applicable, using the following bug report template will help ensure that all + relevant information comes through: + + <BLOCKQUOTE> + <P><B>Overview Description:</B> More detailed expansion of summary. + + <BLOCKQUOTE> + <PRE><FONT SIZE="2">Drag-selecting any page crashes Mac builds in NSGetFactory</FONT></PRE> + + </BLOCKQUOTE> + <P><B>Steps to Reproduce: </B>The minimal set of steps necessary to trigger the bug. + Include any special setup steps. + + <BLOCKQUOTE> + <PRE><FONT SIZE="2">1) View any web page. (I used the default sample page, + resource:/res/samples/test0.html) +2) Drag-select the page. (Specifically, while holding down the + mouse button, drag the mouse pointer downwards from any point in + the browser's content region to the bottom of the browser's + content region.)</FONT></PRE> + + </BLOCKQUOTE> + <P><B>Actual Results:</B> What the application did after performing the above steps. + + <BLOCKQUOTE> + <PRE><FONT SIZE="2">The application crashed. Stack crawl appended below from MacsBug.</FONT></PRE> + + </BLOCKQUOTE> + <P><B>Expected Results:</B> What the application should have done, were the bug not + present. + + <BLOCKQUOTE> + <PRE><FONT SIZE="2">The window should scroll downwards. Scrolled content should +be selected. (Or, at least, the application should not crash.)</FONT></PRE> + + </BLOCKQUOTE> + <P><B>Build Date & Platform:</B> Date and platform of the build that you first + encountered the bug in. + + <BLOCKQUOTE> + <PRE><FONT SIZE="2">11/2/99 build on Mac OS (Checked Viewer & Apprunner)</FONT></PRE> + + </BLOCKQUOTE> + <P><B>Additional Builds and Platforms:</B> Whether or not the bug takes place on + other platforms or browsers. + + <BLOCKQUOTE> + <PRE><FONT SIZE="2"> - Occurs On + Seamonkey (11/2/99 build on Windows NT 4.0) + + - Doesn't Occur On + Seamonkey (11/4/99 build on Red Hat Linux; feature not supported) + Internet Explorer 5.0 (RTM build on Windows NT 4.0) + Netscape Communicator 4.5 (RTM build on Mac OS)</FONT> +</PRE> + + </BLOCKQUOTE> + <P><B>Additional Information:</B> Any other debugging information. For crashing bugs: + + <UL> + <LI><B>Win32:</B> if you receive a Dr. Watson error, please note the type of the + crash, and the module that the application crashed in. (e.g. access violation in + apprunner.exe) + <LI><B>Mac OS:</B> if you're running MacsBug, please provide the results of a <B><TT>how</TT></B> + and an <B><TT>sc</TT></B>. + <LI><B>Unix: </B>please provide a minimized stack trace, which can be generated by + typing <B><TT>gdb apprunner core</TT></B> into a shell prompt. + </UL> + + + <BLOCKQUOTE> + <P> + <PRE><FONT SIZE="2">*** MACSBUG STACK CRAWL OF CRASH (Mac OS) + +Calling chain using A6/R1 links + Back chain ISA Caller + 00000000 PPC 0BA85E74 + 03AEFD80 PPC 0B742248 + 03AEFD30 PPC 0B50FDDC NSGetFactory+027FC +PowerPC unmapped memory exception at 0B512BD0 NSGetFactory+055F0</FONT></PRE> + + </BLOCKQUOTE> + + </BLOCKQUOTE> + + </BLOCKQUOTE> + <P>You're done! <BR> + <BR> + After double-checking your entries for any possible errors, press the "Commit" + button, and your bug report will now be in the Bugzilla database.<BR> + <I><BR> + <BR> + </I><FONT SIZE="2">(Thanks to Claudius Gayle, Peter Mock, Chris Pratt, Tom Schutter, + and Chris Yeh for contributing to this document. Constructive </FONT><A HREF="mailto:eli@prometheus-music.com"><FONT + SIZE="2">suggestions</FONT></A><FONT SIZE="2"> welcome.)</FONT> + </BLOCKQUOTE> + + +</BODY> + +</HTML> diff --git a/changepassword.cgi b/changepassword.cgi index c07d3adc93856670509bbb69d455e9f888057f00..873ababda908af54c64ae449722de00b9e17b53d 100755 --- a/changepassword.cgi +++ b/changepassword.cgi @@ -20,130 +20,18 @@ # # Contributor(s): Terry Weissman <terry@mozilla.org> -require "CGI.pl"; - -confirm_login(); - -print "Content-type: text/html\n\n"; - -if (! defined $::FORM{'pwd1'}) { - PutHeader("Preferences", "Change your password and<br>other preferences", - $::COOKIE{'Bugzilla_login'}); - - my $qacontactpart = ""; - if (Param('useqacontact')) { - $qacontactpart = ", the current QA Contact"; - } - my $loginname = SqlQuote($::COOKIE{'Bugzilla_login'}); - SendSQL("select emailnotification,realname from profiles where login_name = " . - $loginname); - my ($emailnotification, $realname) = (FetchSQLData()); - $realname = value_quote($realname); - print qq{ -<form method=post> -<hr> -<table> -<tr> -<td align=right>Please enter the new password for <b>$::COOKIE{'Bugzilla_login'}</b>:</td> -<td><input type=password name="pwd1"></td> -</tr> -<tr> -<td align=right>Re-enter your new password:</td> -<td><input type=password name="pwd2"></td> -</tr> -<tr> -<td align=right>Your real name (optional):</td> -<td><input size=35 name=realname value="$realname"></td> -</tr> -</table> -<hr> -<table> -<tr> -<td align=right>Bugzilla will send out email notification of changed bugs to -the current owner, the submitter of the bug$qacontactpart, and anyone on the -CC list. However, you can suppress some of those email notifications. -On which of these bugs would you like email notification of changes?</td> -<td><SELECT NAME="emailnotification"> -}; - foreach my $i (["ExcludeSelfChanges", "All qualifying bugs except those which I change"], - ["CConly", "Only those bugs which I am listed on the CC line"], - ["All", "All qualifying bugs"]) { - my ($tag, $desc) = (@$i); - my $selectpart = ""; - if ($tag eq $emailnotification) { - $selectpart = " SELECTED"; - } - print qq{<OPTION$selectpart VALUE="$tag">$desc\n}; - } - print " -</SELECT> -</td> -</tr> -</table> -<hr> -<input type=submit value=Submit> -</form> -<hr> -<a href=\"showvotes.cgi\">Review your votes</a> -<hr> -"; - navigation_header(); - exit; -} - -if ($::FORM{'pwd1'} ne $::FORM{'pwd2'}) { - print "<H1>Try again.</H1> -The two passwords you entered did not match. Please click <b>Back</b> and try again.\n"; - exit; -} - - -my $pwd = $::FORM{'pwd1'}; - - -sub x { - my $sc="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./"; - return substr($sc, int (rand () * 100000) % (length ($sc) + 1), 1); -} - -if ($pwd ne "") { - if ($pwd !~ /^[a-zA-Z0-9-_]*$/ || length($pwd) < 3 || length($pwd) > 15) { - print "<H1>Sorry; we're picky.</H1> -Please choose a password that is between 3 and 15 characters long, and that -contains only numbers, letters, hyphens, or underlines. -<p> -Please click <b>Back</b> and try again.\n"; - exit; - } - - -# Generate a random salt. - - my $salt = x() . x(); - - my $encrypted = crypt($pwd, $salt); - - SendSQL("update profiles set password='$pwd',cryptpassword='$encrypted' where login_name=" . - SqlQuote($::COOKIE{'Bugzilla_login'})); - - SendSQL("update logincookies set cryptpassword = '$encrypted' where cookie = $::COOKIE{'Bugzilla_logincookie'}"); -} - - -SendSQL("update profiles set emailnotification='$::FORM{'emailnotification'}' where login_name = " . - SqlQuote($::COOKIE{'Bugzilla_login'})); - -my $newrealname = $::FORM{'realname'}; - -if ($newrealname ne "") { - $newrealname = SqlQuote($newrealname); - SendSQL("update profiles set realname=$newrealname where login_name = " . - SqlQuote($::COOKIE{'Bugzilla_login'})); +print q{Content-type: text/html + +<HTML> +<HEAD> +<META HTTP-EQUIV="Refresh" + CONTENT="0; URL=userprefs.cgi"> +</HEAD> +<BODY> +This URL is obsolete. Forwarding you to the correct one. +<P> +Going to <A HREF="userprefs.cgi">userprefs.cgi</A> +<BR> +</BODY> +</HTML> } - -PutHeader("Preferences updated."); -print " -Your preferences have been updated. -<p>"; -navigation_header(); - diff --git a/checksetup.pl b/checksetup.pl index 7b02ec0d5a82669ba9bcca91f67ccebbb87fb022..0b76a8fdccffd60463a1cdf90430ef935ba44040 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 @@ -102,11 +103,19 @@ use strict; use vars qw( $webservergroup - $db_host $db_port $db_name $db_user + $db_host $db_port $db_name $db_user $db_pass $db_check @severities @priorities @opsys @platforms ); +# Trim whitespace from front and back. + +sub trim { + ($_) = (@_); + s/^\s+//g; + s/\s+$//g; + return $_; +} @@ -154,11 +163,11 @@ $charts++ if eval "require GD"; $charts++ if eval "require Chart::Base"; if ($charts != 2) { print "If you you want to see graphical bug dependency charts, you may install\n", - "the optional libgd and the Perl modules GD and Chart::Base, e.g. by\n", + "the optional libgd and the Perl modules GD-1.19 and Chart::Base-0.99b, e.g. by\n", "running (as root)\n\n", " perl -MCPAN -eshell\n", - " install GD\n", - " install Chart::Base\n"; + " install LDS/GD-1.19.tar.gz\n", + " install N/NI/NINJAZ/Chart-0.99b.tar.gz"; } @@ -172,18 +181,19 @@ if ($charts != 2) { # # This is quite tricky. But fun! # -# First we read the file 'localconfig'. And then we check if the variables -# we need to be defined are defined. If not, localconfig will be amended by -# the new settings and the user informed to check this. The program then -# stops. +# First we read the file 'localconfig'. Then we check if the variables we +# need are defined. If not, localconfig will be amended by the new settings +# and the user informed to check this. The program then stops. # # Why do it this way around? # # Assume we will enhance Bugzilla and eventually more local configuration # stuff arises on the horizon. # -# But the file 'localconfig' is not in the Bugzilla CVS or tarfile. It should -# not be there so that we never overwrite user's local setups accidentally. +# But the file 'localconfig' is not in the Bugzilla CVS or tarfile. You +# know, we never want to overwrite your own version of 'localconfig', so +# we can't put it into the CVS/tarfile, can we? +# # Now, we need a new variable. We simply add the necessary stuff to checksetup. # The user get's the new version of Bugzilla from the CVS, runs checksetup # and checksetup finds out "Oh, there is something new". Then it adds some @@ -242,9 +252,24 @@ $db_port = 3306; # which port to use $db_name = "bugs"; # name of the MySQL database $db_user = "bugs"; # user to attach to the MySQL database '); +LocalVar('$db_pass', ' +# +# Some people actually use passwords with their MySQL database ... +# +$db_pass = ""; +'); +LocalVar('$db_check', ' +# +# Should checksetup.pl try to check if your MySQL setup is correct? +# (with some combinations of MySQL/Msql-mysql/Perl/moonphase this doesn\'t work) +# +$db_check = 1; +'); + + LocalVar('@severities', ' # # Which bug and feature-request severities do you want? @@ -286,6 +311,7 @@ LocalVar('@opsys', ' "Windows 3.1", "Windows 95", "Windows 98", + "Windows 2000", "Windows NT", "Mac System 7", "Mac System 7.5", @@ -293,6 +319,7 @@ LocalVar('@opsys', ' "Mac System 8.0", "Mac System 8.5", "Mac System 8.6", + "Mac System 9.0", "AIX", "BSDI", "HP-UX", @@ -332,11 +359,11 @@ LocalVar('@platforms', ' if ($newstuff ne "") { - print "This version of Bugzilla contains some variables that you may \n", + print "\nThis version of Bugzilla contains some variables that you may \n", "to change and adapt to your local settings. Please edit the file\n", - "'localconfig' and return checksetup.pl\n\n", + "'localconfig' and rerun checksetup.pl\n\n", "The following variables are new to localconfig since you last ran\n", - "checksetup.pl: $newstuff\n"; + "checksetup.pl: $newstuff\n\n"; exit; } @@ -400,7 +427,8 @@ if ($webservergroup) { 'processmail', 'whineatnews.pl', 'collectstats.pl', - 'checksetup.pl'; + 'checksetup.pl', + 'syncshadowdb'; chmod 0770, 'data', 'shadow'; chmod 0666, glob('data/*'); @@ -422,7 +450,6 @@ if ($webservergroup) { # the fact that we use MySQL and not, say, PostgreSQL. my $db_base = 'mysql'; -my $db_pass = ''; # Password to attach to the MySQL database use DBI; @@ -430,15 +457,24 @@ use DBI; my $drh = DBI->install_driver($db_base) or die "Can't connect to the $db_base. Is the database installed and up and running?\n"; -# Do we have the database itself? -my @databases = $drh->func($db_host, $db_port, '_ListDBs'); -unless (grep /^$db_name$/, @databases) { +if ($db_check) { + # Do we have the database itself? + my @databases = $drh->func($db_host, $db_port, '_ListDBs'); + unless (grep /^$db_name$/, @databases) { print "Creating database $db_name ...\n"; $drh->func('createdb', $db_name, 'admin') - or die "The '$db_name' database does not exist. I tried to create the database,\n", - "but that didn't work, probably because of access rigths. Read the README\n", - "file and the documentation of $db_base to make sure that everything is\n", - "set up correctly.\n"; + or die <<"EOF" + +The '$db_name' database is not accessible. This might have several reasons: + +* MySQL is not running. +* MySQL is running, but the rights are not set correct. Go and read the + README file of Bugzilla and all parts of the MySQL documentation. +* There is an subtle problem with Perl, DBI, DBD::mysql and MySQL. Make + sure all settings in 'localconfig' are correct. If all else fails, set + '\$db_check' to zero.\n +EOF + } } # now get a handle to the database: @@ -483,13 +519,13 @@ $table{bugs_activity} = 'bug_id mediumint not null, who mediumint not null, bug_when datetime not null, - field varchar(64) not null, + fieldid mediumint not null, oldvalue tinytext, newvalue tinytext, index (bug_id), index (bug_when), - index (field)'; + index (fieldid)'; $table{attachments} = @@ -513,11 +549,10 @@ $table{bugs} = assigned_to mediumint not null, # This is a comment. bug_file_loc text, bug_severity enum($severities) not null, - bug_status enum("NEW", "ASSIGNED", "REOPENED", "RESOLVED", "VERIFIED", "CLOSED") not null, + bug_status enum("UNCONFIRMED", "NEW", "ASSIGNED", "REOPENED", "RESOLVED", "VERIFIED", "CLOSED") not null, creation_ts datetime not null, delta_ts timestamp, short_desc mediumtext, - long_desc mediumtext, op_sys enum($opsys) not null, priority enum($priorities) not null, product varchar(64) not null, @@ -526,10 +561,15 @@ $table{bugs} = version varchar(16) not null, component varchar(50) not null, resolution enum("", "FIXED", "INVALID", "WONTFIX", "LATER", "REMIND", "DUPLICATE", "WORKSFORME") not null, - target_milestone varchar(20) not null, + target_milestone varchar(20) not null default "---", qa_contact mediumint not null, status_whiteboard mediumtext not null, votes mediumint not null, + keywords mediumtext not null, ' # Note: keywords field is only a cache; + # the real data comes from the keywords table. + . ' + lastdiffed datetime not null, + everconfirmed tinyint not null, index (assigned_to), index (creation_ts), @@ -552,8 +592,25 @@ $table{cc} = 'bug_id mediumint not null, who mediumint not null, + index(who), + unique(bug_id,who)'; + +$table{watch} = + 'watcher mediumint not null, + watched mediumint not null, + + index(watched), + unique(watcher,watched)'; + + +$table{longdescs} = + 'bug_id mediumint not null, + who mediumint not null, + bug_when datetime not null, + thetext mediumtext, + index(bug_id), - index(who)'; + index(bug_when)'; $table{components} = @@ -609,7 +666,11 @@ $table{products} = description mediumtext, milestoneurl tinytext not null, disallownew tinyint not null, - votesperuser smallint not null'; + votesperuser smallint not null, + maxvotesperbug smallint not null default 10000, + votestoconfirm smallint not null, + defaultmilestone varchar(20) not null default "---" +'; $table{profiles} = @@ -620,13 +681,61 @@ $table{profiles} = realname varchar(255), groupset bigint not null, emailnotification enum("ExcludeSelfChanges", "CConly", "All") not null default "ExcludeSelfChanges", + disabledtext mediumtext not null, + newemailtech tinyint not null, + mybugslink tinyint not null default 1, + blessgroupset bigint not null, + + + unique(login_name)'; - index(login_name)'; +$table{profiles_activity} = + 'userid mediumint not null, + who mediumint not null, + profiles_when datetime not null, + fieldid mediumint not null, + oldvalue tinytext, + newvalue tinytext, + + index (userid), + index (profiles_when), + index (fieldid)'; + + +$table{namedqueries} = + 'userid mediumint not null, + name varchar(64) not null, + watchfordiffs tinyint not null, + linkinfooter tinyint not null, + query mediumtext not null, + + unique(userid, name), + index(watchfordiffs)'; + +# This isn't quite cooked yet... +# +# $table{diffprefs} = +# 'userid mediumint not null, +# fieldid mediumint not null, +# mailhead tinyint not null, +# maildiffs tinyint not null, +# +# index(userid)'; + +$table{fielddefs} = + 'fieldid mediumint not null auto_increment primary key, + name varchar(64) not null, + description mediumtext not null, + mailhead tinyint not null default 0, + sortkey smallint not null, + + unique(name), + index(sortkey)'; $table{versions} = 'value tinytext, - program varchar(64)'; + program varchar(64) not null'; $table{votes} = @@ -637,7 +746,34 @@ $table{votes} = index(who), index(bug_id)'; +$table{keywords} = + 'bug_id mediumint not null, + keywordid smallint not null, + + index(keywordid), + unique(bug_id,keywordid)'; + +$table{keyworddefs} = + 'id smallint not null primary key, + name varchar(64) not null, + description mediumtext, + + unique(name)'; + + +$table{milestones} = + 'value varchar(20) not null, + product varchar(64) not null, + sortkey smallint not null, + unique (product, value)'; +$table{shadowlog} = + 'id int not null auto_increment primary key, + ts timestamp, + reflected tinyint not null, + command mediumtext not null, + + index(reflected)'; @@ -683,22 +819,31 @@ while (my ($tabname, $fielddef) = each %table) { # Populate groups table ########################################################################### +sub GroupExists ($) +{ + my ($name) = @_; + my $sth = $dbh->prepare("SELECT name FROM groups WHERE name='$name'"); + $sth->execute; + if ($sth->rows) { + return 1; + } + return 0; +} + + # # This subroutine checks if a group exist. If not, it will be automatically # created with the next available bit set # -sub AddGroup ($$) -{ - my ($name, $desc) = @_; +sub AddGroup { + my ($name, $desc, $userregexp) = @_; + $userregexp ||= ""; - # does the group exist? - my $sth = $dbh->prepare("SELECT name FROM groups WHERE name='$name'"); - $sth->execute; - return if $sth->rows; + return if GroupExists($name); # get highest bit number - $sth = $dbh->prepare("SELECT bit FROM groups ORDER BY bit DESC"); + my $sth = $dbh->prepare("SELECT bit FROM groups ORDER BY bit DESC"); $sth->execute; my @row = $sth->fetchrow_array; @@ -715,18 +860,30 @@ sub AddGroup ($$) $sth = $dbh->prepare('INSERT INTO groups (bit, name, description, userregexp) VALUES (?, ?, ?, ?)'); - $sth->execute($bit, $name, $desc, ""); + $sth->execute($bit, $name, $desc, $userregexp); + return $bit; } # -# BugZilla uses --GROUPS-- to assign various rights to it's users. +# BugZilla uses --GROUPS-- to assign various rights to its users. # AddGroup 'tweakparams', 'Can tweak operating parameters'; -AddGroup 'editgroupmembers', 'Can put people in and out of groups that they are members of.'; +AddGroup 'editusers', 'Can edit or disable users'; AddGroup 'creategroups', 'Can create and destroy groups.'; AddGroup 'editcomponents', 'Can create, destroy, and edit components.'; +AddGroup 'editkeywords', 'Can create, destroy, and edit keywords.'; + +if (!GroupExists("editbugs")) { + my $id = AddGroup('editbugs', 'Can edit all aspects of any bug.', ".*"); + $dbh->do("UPDATE profiles SET groupset = groupset | $id"); +} + +if (!GroupExists("canconfirm")) { + my $id = AddGroup('canconfirm', 'Can confirm a bug.', ".*"); + $dbh->do("UPDATE profiles SET groupset = groupset | $id"); +} @@ -757,14 +914,69 @@ unless ($sth->rows) { ########################################################################### -# Detect changed local settings +# Populate the list of fields. ########################################################################### -# -# 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 -# table definition. -# +my $headernum = 1; + +sub AddFDef ($$$) { + my ($name, $description, $mailhead) = (@_); + + $name = $dbh->quote($name); + $description = $dbh->quote($description); + + my $sth = $dbh->prepare("SELECT fieldid FROM fielddefs " . + "WHERE name = $name"); + $sth->execute(); + my ($fieldid) = ($sth->fetchrow_array()); + if (!$fieldid) { + $fieldid = 'NULL'; + } + + $dbh->do("REPLACE INTO fielddefs " . + "(fieldid, name, description, mailhead, sortkey) VALUES " . + "($fieldid, $name, $description, $mailhead, $headernum)"); + $headernum++; +} + + +AddFDef("bug_id", "Bug \#", 1); +AddFDef("short_desc", "Summary", 1); +AddFDef("product", "Product", 1); +AddFDef("version", "Version", 1); +AddFDef("rep_platform", "Platform", 1); +AddFDef("bug_file_loc", "URL", 1); +AddFDef("op_sys", "OS/Version", 1); +AddFDef("bug_status", "Status", 1); +AddFDef("status_whiteboard", "Status Whiteboard", 1); +AddFDef("keywords", "Keywords", 1); +AddFDef("resolution", "Resolution", 1); +AddFDef("bug_severity", "Severity", 1); +AddFDef("priority", "Priority", 1); +AddFDef("component", "Component", 1); +AddFDef("assigned_to", "AssignedTo", 1); +AddFDef("reporter", "ReportedBy", 1); +AddFDef("votes", "Votes", 0); +AddFDef("qa_contact", "QAContact", 0); +AddFDef("cc", "CC", 0); +AddFDef("dependson", "BugsThisDependsOn", 0); +AddFDef("blocked", "OtherBugsDependingOnThis", 0); +AddFDef("attachments.description", "Attachment description", 0); +AddFDef("attachments.thedata", "Attachment data", 0); +AddFDef("attachments.mimetype", "Attachment mime type", 0); +AddFDef("attachments.ispatch", "Attachment is patch", 0); +AddFDef("target_milestone", "Target Milestone", 0); +AddFDef("delta_ts", "Last changed date", 0); +AddFDef("(to_days(now()) - to_days(bugs.delta_ts))", "Days since bug changed", + 0); +AddFDef("longdesc", "Comment", 0); + + + + +########################################################################### +# Detect changed local settings +########################################################################### sub GetFieldDef ($$) { @@ -778,6 +990,67 @@ sub GetFieldDef ($$) } } +sub GetIndexDef ($$) +{ + my ($table, $field) = @_; + my $sth = $dbh->prepare("SHOW INDEX FROM $table"); + $sth->execute; + + while (my $ref = $sth->fetchrow_arrayref) { + next if $$ref[2] ne $field; + return $ref; + } +} + +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 +# table definition. +# + sub CheckEnumField ($$@) { my ($table, $field, @against) = @_; @@ -852,8 +1125,15 @@ sub ChangeFieldType ($$$) my $ref = GetFieldDef($table, $field); #print "0: $$ref[0] 1: $$ref[1] 2: $$ref[2] 3: $$ref[3] 4: $$ref[4]\n"; - if ($$ref[1] ne $newtype) { + my $oldtype = $ref->[1]; + if ($ref->[4]) { + $oldtype .= qq{ default "$ref->[4]"}; + } + + if ($oldtype ne $newtype) { print "Updating field type $field in table $table ...\n"; + print "old: $oldtype\n"; + print "new: $newtype\n"; $newtype .= " NOT NULL" if $$ref[3]; $dbh->do("ALTER TABLE $table CHANGE $field @@ -904,8 +1184,12 @@ sub DropField ($$) } +my $regenerateshadow = 0; + + -# 5/12/99 Added a pref to control how much email you get. This needs a new + +# 1999-05-12 Added a pref to control how much email you get. This needs a new # column in the profiles table, so feed the following to mysql: AddField('profiles', 'emailnotification', 'enum("ExcludeSelfChanges", "CConly", @@ -913,7 +1197,7 @@ AddField('profiles', 'emailnotification', 'enum("ExcludeSelfChanges", "CConly", -# 6/22/99 Added an entry to the attachments table to record who the +# 1999-06-22 Added an entry to the attachments table to record who the # submitter was. Nothing uses this yet, but it still should be recorded. AddField('attachments', 'submitter_id', 'mediumint not null'); @@ -930,7 +1214,7 @@ AddField('attachments', 'submitter_id', 'mediumint not null'); -# 9/15/99 Apparently, newer alphas of MySQL won't allow you to have "when" +# 1999-9-15 Apparently, newer alphas of MySQL won't allow you to have "when" # as a column name. So, I have had to rename a column in the bugs_activity # table. @@ -938,11 +1222,13 @@ RenameField ('bugs_activity', 'when', 'bug_when'); -# 10/11/99 Restructured voting database to add a cached value in each bug +# 1999-10-11 Restructured voting database to add a cached value in each bug # recording how many total votes that bug has. While I'm at it, I removed # the unused "area" field from the bugs database. It is distressing to # realize that the bugs table has reached the maximum number of indices # allowed by MySQL (16), which may make future enhancements awkward. +# (P.S. All is not lost; it appears that the latest betas of MySQL support +# a new table format which will allow 32 indices.) DropField('bugs', 'area'); AddField('bugs', 'votes', 'mediumint not null, add index (votes)'); @@ -965,7 +1251,408 @@ ChangeFieldType ('components', 'program', 'varchar(64)'); ChangeFieldType ('products', 'product', 'varchar(64)'); ChangeFieldType ('versions', 'program', 'varchar(64)'); +# 2000-01-16 Added a "keywords" field to the bugs table, which +# contains a string copy of the entries of the keywords table for this +# bug. This is so that I can easily sort and display a keywords +# column in bug lists. + +if (!GetFieldDef('bugs', 'keywords')) { + AddField('bugs', 'keywords', 'mediumtext not null'); + + my @kwords; + print "Making sure 'keywords' field of table 'bugs' is empty ...\n"; + $dbh->do("UPDATE bugs SET delta_ts = delta_ts, keywords = '' " . + "WHERE keywords != ''"); + print "Repopulating 'keywords' field of table 'bugs' ...\n"; + my $sth = $dbh->prepare("SELECT keywords.bug_id, keyworddefs.name " . + "FROM keywords, keyworddefs " . + "WHERE keyworddefs.id = keywords.keywordid " . + "ORDER BY keywords.bug_id, keyworddefs.name"); + $sth->execute; + my @list; + my $bugid = 0; + my @row; + while (1) { + my ($b, $k) = ($sth->fetchrow_array()); + if (!defined $b || $b ne $bugid) { + if (@list) { + $dbh->do("UPDATE bugs SET delta_ts = delta_ts, keywords = " . + $dbh->quote(join(', ', @list)) . + " WHERE bug_id = $bugid"); + } + if (!$b) { + last; + } + $bugid = $b; + @list = (); + } + push(@list, $k); + } +} + + +# 2000-01-18 Added a "disabledtext" field to the profiles table. If not +# empty, then this account has been disabled, and this field is to contain +# text describing why. + +AddField('profiles', 'disabledtext', 'mediumtext not null'); + + + +# 2000-01-20 Added a new "longdescs" table, which is supposed to have all the +# long descriptions in it, replacing the old long_desc field in the bugs +# table. The below hideous code populates this new table with things from +# the old field, with ugly parsing and heuristics. + +sub WriteOneDesc { + my ($id, $who, $when, $buffer) = (@_); + $buffer = trim($buffer); + if ($buffer eq '') { + return; + } + $dbh->do("INSERT INTO longdescs (bug_id, who, bug_when, thetext) VALUES " . + "($id, $who, " . time2str("'%Y/%m/%d %H:%M:%S'", $when) . + ", " . $dbh->quote($buffer) . ")"); +} + + +if (GetFieldDef('bugs', 'long_desc')) { + eval("use Date::Parse"); + eval("use Date::Format"); + my $sth = $dbh->prepare("SELECT count(*) FROM bugs"); + $sth->execute(); + my ($total) = ($sth->fetchrow_array); + + print "Populating new long_desc table. This is slow. There are $total\n"; + print "bugs to process; a line of dots will be printed for each 50.\n\n"; + $| = 1; + + $dbh->do("LOCK TABLES bugs write, longdescs write, profiles write"); + + $dbh->do('DELETE FROM longdescs'); + + $sth = $dbh->prepare("SELECT bug_id, creation_ts, reporter, long_desc " . + "FROM bugs ORDER BY bug_id"); + $sth->execute(); + my $count = 0; + while (1) { + my ($id, $createtime, $reporterid, $desc) = ($sth->fetchrow_array()); + if (!$id) { + last; + } + print "."; + $count++; + if ($count % 10 == 0) { + print " "; + if ($count % 50 == 0) { + print "$count/$total (" . int($count * 100 / $total) . "%)\n"; + } + } + $desc =~ s/\r//g; + my $who = $reporterid; + my $when = str2time($createtime); + my $buffer = ""; + foreach my $line (split(/\n/, $desc)) { + $line =~ s/\s+$//g; # Trim trailing whitespace. + if ($line =~ /^------- Additional Comments From ([^\s]+)\s+(\d.+\d)\s+-------$/) { + my $name = $1; + my $date = str2time($2); + $date += 59; # Oy, what a hack. The creation time is + # accurate to the second. But we the long + # text only contains things accurate to the + # minute. And so, if someone makes a comment + # within a minute of the original bug creation, + # then the comment can come *before* the + # bug creation. So, we add 59 seconds to + # the time of all comments, so that they + # are always considered to have happened at + # the *end* of the given minute, not the + # beginning. + if ($date >= $when) { + WriteOneDesc($id, $who, $when, $buffer); + $buffer = ""; + $when = $date; + my $s2 = $dbh->prepare("SELECT userid FROM profiles " . + "WHERE login_name = " . + $dbh->quote($name)); + $s2->execute(); + ($who) = ($s2->fetchrow_array()); + if (!$who) { + # This username doesn't exist. Try a special + # netscape-only hack (sorry about that, but I don't + # think it will hurt any other installations). We + # have many entries in the bugsystem from an ancient + # world where the "@netscape.com" part of the loginname + # was omitted. So, look up the user again with that + # appended, and use it if it's there. + if ($name !~ /\@/) { + my $nsname = $name . "\@netscape.com"; + $s2 = + $dbh->prepare("SELECT userid FROM profiles " . + "WHERE login_name = " . + $dbh->quote($nsname)); + $s2->execute(); + ($who) = ($s2->fetchrow_array()); + } + } + + if (!$who) { + # This username doesn't exist. Maybe someone renamed + # him or something. Invent a new profile entry, + # disabled, just to represent him. + $dbh->do("INSERT INTO profiles " . + "(login_name, password, cryptpassword," . + " disabledtext) VALUES (" . + $dbh->quote($name) . + ", 'okthen', encrypt('okthen'), " . + "'Account created only to maintain database integrity')"); + $s2 = $dbh->prepare("SELECT LAST_INSERT_ID()"); + $s2->execute(); + ($who) = ($s2->fetchrow_array()); + } + next; + } else { +# print "\nDecided this line of bug $id has a date of " . +# time2str("'%Y/%m/%d %H:%M:%S'", $date) . +# "\nwhich is less than previous line:\n$line\n\n"; + } + + } + $buffer .= $line . "\n"; + } + WriteOneDesc($id, $who, $when, $buffer); + } + + + print "\n\n"; + + DropField('bugs', 'long_desc'); + + $dbh->do("UNLOCK TABLES"); + $regenerateshadow = 1; + +} + + +# 2000-01-18 Added a new table fielddefs that records information about the +# different fields we keep an activity log on. The bugs_activity table +# now has a pointer into that table instead of recording the name directly. + +if (GetFieldDef('bugs_activity', 'field')) { + AddField('bugs_activity', 'fieldid', + 'mediumint not null, ADD INDEX (fieldid)'); + print "Populating new fieldid field ...\n"; + + $dbh->do("LOCK TABLES bugs_activity WRITE, fielddefs WRITE"); + + my $sth = $dbh->prepare('SELECT DISTINCT field FROM bugs_activity'); + $sth->execute(); + my %ids; + while (my ($f) = ($sth->fetchrow_array())) { + my $q = $dbh->quote($f); + my $s2 = + $dbh->prepare("SELECT fieldid FROM fielddefs WHERE name = $q"); + $s2->execute(); + my ($id) = ($s2->fetchrow_array()); + if (!$id) { + $dbh->do("INSERT INTO fielddefs (name, description) VALUES " . + "($q, $q)"); + $s2 = $dbh->prepare("SELECT LAST_INSERT_ID()"); + $s2->execute(); + ($id) = ($s2->fetchrow_array()); + } + $dbh->do("UPDATE bugs_activity SET fieldid = $id WHERE field = $q"); + } + $dbh->do("UNLOCK TABLES"); + + DropField('bugs_activity', 'field'); +} + + +# 2000-01-18 New email-notification scheme uses a new field in the bug to +# record when email notifications were last sent about this bug. Also, +# added a user pref whether a user wants to use the brand new experimental +# stuff. + +if (!GetFieldDef('bugs', 'lastdiffed')) { + AddField('bugs', 'lastdiffed', 'datetime not null'); + $dbh->do('UPDATE bugs SET lastdiffed = now(), delta_ts = delta_ts'); +} + +AddField('profiles', 'newemailtech', 'tinyint not null'); + + +# 2000-01-22 The "login_name" field in the "profiles" table was not +# declared to be unique. Sure enough, somehow, I got 22 duplicated entries +# in my database. This code detects that, cleans up the duplicates, and +# then tweaks the table to declare the field to be unique. What a pain. + +if (GetIndexDef('profiles', 'login_name')->[1]) { + print "Searching for duplicate entries in the profiles table ...\n"; + while (1) { + # This code is weird in that it loops around and keeps doing this + # select again. That's because I'm paranoid about deleting entries + # out from under us in the profiles table. Things get weird if + # there are *three* or more entries for the same user... + $sth = $dbh->prepare("SELECT p1.userid, p2.userid, p1.login_name " . + "FROM profiles AS p1, profiles AS p2 " . + "WHERE p1.userid < p2.userid " . + "AND p1.login_name = p2.login_name " . + "ORDER BY p1.login_name"); + $sth->execute(); + my ($u1, $u2, $n) = ($sth->fetchrow_array); + if (!$u1) { + last; + } + print "Both $u1 & $u2 are ids for $n! Merging $u2 into $u1 ...\n"; + foreach my $i (["bugs", "reporter"], + ["bugs", "assigned_to"], + ["bugs", "qa_contact"], + ["attachments", "submitter_id"], + ["bugs_activity", "who"], + ["cc", "who"], + ["votes", "who"], + ["longdescs", "who"]) { + my ($table, $field) = (@$i); + print " Updating $table.$field ...\n"; + my $extra = ""; + if ($table eq "bugs") { + $extra = ", delta_ts = delta_ts"; + } + $dbh->do("UPDATE $table SET $field = $u1 $extra " . + "WHERE $field = $u2"); + } + $dbh->do("DELETE FROM profiles WHERE userid = $u2"); + } + print "OK, changing index type to prevent duplicates in the future ...\n"; + + $dbh->do("ALTER TABLE profiles DROP INDEX login_name"); + $dbh->do("ALTER TABLE profiles ADD UNIQUE (login_name)"); + +} + + +# 2000-01-24 Added a new field to let people control whether the "My +# bugs" link appears at the bottom of each page. Also can control +# whether each named query should show up there. + +AddField('profiles', 'mybugslink', 'tinyint not null default 1'); +AddField('namedqueries', 'linkinfooter', 'tinyint not null'); + +# 2000-02-12 Added a new state to bugs, UNCONFIRMED. Added ability to confirm +# a vote via bugs. Added user bits to control which users can confirm bugs +# by themselves, and which users can edit bugs without their names on them. +# Added a user field which controls which groups a user can put other users +# into. + +my @states = ("UNCONFIRMED", "NEW", "ASSIGNED", "REOPENED", "RESOLVED", + "VERIFIED", "CLOSED"); +CheckEnumField('bugs', 'bug_status', @states); +if (!GetFieldDef('bugs', 'everconfirmed')) { + AddField('bugs', 'everconfirmed', 'tinyint not null'); + $dbh->do("UPDATE bugs SET everconfirmed = 1, delta_ts = delta_ts"); +} +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 + +$sth = $dbh->prepare("SELECT count(*) from milestones"); +$sth->execute(); +if (!($sth->fetchrow_arrayref()->[0])) { + print "Replacing blank milestones...\n"; + $dbh->do("UPDATE bugs SET target_milestone = '---', delta_ts=delta_ts WHERE target_milestone = ' '"); + +# Populate milestone table with all exisiting values in database + $sth = $dbh->prepare("SELECT DISTINCT target_milestone, product FROM bugs"); + $sth->execute(); + + print "Populating milestones table...\n"; + + my $value; + my $product; + while(($value, $product) = $sth->fetchrow_array()) + { + # check if the value already exists + my $sortkey = substr($value, 1); + if ($sortkey !~ /^\d+$/) { + $sortkey = 0; + } else { + $sortkey *= 10; + } + $value = $dbh->quote($value); + $product = $dbh->quote($product); + my $s2 = $dbh->prepare("SELECT value FROM milestones WHERE value = $value AND product = $product"); + $s2->execute(); + + if(!$s2->fetchrow_array()) + { + $dbh->do("INSERT INTO milestones(value, product, sortkey) VALUES($value, $product, $sortkey)"); + } + } +} + +# 2000-03-22 Changed the default value for target_milestone to be "---" +# (which is still not quite correct, but much better than what it was +# doing), and made the size of the value field in the milestones table match +# the size of the target_milestone field in the bugs table. + +ChangeFieldType('bugs', 'target_milestone', + 'varchar(20) default "---"'); +ChangeFieldType('milestones', 'value', 'varchar(20)'); + + +# 2000-03-23 Added a defaultmilestone field to the products table, so that +# we know which milestone to initially assign bugs to. + +if (!GetFieldDef('products', 'defaultmilestone')) { + AddField('products', 'defaultmilestone', + 'varchar(20) not null default "---"'); + $sth = $dbh->prepare("SELECT product, defaultmilestone FROM products"); + $sth->execute(); + while (my ($product, $defaultmilestone) = $sth->fetchrow_array()) { + $product = $dbh->quote($product); + $defaultmilestone = $dbh->quote($defaultmilestone); + my $s2 = $dbh->prepare("SELECT value FROM milestones " . + "WHERE value = $defaultmilestone " . + "AND product = $product"); + $s2->execute(); + if (!$s2->fetchrow_array()) { + $dbh->do("INSERT INTO milestones(value, product) " . + "VALUES ($defaultmilestone, $product)"); + } + } +} + +# 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)"); + + $regenerateshadow=1; # cc fields no longer have spaces in them +} + +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 @@ -977,3 +1664,13 @@ ChangeFieldType ('versions', 'program', 'varchar(64)'); # AddField/DropField/ChangeFieldType/RenameField code above. This would then # be honored by everyone who updates his Bugzilla installation. # +# +# Final checks... +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"; +print "Reminder: Bugzilla now requires version 8.7 or later of sendmail.\n"; diff --git a/colchange.cgi b/colchange.cgi index 077b0bc29a8efd9baf43d5f94794c48444f5ae1c..2d145955bb65b05f9da902698d8bfd839409ef3e 100755 --- a/colchange.cgi +++ b/colchange.cgi @@ -30,6 +30,9 @@ print "Content-type: text/html\n"; # The master list not only says what fields are possible, but what order # they get displayed in. +ConnectToDatabase(); +GetVersionTable(); + my @masterlist = ("opendate", "changeddate", "severity", "priority", "platform", "owner", "reporter", "status", "resolution", "component", "product", "version", "project", "os", "votes"); @@ -43,6 +46,9 @@ if (Param("useqacontact")) { if (Param("usestatuswhiteboard")) { push(@masterlist, "status_whiteboard"); } +if (@::legal_keywords) { + push(@masterlist, "keywords"); +} push(@masterlist, ("summary", "summaryfull")); @@ -50,6 +56,7 @@ push(@masterlist, ("summary", "summaryfull")); my @collist; if (defined $::FORM{'rememberedquery'}) { + my $splitheader = 0; if (defined $::FORM{'resetit'}) { @collist = @::default_column_list; } else { @@ -58,9 +65,13 @@ if (defined $::FORM{'rememberedquery'}) { push @collist, $i; } } + if (exists $::FORM{'splitheader'}) { + $splitheader = $::FORM{'splitheader'}; + } } my $list = join(" ", @collist); print "Set-Cookie: COLUMNLIST=$list ; path=/ ; expires=Sun, 30-Jun-2029 00:00:00 GMT\n"; + print "Set-Cookie: SPLITHEADER=$::FORM{'splitheader'} ; path=/ ; expires=Sun, 30-Jun-2029 00:00:00 GMT\n"; print "Refresh: 0; URL=buglist.cgi?$::FORM{'rememberedquery'}\n"; print "\n"; print "<TITLE>What a hack.</TITLE>\n"; @@ -75,6 +86,11 @@ if (defined $::COOKIE{'COLUMNLIST'}) { @collist = @::default_column_list; } +my $splitheader = 0; +if ($::COOKIE{'SPLITHEADER'}) { + $splitheader = 1; +} + my %desc; foreach my $i (@masterlist) { @@ -88,7 +104,7 @@ $desc{'summaryfull'} = "Full Summary"; print "\n"; PutHeader ("Change columns"); print "Check which columns you wish to appear on the list, and then click\n"; -print "on submit.\n"; +print "on submit. (Cookies are required.)\n"; print "<p>\n"; print "<FORM ACTION=colchange.cgi>\n"; print "<INPUT TYPE=HIDDEN NAME=rememberedquery VALUE=$::buffer>\n"; @@ -103,6 +119,12 @@ foreach my $i (@masterlist) { print "<INPUT TYPE=checkbox NAME=column_$i $c>$desc{$i}<br>\n"; } print "<P>\n"; +print BuildPulldown("splitheader", + [["0", "Normal headers (prettier)"], + ["1", "Stagger headers (often makes list more compact)"]], + $splitheader); +print "<P>\n"; + print "<INPUT TYPE=\"submit\" VALUE=\"Submit\">\n"; print "</FORM>\n"; print "<FORM ACTION=colchange.cgi>\n"; @@ -110,3 +132,4 @@ print "<INPUT TYPE=HIDDEN NAME=rememberedquery VALUE=$::buffer>\n"; print "<INPUT TYPE=HIDDEN NAME=resetit VALUE=1>\n"; print "<INPUT TYPE=\"submit\" VALUE=\"Reset to Bugzilla default\">\n"; print "</FORM>\n"; +PutFooter(); diff --git a/confirmhelp.html b/confirmhelp.html new file mode 100644 index 0000000000000000000000000000000000000000..5c8e64434142af9a8ce2bbe3efd897722f018374 --- /dev/null +++ b/confirmhelp.html @@ -0,0 +1,154 @@ +<HTML><head> + +<!-- + 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): Terry Weissman <terry@mozilla.org> +--> + +<title>Understanding the UNCONFIRMED state, and other recent changes</title> +</head> + +<body> +<h1>Understanding the UNCONFIRMED state, and other recent changes</h1> + +[This document is aimed primarily at people who have used Bugzilla +before the UNCONFIRMED state was implemented. It might be helpful for +newer users as well.] + +<p> + +New bugs in some products will now show up in a new state, +UNCONFIRMED. This means that we have nobody has confirmed that the +bug is real. Very busy engineers will probably generally ignore +UNCONFIRMED that have been assigned to them, until they have been +confirmed in one way or another. (Engineers with more time will +hopefully glance over their UNCONFIRMED bugs regularly.) + +<p> + +The <a href="bug_status.html">page describing bug fields</a> has been +updated to include UNCONFIRMED. +<p> + +There are two basic ways that a bug can become confirmed (and enter +the NEW) state. + +<ul> + <li> A user with the appropriate permissions (see below for more on + permissions) decides that the bug is a valid one, and confirms + it. We hope to gather a small army of responsible volunteers + to regularly go through bugs for us. + <li> The bug gathers a certain number of votes. <b>Any</b> valid + Bugzilla user may vote for bugs (each user gets a certain + number of bugs); any UNCONFIRMED bug which enough bugs becomes + automatically confirmed, and enters the NEW state. +</ul> + +One implication of this is that it is worth your time to search the +bug system for duplicates of your bug to vote on them, before +submitting your own bug. If we can spread around knowledge of this +fact, it ought to help cut down the number of duplicate bugs in the +system. + +<h2>Permissions.</h2> + +Users now have a certain set of permissions. To see your permissions, +check out the +<a href="userprefs.cgi?bank=permissions">user preferences</a> page. + +<p> + +If you have the "Can confirm a bug" permission, then you will be able +to move UNCONFIRMED bugs into the NEW state. + +<p> + +If you have the "Can edit all aspects of any bug" permission, then you +can tweak anything about any bug. If not, you may only edit those +bugs that you have submitted, or that you have assigned to you (or +qa-assigned to you). However, anyone may add a comment to any bug. + +<p> + +Some people (initially, the initial owners and initial qa-contacts for +components in the system) have the ability to give the above two +permissions to other people. So, if you really feel that you ought to +have one of these permissions, a good person to ask (via private +email, please!) is the person who is assigned a relevant bug. + +<h2>Other details.</h2> + +An initial stab was taken to decide who would be given which of the +above permissions. This was determined by some simple heurstics of +who was assigned bugs, and who the default owners of bugs were, and a +look at people who seem to have submitted several bugs that appear to +have been interesting and valid. Inevitably, we have failed to give +someone the permissions they deserve. Please don't take it +personally; just bear with us as we shake out the new system. + +<p> + + +People with one of the two bits above can easily confirm their own +bugs, so bugs they submit will actually start out in the NEW state. +They can override this when submitting a bug. + +<p> + +People can ACCEPT or RESOLVE a bug assigned to them, even if they +aren't allowed to confirm it. However, the system remembers, and if +the bug gets REOPENED or reassigned to someone else, it will revert +back to the UNCONFIRMED state. If the bug has ever been confirmed, +then REOPENing or reassigning will cause it to go to the NEW or +REOPENED state. + +<p> + +Note that only some products support the UNCONFIRMED state. In other +products, all new bugs will automatically start in the NEW state. + +<h2>Things still to be done.</h2> + +There probably ought to be a way to get a bug back into the +UNCONFIRMED state, but there isn't yet. + +<p> + +If a person has submitted several bugs that get confirmed, then this +is probably a person who understands the system well, and deserves the +"Can confirm a bug" permission. This kind of person should be +detected and promoted automatically. + +<p> + +There should also be a way to automatically promote people to get the +"Can edit all aspects of any bug" permission. + +<p> + +The "enter a new bug" page needs to be revamped with easy ways for new +people to educate themselves on the benefit of searching for a bug +like the one they're about to submit and voting on it, rather than +adding a new useless duplicate. + +<hr> +<!-- hhmts start --> +Last modified: Wed Feb 16 21:04:56 2000 +<!-- hhmts end --> +</body> </html> diff --git a/contrib/BugzillaEmail.pm b/contrib/BugzillaEmail.pm new file mode 100644 index 0000000000000000000000000000000000000000..aaba0f4e0a536b769ddff2c58e7118883f6e04fe --- /dev/null +++ b/contrib/BugzillaEmail.pm @@ -0,0 +1,79 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- + +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# This code is based on code found in bug_email.pl from the bugzilla +# email tracker. Initial contributors are :: +# Terry Weissman <terry@mozilla.org> +# Gregor Fischer <fischer@suse.de> +# Klaas Freitag <freitag@suse.de> +# Seth Landsman <seth@dworkin.net> + +# The purpose of this module is to abstract out a bunch of the code +# that is central to email interfaces to bugzilla and its database + +# Contributor : Seth Landsman <seth@dworkin.net> + +# Initial checkin : 03/15/00 (SML) +# findUser() function moved from bug_email.pl to here + +push @INC, "../."; # this script now lives in contrib + +require "globals.pl"; + +use diagnostics; +use strict; + +my $EMAIL_TRANSFORM_NONE = "email_transform_none"; +my $EMAIL_TRANSFORM_BASE_DOMAIN = "email_transform_base_domain"; +my $EMAIL_TRANSFORM_NAME_ONLY = "email_transform_name_only"; + +# change to do incoming email address fuzzy matching +my $email_transform = $EMAIL_TRANSFORM_NAME_ONLY; + +# findUser() +# This function takes an email address and returns the user email. +# matching is sloppy based on the $email_transform parameter +sub findUser($) { + my ($address) = @_; + # if $email_transform is $EMAIL_TRANSFORM_NONE, return the address, otherwise, return undef + if ($email_transform eq $EMAIL_TRANSFORM_NONE) { + my $stmt = "SELECT login_name FROM profiles WHERE profiles.login_name = \'$address\';"; + SendSQL($stmt); + my $found_address = FetchOneColumn(); + return $found_address; + } elsif ($email_transform eq $EMAIL_TRANSFORM_BASE_DOMAIN) { + my ($username) = ($address =~ /(.+)@/); + my $stmt = "SELECT login_name FROM profiles WHERE profiles.login_name RLIKE \'$username\';"; + SendSQL($stmt); + + my $domain; + my $found = undef; + my $found_address; + my $new_address = undef; + while ((!$found) && ($found_address = FetchOneColumn())) { + ($domain) = ($found_address =~ /.+@(.+)/); + if ($address =~ /$domain/) { + $found = 1; + $new_address = $found_address; + } + } + return $new_address; + } elsif ($email_transform eq $EMAIL_TRANSFORM_NAME_ONLY) { + my ($username) = ($address =~ /(.+)@/); + my $stmt = "SELECT login_name FROM profiles WHERE profiles.login_name RLIKE \'$username\';"; + SendSQL($stmt); + my $found_address = FetchOneColumn(); + return $found_address; + } +} + +1; diff --git a/contrib/CVS/Entries b/contrib/CVS/Entries index 8152b4812bece9a14ededd24919a91c93b9ba682..8d39e8518923082db04cfdb2734c2901071087d1 100644 --- a/contrib/CVS/Entries +++ b/contrib/CVS/Entries @@ -1,3 +1,9 @@ -/README/1.1/Tue Sep 21 20:11:56 1999// +/BugzillaEmail.pm/1.1/Wed Mar 15 22:29:44 2000// +/README/1.2/Tue Mar 7 17:36:38 2000// +/README.Mailif/1.3/Wed Mar 15 23:39:03 2000// +/bug_email.pl/1.6/Sat Mar 18 23:32:49 2000// +/bugmail_help.html/1.1/Tue Mar 7 17:36:48 2000// +/bugzilla.procmailrc/1.1/Wed Mar 15 23:39:09 2000// +/bugzilla_email_append.pl/1.2/Wed Mar 15 23:39:11 2000// /gnats2bz.pl/1.5/Thu Nov 18 17:29:58 1999// D diff --git a/contrib/CVS/Root b/contrib/CVS/Root index db0def341f4c0526aa7407d123007dbb03cee412..cdb6f4a0739a0dc53e628026726036377dec3637 100644 --- a/contrib/CVS/Root +++ b/contrib/CVS/Root @@ -1 +1 @@ -:pserver:terry%mozilla.org@cvs.mozilla.org:/cvsroot +:pserver:anonymous@cvs-mirror.mozilla.org:/cvsroot diff --git a/contrib/README b/contrib/README index 89ad2c58d0173164e5b3466801e4f8a60bc289f2..32ec834a9e5fba4dfbb5f76f39d755d5649e032c 100644 --- a/contrib/README +++ b/contrib/README @@ -8,3 +8,11 @@ This directory includes: gnats2bz.pl -- a perl script to help import bugs from a GNATS database into a Bugzilla database. Contributed by Tom Schutter <tom@platte.com> + + bug_email.pl -- a perl script that can receive email containing + bug reports (email-interface). Contributed by + Klaas Freitag <freitag@SuSE.de> + + README.Mailif -- Readme describing the mail interface. + bugmail_help.html -- User help page for the mail interface. + diff --git a/contrib/README.Mailif b/contrib/README.Mailif new file mode 100644 index 0000000000000000000000000000000000000000..8b66044388f2418272277abb424b427d6c4be1fa --- /dev/null +++ b/contrib/README.Mailif @@ -0,0 +1,80 @@ + +The Bugzilla Mail interface +=========================== + +(UPDATE 03/14/00 to better reflect reality by SML) + +The Bugzilla Mail interface allows to submit bugs to Bugzilla by email. + +The Mail Interface Contribution consists of three files: +README.Mailif - this readme. +bug_email.pl - the script +bugmail_help.html - a user help html site + +Installation: + +Next is to add a user who receives the bugmails, e. g. bugmail. Create a +mail account and a home directory for the user. + +The mailinterface script bug_email.pl needs to get the mail through stdin. +I use procmail for that, with the following line in the .procmailrc: + +BUGZILLA_HOME=/usr/local/httpd/htdocs/bugzilla +:0 c +|(cd $BUGZILLA_HOME/contrib; ./bug_email.pl) + +This defines the Bugzilla directory as the variable BUGZILLA_HOME and passes +all incoming mail to the script after cd'ing into the bugzilla home. + +In some cases, it is necessary to alter the headers of incoming email. The +additional line to procmail : + +:0 fhw +| formail -I "From " -a "From " + +fixes many problems. + +See bugzilla.procmailrc for a sample procmailrc that works for me (SML) and +also deals with bugzilla_email_append.pl + +Customation: + +There are some values inside the script which need to be customized for your +needs: + +1. In sub-routine Reply (search 'sub Reply': +there is the line + print MAIL "From: Bugzilla Mailinterface<yourmail\@here.com>\n"; + ^^^^^^^^^^^^^^^^^^^^ +Fill in your correct mail here. That will make it easy for people to reply +to the mail. + +2. check, if your sendmail resides in /usr/sbin/sendmail, change the path if neccessary. +Search the script after 'default' - you find some default-Settings for bug +reports, which are used, if the sender did not send a field for it. The defaults +should be checked and changed. + +Thats hopefully all, we will come up with any configuration file or something. + + +If your mail works, your script will insert mails from now on. + +The mailinterface supports two commandline switches: + +There are two command line switches : + +-t: Testmode + The mailinterface does not really insert the bug into the database, but + writes some debug output to stdout and writes the mail into the file + bug_email_test.log in the data-dir. + +-r: restricted mode + All lines before the first line with a keyword character are skipped. + In not restricted, default mode, these lines are added to the long + description of the bug. + + +02/2000 - Klaas Freitag, SuSE GmbH <freitag@suse.de> +03/2000 - Seth M. Landsman <seth@cs.brandeis.edu> + bug_email.pl now lives out of bugzilla/contrib + added line about formail diff --git a/contrib/bug_email.pl b/contrib/bug_email.pl new file mode 100755 index 0000000000000000000000000000000000000000..635ce93834096ca51f0490bcb2f9f60065908574 --- /dev/null +++ b/contrib/bug_email.pl @@ -0,0 +1,1294 @@ +#!/usr/bin/perl -w +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Bug Tracking System. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are +# Copyright (C) 1998 Netscape Communications Corporation. All +# Rights Reserved. +# Contributor(s): Terry Weissman <terry@mozilla.org> +# Gregor Fischer <fischer@suse.de> +# Klaas Freitag <freitag@suse.de> +# Seth Landsman <seth@dworkin.net> +############################################################### +# Bugzilla: Create a new bug via email +############################################################### +# The email needs to be feeded to this program on STDIN. +# This is usually done by having an entry like this in your +# .procmailrc: +# +# BUGZILLA_HOME=/usr/local/httpd/htdocs/bugzilla +# :0 c +# |(cd $BUGZILLA_HOME/contrib; ./bug_email.pl) +# +# +# Installation note: +# +# You need to work with bug_email.pl the MIME::Parser installed. +# +# $Id: bug_email.pl,v 1.6 2000/03/18 23:32:49 seth%cs.brandeis.edu Exp $ +############################################################### + +# 02/12/2000 (SML) +# - updates to work with most recent database changes to the bugs database +# - updated so that it works out of bugzilla/contrib +# - initial checkin into the mozilla CVS tree (yay) + +# 02/13/2000 (SML) +# - email transformation code. +# EMAIL_TRANSFORM_NONE does exact email matches +# EMAIL_TRANSFORM_NAME_ONLY matches on the username +# EMAIL_TRANSFORM_BASE_DOMAIN matches on the username and checks the domain of +# to see that the one in the database is a subset of the one in the sender address +# this is probably prone to false positives and probably needs more work. + +# 03/07/2000 (SML) +# - added in $DEFAULT_PRODUCT and $DEFAULT_COMPONENT. i.e., if $DEFAULT_PRODUCT = "PENDING", +# any email submitted bug will be entered with a product of PENDING, if no other product is +# specified in the email. + +# Next round of revisions : +# - querying a bug over email +# - appending a bug over email +# - keywords over email +# - use the globals.pl parameters functionality to edit and save this script's parameters +# - integrate some setup in the checksetup.pl script +# - gpg signatures for security + +use diagnostics; +use strict; +use MIME::Parser; + +push @INC, "../."; # this script now lives in contrib + +require "globals.pl"; +require "BugzillaEmail.pm"; + +my @mailerrors = (); # Buffer for Errors in the mail +my @mailwarnings = (); # Buffer for Warnings found in the mail +my $critical_err = 0; # Counter for critical errors - must be zero for success +my %Control; +my $Header = ""; +my @RequiredLabels = (); +my @AllowedLabels = (); +my $Body = ""; +my @attachments = (); + +my $product_valid = 0; +my $test = 0; +my $restricted = 0; +my $SenderShort; +my $Message_ID; + +# change to use default product / component functionality +my $DEFAULT_PRODUCT = "PENDING"; +my $DEFAULT_COMPONENT = "PENDING"; + +############################################################### +# storeAttachments +# +# in this sub, attachments found in the dump-sub will be written to +# the database. The info, which attachments need saving is stored +# in the global @attachments-list. +# The sub returns the number of stored attachments. +sub storeAttachments( $$ ) +{ + my ($bugid, $submitter_id ) = @_; + my $maxsize = 0; + my $data; + my $listref = \@attachments; + my $att_count = 0; + + $submitter_id ||= 0; + + foreach my $pairref ( @$listref ) { + my ($decoded_file, $mime, $on_disk, $description) = @$pairref; + + + # Size check - mysql has a maximum space for the data ? + $maxsize = 1047552; # should be queried by a system( "mysqld --help" );, + # but this seems not to be supported by all current mysql-versions + + # Read data file binary + if( $on_disk ) { + if( open( FILE, "$decoded_file" )) { + binmode FILE; + read FILE, $data, $maxsize; + close FILE; + $att_count ++; + } else { + print "Error while reading attachment $decoded_file!\n"; + next; + } + # print "unlinking data/mimedump-tmp/$decoded_file"; + # unlink "data/mimedump-tmp/$decoded_file"; + } else { + # data is in the scalar + $data = $decoded_file; + } + + + # Make SQL-String + my $sql = "insert into attachments (bug_id, creation_ts, description, mimetype, ispatch, filename, thedata, submitter_id) values ("; + $sql .= "$bugid, now(), " . SqlQuote( $description ) . ", "; + $sql .= SqlQuote( $mime ) . ", "; + $sql .= "0, "; + $sql .= SqlQuote( $decoded_file ) . ", "; + $sql .= SqlQuote( $data ) . ", "; + $sql .= "$submitter_id );"; + SendSQL( $sql ) unless( $test ); + } + + return( $att_count ); +} + + + +############################################################### +# Beautification +sub horLine( ) +{ + return( "-----------------------------------------------------------------------\n" ); +} + + +############################################################### +# Check if $Name is in $GroupName + +# This is no more CreateBugs group, so I'm using this routine to just determine if the user is +# in the database. Eventually, here should be a seperate routine or renamed, or something (SML) +sub CheckPermissions { + my ($GroupName, $Name) = @_; + +# SendSQL("select login_name from profiles,groups where groups.name='$GroupName' and profiles.groupset & groups.bit = groups.bit and profiles.login_name=\'$Name\'"); +# my $NewName = FetchOneColumn(); +# if ( $NewName eq $Name ) { +# return $Name; +# } else { +# return; +# } +# my $query = "SELECT login_name FROM profiles WHERE profiles.login_name=\'$Name\'"; +# SendSQL($query); +# my $check_name = FetchOneColumn(); +# if ($check_name eq $Name) { +# return $Name; +# } else { +# return; +# } + return findUser($Name); +} + +############################################################### +# Check if product is valid. +sub CheckProduct { + my $Product = shift; + + SendSQL("select product from products where product='$Product'"); + my $Result = FetchOneColumn(); + if (lc($Result) eq lc($Product)) { + return $Result; + } else { + return ""; + } +} + +############################################################### +# Check if component is valid for product. +sub CheckComponent { + my $Product = shift; + my $Component = shift; + + SendSQL("select value from components where program=" . SqlQuote($Product) . " and value=" . SqlQuote($Component) . ""); + my $Result = FetchOneColumn(); + if (lc($Result) eq lc($Component)) { + return $Result; + } else { + return ""; + } +} + +############################################################### +# Check if component is valid for product. +sub CheckVersion { + my $Product = shift; + my $Version = shift; + + SendSQL("select value from versions where program=" . SqlQuote($Product) . " and value=" . SqlQuote($Version) . ""); + my $Result = FetchOneColumn(); + if (lc($Result) eq lc($Version)) { + return $Result; + } else { + return ""; + } +} + +############################################################### +# Reply to a mail. +sub Reply( $$$$ ) { + my ($Sender, $MessageID, $Subject, $Text) = @_; + + + die "Cannot find sender-email-address" unless defined( $Sender ); + + if( $test ) { + open( MAIL, ">>data/bug_email_test.log" ); + } + else { + open( MAIL, "| /usr/sbin/sendmail -t" ); + } + + print MAIL "To: $Sender\n"; + print MAIL "From: Bugzilla Mailinterface<yourmail\@here.com>\n"; + print MAIL "Subject: $Subject\n"; + print MAIL "In-Reply-To: $MessageID\n" if ( defined( $MessageID )); + print MAIL "\n"; + print MAIL "$Text"; + close( MAIL ); + +} + + +############################################################### +# getEnumList +# Queries the Database for the table description and figures the +# enum-settings out - usefull for checking fields for enums like +# prios +sub getEnumList( $ ) +{ + my ($fieldname) = @_; + SendSQL( "describe bugs $fieldname" ); + my ($f, $type) = FetchSQLData(); + + # delete unneeded stuff + $type =~ s/enum\(|\)//g; + $type =~ s/\',//g; + + my @all_prios = split( /\'/, $type ); + return( @all_prios ); +} + +############################################################### +# CheckPriority +# Checks, if the priority setting is one of the enums defined +# in the data base +# Uses the global var. $Control{ 'priority' } +sub CheckPriority +{ + my $prio = ($Control{'priority'} ||= ""); + my @all_prios = getEnumList( "priority" ); + + if( (lsearch( \@all_prios, $prio ) == -1) || $prio eq "" ) { + # OK, Prio was not defined - create Answer + my $Text = "You sent wrong priority-setting, valid values are:" . + join( "\n\t", @all_prios ) . "\n\n"; + $Text .= "* The priority is set to the default value ". + SqlQuote( Param('defaultpriority')) . "\n"; + + BugMailError( 0, $Text ); + + # set default value from param-file + $Control{'priority'} = Param( 'defaultpriority' ); + } else { + # Nothing to do + } +} + +############################################################### +# CheckSeverity +# checks the bug_severity +sub CheckSeverity +{ + my $sever = ($Control{'bug_severity'} ||= "" ); + my @all_sever = getEnumList( "bug_severity" ); + + if( (lsearch( \@all_sever, $sever ) == -1) || $sever eq "" ) { + # OK, Prio was not defined - create Answer + my $Text = "You sent wrong bug_severity-setting, valid values are:" . + join( "\n\t", @all_sever ) . "\n\n"; + $Text .= "* The bug_severity is set to the default value ". + SqlQuote( "normal" ) . "\n"; + + BugMailError( 0, $Text ); + + # set default value from param-file + $Control{'bug_severity'} = "normal"; + } +} + +############################################################### +# CheckArea +# checks the area-field +sub CheckArea +{ + my $area = ($Control{'area'} ||= "" ); + my @all= getEnumList( "area" ); + + if( (lsearch( \@all, $area ) == -1) || $area eq "" ) { + # OK, Area was not defined - create Answer + my $Text = "You sent wrong area-setting, valid values are:" . + join( "\n\t", @all ) . "\n\n"; + $Text .= "* The area is set to the default value ". + SqlQuote( "BUILD" ) . "\n"; + + BugMailError( 0, $Text ); + + # set default value from param-file + $Control{'area'} = "BUILD"; + } +} + +############################################################### +# CheckPlatform +# checks the given Platform and corrects it +sub CheckPlatform +{ + my $platform = ($Control{'rep_platform'} ||= "" ); + my @all = getEnumList( "rep_platform" ); + + if( (lsearch( \@all, $platform ) == -1) || $platform eq "" ) { + # OK, Prio was not defined - create Answer + my $Text = "You sent wrong platform-setting, valid values are:" . + join( "\n\t", @all ) . "\n\n"; + $Text .= "* The rep_platform is set to the default value ". + SqlQuote( "All" ) . "\n"; + + BugMailError( 0, $Text ); + + # set default value from param-file + $Control{'rep_platform'} = "All"; + } +} + +############################################################### +# CheckSystem +# checks the given Op-Sys and corrects it +sub CheckSystem +{ + my $sys = ($Control{'op_sys'} ||= "" ); + my @all = getEnumList( "op_sys" ); + + if( (lsearch( \@all, $sys ) == -1) || $sys eq "" ) { + # OK, Prio was not defined - create Answer + my $Text = "You sent wrong OS-setting, valid values are:" . + join( "\n\t", @all ) . "\n\n"; + $Text .= "* The op_sys is set to the default value ". + SqlQuote( "Linux" ) . "\n"; + + BugMailError( 0, $Text ); + + # set default value from param-file + $Control{'op_sys'} = "Linux"; + } +} + + +############################################################### +# Fetches all lines of a query with a single column selected and +# returns it as an array +# +sub FetchAllSQLData( ) +{ + my @res = (); + + while( MoreSQLData() ){ + push( @res, FetchOneColumn() ); + } + return( @res ); +} + +############################################################### +# Error Handler for Errors in the mail +# +# This function can be called multiple within processing one mail and +# stores the errors found in the Mail. Errors are for example empty +# required tags, missing required tags and so on. +# +# The benefit is, that the mail users get a reply, where all mail errors +# are reported. The reply mail includes all messages what was wrong and +# the second mail the user sends can be ok, cause all his faults where +# reported. +# +# BugMailError takes two arguments: The first one is a flag, how heavy +# the error is: +# +# 0 - Its an error, but bugzilla can process the bug. The user should +# handle that as a warning. +# +# 1 - Its a real bug. Bugzilla cant store the bug. The mail has to be +# resent. +# +# 2 - Permission error: The user does not have the permission to send +# a bug. +# +# The second argument is a Text which describs the bug. +# +# +# # +sub BugMailError($ $ ) +{ + my ( $errflag, $text ) = @_; + + # On permission error, dont sent all other Errors back -> just quit ! + if( $errflag == 2 ) { # Permission-Error + Reply( $SenderShort, $Message_ID, "Bugzilla Error", "Permission denied.\n\n" . + "You do not have the permissions to create a new bug. Sorry.\n" ); + exit; + } + + + # Warnings - store for the reply mail + if( $errflag == 0 ) { + push( @mailwarnings, $text ); + } + + # Critical Error + if( $errflag == 1 ) { + $critical_err += 1; + push( @mailerrors, $text ); + } +} + +############################################################### +# getWarningText() +# +# getWarningText() returns a reply-ready Textline of all the +# Warnings in the Mail +sub getWarningText() +{ + my $anz = @mailwarnings; + + my $ret = <<END + +The Bugzilla Mail Interface found warnings (JFYI): + +END + ; + + # Handshake if no warnings at all + return( "\n\n Your mail was processed without Warnings !\n" ) if( $anz == 0 ); + + # build a text + $ret .= join( "\n ", @mailwarnings ); + return( horLine() . $ret ); +} + +sub getErrorText() +{ + my $anz = @mailerrors; + + my $ret = <<END + +************************** ERROR ************************** + +Your request to the Bugzilla mail interface could not be met +due to errors in the mail. We will find it ! + + +END + ; + return( "\n\n Your mail was processed without errors !\n") if( $anz == 0 ); + # build a text + $ret .= join( "\n ", @mailerrors ); + return( $ret ); +} + +############################################################### +# generateTemplate +# +# This functiuon generates a mail-Template with the +sub generateTemplate() +{ + my $w; + my $ret; + + # Required Labels + $ret =<<EOF + + +You may want to use this template to resend your mail. Please fill in the missing +keys. + +_____ snip _______________________________________________________________________ + +EOF + ; + foreach ( @RequiredLabels ) { + $w = ""; + $w = $Control{$_} if defined( $Control{ $_ } ); + $ret .= sprintf( " \@%-15s: %s\n", $_, $w ); + } + + $ret .= "\n"; + # Allowed Labels + foreach( @AllowedLabels ) { + next if( /reporter/ ); # Reporter is not a valid label + next if( /assigned_to/ ); # Assigned to is just a number + if( defined( $Control{ $_ } ) && lsearch( \@RequiredLabels, $_ ) == -1 ) { + $ret .= sprintf( " \@%-15s: %s\n", $_, $Control{ $_ } ); + } + } + + if( $Body eq "" ) { + $ret .= <<END + + < the bug-description follows here > + +_____ snip _______________________________________________________________________ + +END + ; } else { + $ret .= "\n" . $Body; + } + + return( $ret ); + +} +############################################################### +# groupBitToString( $ ) +# converts a given number back to the groupsetting-names +# This function accepts single numbers as added bits or +# Strings with +-Signs +sub groupBitToString( $ ) +{ + my ($bits) = @_; + my $type; + my @bitlist = (); + my $ret = ""; + + if( $bits =~ /^\d+$/ ) { # only numbers + $type = 1; + + } elsif( $bits =~ /^(\s*\d+\s*\+\s*)+/ ) { + $type = 2; + } else { + # Error: unknown format ! + $type = 0; + } + + $bits =~ s/\s*//g; + # + # Query for groupset-Information + SendSQL( "Select Bit,Name, Description from groups where isbuggroup=1" ); + my @line; + while( MoreSQLData() ){ + @line = FetchSQLData(); + + if( $type == 1 ) { + if( ((0+$bits) & (0+$line[0])) == 0+$line[0] ) { + $ret .= sprintf( "%s ", $line[1] ); + } + } elsif( $type == 2 ) { + if( $bits =~ /$line[0]/ ) { + $ret .= sprintf( "%s ", $line[1] ); + } + } + } + + return( $ret ); +} + + + +#------------------------------ +# +# dump_entity ENTITY, NAME +# +# Recursive routine for parsing a mime coded mail. +# One mail may contain more than one mime blocks, which need to be +# handled. Therefore, this function is called recursively. +# +# It gets the for bugzilla important information from the mailbody and +# stores them into the global attachment-list @attachments. The attachment-list +# is needed in storeAttachments. +# +sub dump_entity { + my ($entity, $name) = @_; + defined($name) or $name = "'anonymous'"; + my $IO; + + + # Output the body: + my @parts = $entity->parts; + if (@parts) { # multipart... + my $i; + foreach $i (0 .. $#parts) { # dump each part... + dump_entity($parts[$i], ("$name, part ".(1+$i))); + } + } else { # single part... + + # Get MIME type, and display accordingly... + my $msg_part = $entity->head->get( 'Content-Disposition' ); + + $msg_part ||= ""; + + my ($type, $subtype) = split('/', $entity->head->mime_type); + my $body = $entity->bodyhandle; + my ($data, $on_disk ); + + if( $msg_part =~ /^attachment/ ) { + # Attached File + my $des = $entity->head->get('Content-Description'); + $des ||= ""; + + if( defined( $body->path )) { # Data is on disk + $on_disk = 1; + $data = $body->path; + + } else { # Data is in core + $on_disk = 0; + $data = $body->as_string; + } + push ( @attachments, [ $data, $entity->head->mime_type, $on_disk, $des ] ); + } else { + # Real Message + if ($type =~ /^(text|message)$/) { # text: display it... + if ($IO = $body->open("r")) { + $Body .= $_ while (defined($_ = $IO->getline)); + $IO->close; + } else { # d'oh! + print "$0: couldn't find/open '$name': $!"; + } + } else { print "Oooops - no Body !\n"; } + } + } +} + +############################################################### +# sub extractControls +############################################################### +# +# This sub parses the message Body and filters the control-keys. +# Attention: Global hash Controls affected +# +sub extractControls( $ ) +{ + my ($body) = @_; + my $backbody = ""; + + my @lbody = split( /\n/, $body ); + + # In restricted mode, all lines before the first keyword + # are skipped. + if( $restricted ) { + while( $lbody[0] =~ /^\s*\@.*/ ){ shift( @lbody );} + } + + # Filtering for keys + foreach( @lbody ) { + if( /^\s*\@description/ ) { + s/\s*\@description//; + $backbody .= $_; + } elsif( /^\s*\@(.*?)(?:\s*=\s*|\s*:\s*|\s+)(.*?)\s*$/ ) { + $Control{lc($1)} = $2; + } else { + $backbody .= "$_" . "\n"; + } + } + + # thats it. + return( $backbody ); +} + +############################################################### +# Main starts here +############################################################### +# +# Commandline switches: +# -t: test mode - no DB-Inserts +foreach( @ARGV ) { + $restricted = 1 if ( /-r/ ); + $test = 1 if ( /-t/ ); +} + +# +# Parsing a mime-message +# +if( -t STDIN ) { +print STDERR <<END + Bugzilla Mail Interface + + This scripts reads a mail message through stdin and parses the message, + for to insert a bug to bugzilla. + + Options + -t: Testmode - No insert to the DB, but logfile + -r: restricted mode - all lines before the keys in the mail are skipped + +END + ; +exit; +} + + +# Create a new MIME parser: +my $parser = new MIME::Parser; + +# Create and set the output directory: +# FIXME: There should be a $BUGZILLA_HOME variable (SML) +(-d "../data/mimedump-tmp") or mkdir "../data/mimedump-tmp",0755 or die "mkdir: $!"; +(-w "../data/mimedump-tmp") or die "can't write to directory"; + +$parser->output_dir("../data/mimedump-tmp"); + +# Read the MIME message: +my $entity = $parser->read(\*STDIN) or die "couldn't parse MIME stream"; +$entity->remove_sig(10); # Removes the signature in the last 10 lines + +# Getting values from parsed mail +my $Sender = $entity->get( 'From' ); +$Sender ||= $entity->get( 'Reply-To' ); +$Message_ID = $entity->get( 'Message-Id' ); + +die (" *** Cant find Sender-adress in sent mail ! ***\n" ) unless defined( $Sender ); +chomp( $Sender ); +chomp( $Message_ID ); + +ConnectToDatabase(); + +$SenderShort = $Sender; +$SenderShort =~ s/^.*?([a-zA-Z0-9_.-]+?\@[a-zA-Z0-9_.-]+\.[a-zA-Z0-9_.-]+).*$/$1/; + +$SenderShort = findUser($SenderShort); + +print "SenderShort is $SenderShort\n"; +if (!defined($SenderShort)) { + $SenderShort = $Sender; + $SenderShort =~ s/^.*?([a-zA-Z0-9_.-]+?\@[a-zA-Z0-9_.-]+\.[a-zA-Z0-9_.-]+).*$/$1/; +} +print "The sendershort is now $SenderShort\n"; + +my $Subject = ""; +$Subject = $entity->get( 'Subject' ); +chomp( $Subject ); + +# Get all the attachments +dump_entity($entity); +# print $Body; +$Body = extractControls( $Body ); # fills the Control-Hash + +if( $test ) { + foreach (keys %Control ) { + print "$_ => $Control{$_}\n"; + } +} + +$Control{'short_desc'} ||= $Subject; +# +# * Mailparsing finishes here * +# + +###################################################################### +# Now a lot of Checks of the given Labels start. +# Check Control-Labels +# not: reporter ! +@AllowedLabels = ("product", "version", "rep_platform", + "bug_severity", "priority", "op_sys", "assigned_to", + "bug_status", "bug_file_loc", "short_desc", "component", + "status_whiteboard", "target_milestone", "groupset", + "qa_contact"); +#my @AllowedLabels = qw{Summary priority platform assign}; +foreach (keys %Control) { + if ( lsearch( \@AllowedLabels, $_) < 0 ) { + BugMailError( 0, "You sent a unknown label: " . $_ ); + } +} + +push( @AllowedLabels, "reporter" ); +$Control{'reporter'} = $SenderShort; + +# Check required Labels - not all labels are required, because they could be generated +# from the given information +# Just send a warning- the error-Flag will be set later +@RequiredLabels = qw{product version component short_desc}; +foreach my $Label (@RequiredLabels) { + if ( ! defined $Control{$Label} ) { + BugMailError( 0, "You were missing a required label: \@$Label\n" ); + next; + } + + if( $Control{$Label} =~ /^\s*$/ ) { + BugMailError( 0, "One of your required labels is empty: $Label" ); + next; + } +} + +if ( $Body =~ /^\s*$/s ) { + BugMailError( 1, "You sent a completely empty body !" ); +} + + +# umask 0; + +# Check Permissions ... +if (! CheckPermissions("CreateBugs", $SenderShort ) ) { + BugMailError( 2, "Permission denied.\n\n" . + "You do not have the permissions to create a new bug. Sorry.\n" ); +} + +# Set QA +SendSQL("select initialqacontact from components where program=" . + SqlQuote($Control{'product'}) . + " and value=" . SqlQuote($Control{'component'})); +my $qacontact = FetchOneColumn(); +if (defined $qacontact && $qacontact !~ /^\s*$/) { + #$Control{'qa_contact'} = DBNameToIdAndCheck($qacontact, 1); + $Control{'qa_contact'} = DBname_to_id($qacontact); + + if ( ! $Control{'qa_contact'} ) { + BugMailError( 0, "Could not resolve qa_contact !\n" ); + } + + #push(@bug_fields, "qa_contact"); +} + +# Set Assigned - assigned_to depends on the product, cause initialowner +# depends on the product ! +# => first check product ! +# Product +my @all_products = (); +# set to the default product. If the default product is empty, this has no effect +my $Product = $DEFAULT_PRODUCT; +$Product = CheckProduct( $Control{'product'} ) if( defined( $Control{ 'product'} )); + +if ( $Product eq "" ) { + my $Text = "You didnt send a value for the required key \@product !\n\n"; + + $Text = "You sent the invalid product \"$Control{'product'}\"!\n\n" + if( defined( $Control{ 'product'} )); + + $Text .= "Valid products are:\n\t"; + + SendSQL("select product from products"); + @all_products = FetchAllSQLData(); + $Text .= join( "\n\t", @all_products ) . "\n\n"; + $Text .= horLine(); + + BugMailError( 1, $Text ); +} else { + # Fill list @all_products, which is needed in case of component-help + @all_products = ( $Product ); + $product_valid = 1; +} +$Control{'product'} = $Product; + +# +# Check the Component: +# + +# set to the default component. If the default component is empty, this has no effect +my $Component = $DEFAULT_COMPONENT; + +if( defined( $Control{'component' } )) { + $Component = CheckComponent( $Control{'product'}, $Control{'component'} ); +} + +if ( $Component eq "" ) { + + my $Text = "You did not send a value for the required key \@component!\n\n"; + + if( defined( $Control{ 'component' } )) { + $Text = "You sent the invalid component \"$Control{'component'}\" !\n"; + } + + # + # Attention: If no product was sent, the user needs info for all components of all + # products -> big reply mail :) + # if a product was sent, only reply the components of the sent product + my @val_components = (); + foreach my $prod ( @all_products ) { + $Text .= "\nValid components for product `$prod' are: \n\t"; + + SendSQL("select value from components where program=" . SqlQuote( $prod ) . ""); + @val_components = FetchAllSQLData(); + + $Text .= join( "\n\t", @val_components ) . "\n"; + } + + # Special: if there is a valid product, maybe it has only one component -> use it ! + # + my $amount_of_comps = @val_components; + if( $product_valid && $amount_of_comps == 1 ) { + $Component = $val_components[0]; + + $Text .= " * You did not send a component, but a valid product " . SqlQuote( $Product ) . ".\n"; + $Text .= " * This product only has one component ". SqlQuote( $Component ) .".\n" . + " * This component was set by bugzilla for submitting the bug.\n\n"; + BugMailError( 0, $Text ); # No blocker + + } else { # The component is really buggy :( + $Text .= horLine(); + BugMailError( 1, $Text ); + } +} +$Control{'component'} = $Component; + + +# +# Check assigned_to +# if no assigned_to was given, generate it from the product-DB +my $forceAssignedOK = 0; +if ( (! defined($Control{'assigned_to'}) ) + || $Control{'assigned_to'} =~ /^\s*$/ ) { + SendSQL("select initialowner from components where program=" . + SqlQuote($Control{'product'}) . + " and value=" . SqlQuote($Control{'component'})); + $Control{'assigned_to'} = FetchOneColumn(); + $forceAssignedOK = 1; +} + + +# Recode Names +$Control{'assigned_to'} = DBname_to_id($Control{'assigned_to'}, $forceAssignedOK); + +if ( $Control{'assigned_to'} == 0 ) { + my $Text = "Could not resolve key \@assigned_to !\n" . + "If you do NOT send a value for assigned_to, the bug will be assigned to\n" . + "the qa-contact for the product and component.\n"; + $Text .= "This works only if product and component are OK. \n" + . horLine(); + + BugMailError( 1, $Text ); +} + + +$Control{'reporter'} = DBname_to_id($Control{'reporter'}); +if ( ! $Control{'reporter'} ) { + BugMailError( 1, "Could not resolve reporter !\n" ); +} + +### Set default values +CheckPriority( ); +CheckSeverity( ); +CheckPlatform( ); +CheckSystem( ); +# CheckArea(); + +### Check values ... +# Version +my $Version = ""; +$Version = CheckVersion( $Control{'product'}, $Control{'version'} ) if( defined( $Control{'version'})); +if ( $Version eq "" ) { + my $Text = "You did not send a value for the required key \@version!\n\n"; + + if( defined( $Control{'version'})) { + my $Text = "You sent the invalid version \"$Control{'version'}\"!\n"; + } + + my $anz_versions; + my @all_versions; + # Assemble help text + foreach my $prod ( @all_products ) { + $Text .= "Valid versions for product " . SqlQuote( $prod ) . " are: \n\t"; + + SendSQL("select value from versions where program=" . SqlQuote( $prod ) . ""); + @all_versions = FetchAllSQLData(); + $anz_versions = @all_versions; + $Text .= join( "\n\t", @all_versions ) . "\n" ; + + } + + # Check if we could use the only version + if( $anz_versions == 1 && $product_valid ) { + $Version = $all_versions[0]; + # Fine, there is only one version string + $Text .= " * You did not send a version, but a valid product " . SqlQuote( $Product ) . ".\n"; + $Text .= " * This product has has only the one version ". SqlQuote( $Version) .".\n" . + " * This version was set by bugzilla for submitting the bug.\n\n"; + $Text .= horLine(); + BugMailError( 0, $Text ); # No blocker + } else { + $Text .= horLine(); + BugMailError( 1, $Text ); + } + +} + +$Control{'version'} = $Version; + +# GroupsSet: Protections for Bug info. This paramter controls the visiblility of the +# given bug. An Error in the given Buggroup is not a blocker, a default is taken. +# +# The GroupSet is accepted in three ways: As single number like 65536 +# As added numbers like 65536 + 6 +8 +# As literals linked with whitespaces, plus-signs or kommas +# +my $GroupSet = ""; +$GroupSet = $Control{'groupset'} if( defined( $Control{ 'groupset' })); +# +# Fetch the default value for groupsetting +SendSQL("select bit from groups where name=" . SqlQuote( "ReadInternal" )); +my $default_group = FetchOneColumn(); + +if( $GroupSet eq "" ) { + # To bad: Groupset does not contain anything -> set to default + $GroupSet = $default_group; + # + # Give the user a hint + my $Text = "You did not send a value for optional key \@groupset, which controls\n"; + $Text .= "the Permissions of the bug. It was set to a default value 'Internal Bug'\n"; + $Text .= "Probably the QA will change that if desired.\n"; + + BugMailError( 0, $Text ); +} elsif( $GroupSet =~ /^\d+$/ ) { + # Numerical Groups (no +-signs), the GroupSet must be the sum of the bits + # + my $grp_num = $GroupSet; + # print "Numeric: $GroupSet\n"; + SendSQL("select bit from groups where isbuggroup=1 order by bit"); + my @Groups = FetchAllSQLData(); + + # DANGEROUS: This code implies, that perl *CAN* cope with large numbers + # Its probably better to allow only one default-group when mailing ! + my $Val = "0"; + foreach ( @Groups ) { + # print 0+$grp_num & 0+$_ , "\n"; + if ( ( (0+$grp_num) & (0+$_) ) == 0+$_ ) { + $Val .= sprintf( "+%d", $_ ); + } + } + if( $Val eq "0" ) { + # No valid group found + my $Text = "The number you sent for the groupset of the bug was wrong.\n" . + "It was not the sum of valid bits, which are:\n\t"; + $Text .= join( "\n\t", @Groups ) . "\n"; + $Text .= "The groupset for your bug is set to default $default_group, which\n" . + "means 'ReadInternal'"; + + BugMailError( 0, $Text ); + $GroupSet = $default_group; + } else { + $GroupSet = $Val; + } + +} elsif( $GroupSet =~ /^(\s*\d+\s*\+\s*)+/ ) { + # + # Groupset given as String with added numbers like 65536+131072 + # The strings are splitted and checked if the numbers are in the DB + my @bits = split( /\s*\+\s*/, $GroupSet ); + my $new_groupset = "0"; + # Get all bits for groupsetting + SendSQL("select bit from groups where isbuggroup=1" ); + my @db_bits = FetchAllSQLData(); + + # ... and check, if the given bits and the one in the DB fit together + foreach my $bit ( @bits ) { + # print "The Bit is: $bit \n"; + if( lsearch( \@db_bits, $bit ) == -1 ) { + # Bit not found ! + my $Text = "Checking the Group-Settings: You sent the Groupset-Bit $bit\n" . + "which is not a valid Groupset-Bit. It will be scipped !\n\n"; + BugMailError( 0, $Text ); + } else { + # Cool bit, add to the result-String + $new_groupset .= "+" . $bit; + } + } + + # Is the new-String larger than 0 + if( $new_groupset eq "0" ) { + $new_groupset = $default_group; + + my $Text = "All given Groupsetting-Bits are invalid. Setting Groupsetting to\n" . + "default-Value $new_groupset, what means 'ReadInternal'\n\n"; + + BugMailError( 0, $Text ); + } + # Restore to Groupset-Variable + $GroupSet = $new_groupset; + +} else { + # literal e.g. 'ReadInternal' + my $Value = "0"; + my $gserr = 0; + my $Text = ""; + + # + # Split literal Groupsettings either on Whitespaces, +-Signs or , + # Then search for every Literal in the DB - col name + foreach ( split /\s+|\s*\+\s*|\s*,\s*/, $GroupSet ) { + SendSQL("select bit, Name from groups where name=" . SqlQuote($_)); + my( $bval, $bname ) = FetchSQLData(); + + if( defined( $bname ) && $_ eq $bname ) { + $Value .= sprintf( "+%d", $bval ); + } else { + $Text .= "You sent the wrong GroupSet-String $_\n"; + $gserr = 1; + } + } + # + # Give help if wrong GroupSet-String came + if( $gserr > 0 ) { + # There happend errors + $Text .= "Here are all valid literal Groupsetting-strings:\n\t"; + SendSQL( "select name from groups where isbuggroup=1" ); + $Text .= join( "\n\t", FetchAllSQLData()) . "\n"; + BugMailError( 0, $Text ); + } + + # + # Check if anything was right, if not -> set default + if( $Value eq "0" ) { + $Value = $default_group; + $Text .= "\nThe group will be set to $default_group, what means 'ReadInternal'\n\n"; + } + + $GroupSet = $Value; +} # End of checking groupsets + +$Control{'groupset'} = $GroupSet; + +# ################################################################################### +# Checking is finished +# + +# Check used fields +my @used_fields; + +foreach my $f (@AllowedLabels) { + if ((exists $Control{$f}) && ($Control{$f} !~ /^\s*$/ )) { + push (@used_fields, $f); + } +} + +# +# Creating the query for inserting the bug +# -> this should only be done, if there was no critical error before +if( $critical_err == 0 ) +{ + + my $reply = <<END + + +---------------------------------------------------------------------------+ + B U G Z I L L A - M A I L - I N T E R F A C E + +---------------------------------------------------------------------------+ + + Your Bugzilla Mail Interface request was successfull. + +END +; + + $reply .= "Your Bug-ID is "; + my $reporter = ""; + + my $query = "insert into bugs (\n" . join(",\n", @used_fields ) . + ", bug_status, creation_ts, everconfirmed) values ( "; + + my $tmp_reply = "These values were stored by bugzilla:\n"; + my $val; + foreach my $field (@used_fields) { + if( $field eq "groupset" ) { + $query .= $Control{$field} . ",\n"; + } else { + $query .= SqlQuote($Control{$field}) . ",\n"; + } + + $val = $Control{ $field }; + + $val = DBID_to_name( $val ) if( $field =~ /reporter|assigned_to|qa_contact/ ); + $val = groupBitToString( $val ) if( $field =~ /groupset/ ); + + $tmp_reply .= sprintf( " \@%-15s = %-15s\n", $field, $val ); + + if ($field eq "reporter") { + $reporter = $val; + } + } + + $tmp_reply .= " ... and your error-description !\n"; + + my $comment = $Body; + $comment =~ s/\r\n/\n/g; # Get rid of windows-style line endings. + $comment =~ s/\r/\n/g; # Get rid of mac-style line endings. + $comment = trim($comment); + + SendSQL("SELECT now()"); + my $bug_when = FetchOneColumn(); + + my $ever_confirmed = 0; + my $state = SqlQuote("UNCONFIRMED"); + + SendSQL("SELECT votestoconfirm FROM products WHERE product = " . + SqlQuote($Control{'product'}) . ";"); + if (!FetchOneColumn()) { + $ever_confirmed = 1; + $state = SqlQuote("NEW"); + } + + + $query .= $state . ", \'$bug_when\', $ever_confirmed)\n"; +# $query .= SqlQuote( "NEW" ) . ", now(), " . SqlQuote($comment) . " )\n"; + + SendSQL("SELECT userid FROM profiles WHERE login_name=\'$reporter\'"); + my $userid = FetchOneColumn(); + + my $id; + + if( ! $test ) { + SendSQL($query); + + SendSQL("select LAST_INSERT_ID()"); + $id = FetchOneColumn(); + + my $long_desc_query = "INSERT INTO longdescs SET bug_id=$id, who=$userid, bug_when=\'$bug_when\', thetext=" . SqlQuote($comment); + SendSQL($long_desc_query); + + # Cool, the mail was successfull + system("cd .. ; ./processmail $id '$Sender'"); + } else { + $id = 0xFFFF; # TEST ! + print "\n-------------------------------------------------------------------------\n"; + print "$query\n"; + } + + # + # handle Attachments + # + my $attaches = storeAttachments( $id, $Control{'reporter'} ); + $tmp_reply .= "\n\tYou sent $attaches attachment(s). \n" if( $attaches > 0 ); + + $reply .= $id . "\n\n" . $tmp_reply . "\n" . getWarningText(); + + $entity->purge(); # Removes all temp files + + # + # Send the 'you did it'-reply + Reply( $SenderShort, $Message_ID,"Bugzilla success (ID $id)", $reply ); + +} else { + # There were critical errors in the mail - the bug couldnt be inserted. ! +my $errreply = <<END + + +---------------------------------------------------------------------------+ + B U G Z I L L A - M A I L - I N T E R F A C E + +---------------------------------------------------------------------------+ + +END + ; + + $errreply .= getErrorText() . getWarningText() . generateTemplate(); + + Reply( $SenderShort, $Message_ID, "Bugzilla Error", $errreply ); + + # print getErrorText(); + # print getWarningText(); + # print generateTemplate(); +} + + + + + +exit; + diff --git a/contrib/bugmail_help.html b/contrib/bugmail_help.html new file mode 100644 index 0000000000000000000000000000000000000000..00b0f515373170286765c7e83d7fd8cf274ee567 --- /dev/null +++ b/contrib/bugmail_help.html @@ -0,0 +1,223 @@ +<HTML> +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN"> +<!-- + 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. + + Contributor(s): Klaas Freitag <Freitag@SuSE.de> +--> + +<HEAD> <TITLE>Bugzilla Mail Interface</TITLE> </HEAD> +<BODY BGCOLOR="#FFFFFF"> +<CENTER><H1>The Bugzilla Mail Interface</H1> +Contributor: <A HREF="mailto:freitag@suse.de">Klaas Freitag</A>, SuSE GmbH +</CENTER> +<P> +The bugzilla Mail interface allows the registered bugzilla users to submit bugs by +sending email with a bug description. This is usefull for people, who do not work +inhouse and want to submitt bugs to the bugzilla system. +<p> + + +I know, show me the <A HREF="#examplemail">example-mail !</A> + + +<H2>What do you need to do to submitt a bug by mail ?</H2> +You need to send a email in the described format to the bugmail-user of the +bugzilla-system. This is <A HREF="mailto:our_bugzilla@xyz.com">yourbugzilla@here.com</A> + +You receive a reply mail with the new bug-ID if your request was ok. +If not, you get a mail with +some help on the bugmail system and a specific analysis of your request. +<P> +Please dont refuse to send one or two wrong mails, you will get all the information +you need in the replies, and <I>only</I> in the mail replies. The information on this +page, concerning available products, versions and so on, is not dynamicly generated and +may be old therefore. + +<H1>The Mail Format</H1> +The bugmail needs a special format , which consists of some keywords and suitable +values for them and a description text. Note that the keyword block needs to be +above of the description text. + +<H2>Keywords</H2> +You need to tell bugzilla some properties of the bugs. This is done by keywords, which +start on a new line with a @, followed by the keyword and and equal-sign, followed by a +hopefully valid value. + + +<TABLE BORDER=4 FRAME=box CELLSPACING="5" width=95%> <COLGROUP> <col width="2*"> +<col width="5*"> <col width="1*"> </COLGROUP> + <TR> + <TH>Keyword</TH> + <TH>Value description</TH> + <TH>required and default value</TH> + </TR> + <TR> + <TD>@product</TD> + <TD>The product which has a bug</TD> + <TD>yes. <br> This is the most important information. Many other + fields depend on the product.</TD> + </TR> + <TR> + <TD>@component</TD> + <TD>the desired component which is affected by the bug</TD> + <TD>yes. <br> As the @product, this is a very important + field.</TD> + </TR> + <TR> + <TD>@version</TD> + <TD>The version of the product</TD> + <TD>yes. <br>See @product and @component</TD> + </TR> + <TR> + <TD>@short_desc</TD> + <TD>A summary of your bug report</TD> + <TD>yes. <br>This summary of the error you want to report + describes what happen. You may skip the long description, + but not this summary.<br> + <b>Note:</b>The short description may be given in the mail subject + instead of using the keyword !</TD> + </TR> + <TR> + <TD>@rep_platform</TD> + <TD>The desired platform</TD> + <TD>no.<br>If you dont give a value, this field is set to <I>All</I>.</TD> + </TR> + <TR> + <TD>@bug_severity</TD> + <TD>The severity of the bug</TD> + <TD>no. <br> If you dont give a value, this field is set to + <I>normal</I></TD> + </TR> + <TR> + <TD>@priority</TD> + <TD>The priority of the bug</TD> + <TD>no.<br>If you dont give a value, this field is set to <I>P3</I></TD> + </TR> + <TR> + <TD>@op_sys</TD> + <TD>The operating system</TD> + <TD>no.<br>If you dont give a value, this field is set to <I>Linux</I>.</TD> + </TR> + <TR> + <TD>@assigned_to</TD> + <TD>The one to whom the bug is assigned to</TD> + <TD>no. <br>There is an initial owner for every product/version/component. + He owns the bug by default. The initial owner can only be found if + product, version and component are valid.</TD> + </TR> + <TR> + <TD>@bug_file_loc</TD> + <TD>?</TD> + <TD>no.</TD> + </TR> + <TR> + <TD>@status_whiteboard</TD> + <TD>?</TD> + <TD>no.</TD> + </TR> + <TR> + <TD>@target_milestone</TD> + <TD>?</TD> + <TD>no.</TD> + </TR> + <TR> + <TD>@groupset</TD> + <TD>rules the visibility of the bug.</TD> + <TD>no.<br>This value defaults to the smallest of the available groups, + which is <I>readInternal</I>.</TD> + </TR> + <TR> + <TD>@qa_contact</TD> + <TD>the quality manager for the product</TD> + <TD>no.<br>This value can be retrieved from product, component and + version</TD> + </TR> + +</TABLE> +<H2>Valid values</H2> +Give string values for the most keys above. Some keywords require special values:<br> +<ol> +<li>E-Mail adresses: If you want to set the qa-contact, specify a email-adress for @qa_contact. The email must be known by bugzilla of course.</li> +<li>Listvalues: Most of the values have to be one of a list of valid values. Try by sending +a mail and read the reply. Skip fields if you dont get help for them unless you dont know +which values you may choose.</li> +<li>free Text: The descriptions may be free text. </li> +<li>Special: The field groupset may be specified in different in three different kinds: + <ol> + <li> A plain numeric way, which is one usually huge number, e. g. <I>65536</I></li> + <li> a string with added numbers e.g. <I>65536+131072</I></li> + <li> a string list, e.g. <I>ReadInternal, ReadBeta </I></li> + </ol> +</li> +</ol> + +<p> + +But most of them need <b>valid</b> values. +<p> +Sorry, you will not find lists of valid products, components and the other stuff +here. Send a mail to with any text, and you will get a list of valid keywords in the reply. + +<p> +Some of the values must be choosen from a list:<br> +<ol> + <li>bug_severity: blocker, critical, major, normal, minor, trivial, enhancement</li> + <li>op_sys: Linux </li> + <li>priority: P1, P2, P3, P4, P5</li> + <li>rep_platform: All, i386, AXP, i686, Other</li></ol> + + +<p> + +After you have specified the required keywords and maybe some other value, you may +describe your bug. You dont need a keyword for starting your bug description. All +text which follows the keyword block is handled as long description of the bug. +<p> + +The bugmail interface is able to find required information by itself. E.g. if you specify +a product which has exactly one component, this component will be found by the interface +automatically. + +<H1>Attachments</H1> + +The mail interface is able to cope with MIME-attachments. +People could for example add a logfile as a mail attachment, and it will appear in +bugzilla as attachment. A comment for the attachment should be added, it will describe +the attachment in bugzilla. + +<H1><A NAME="examplemail">Example Mail</A></H1> + +See the example of the mail <b>body</b> (Dont forget to specify the short description +in the mail subject):<hr><pre> + + @product = Bugzilla + @component = general + @version = All + @groupset = ReadWorld ReadPartners + @op_sys = Linux + @priority = P3 + @rep_platform = i386 + + + This is the description of the bug I found. It is not neccessary to start + it with a keyword. + + Note: The short_description is neccessary and may be given with the keyword + @short_description or will be retrieved from the mail subject. + + +</pre><hr> + +</BODY> +</HTML> diff --git a/contrib/bugzilla.procmailrc b/contrib/bugzilla.procmailrc new file mode 100644 index 0000000000000000000000000000000000000000..36656b4d2d7b14d2a772516d622751a9940b09eb --- /dev/null +++ b/contrib/bugzilla.procmailrc @@ -0,0 +1,30 @@ +:0 fhw +| formail -I "From " -a "From " + +BUGZILLA_HOME=/home/bugzilla/WEB/bugzilla/contrib + +:0 +* ^Subject: .*\[Bug .*\] +RESULT=|(cd $BUGZILLA_HOME && ./bugzilla_email_append.pl) + + +# Feed mail to stdin of bug_email.pl +:0 Ec +#* !^Subject: .*[Bug .*] +RESULT=|(cd $BUGZILLA_HOME && ./bug_email.pl ) + +# write result to a logfile +:0 c +|echo `date '+%d.%m.%y %H:%M: '` $RESULT >> $HOME/bug_email.log + + +:0 c +|echo "----------------------------------" >> $HOME/bug_email.log + +:0 c +$HOME/bug_email.log + +# Move mail to the inbox +:0 +$HOME/Mail/INBOX + diff --git a/contrib/bugzilla_email_append.pl b/contrib/bugzilla_email_append.pl new file mode 100755 index 0000000000000000000000000000000000000000..b10d8e0308712efaca4f0c5e2fe966764984cec7 --- /dev/null +++ b/contrib/bugzilla_email_append.pl @@ -0,0 +1,189 @@ +#!/usr/bin/perl -w +# -*- Mode: perl; indent-tabs-mode: nil -*- + +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. + +# The purpose of this script is to take an email message, which +# specifies a bugid and append it to the bug as part of the longdesc +# table + +# Contributor : Seth M. Landsman <seth@dworkin.net> + +# 03/15/00 : Initial version by SML +# 03/15/00 : processmail gets called + +# Email subject must be of format : +# .* Bug ### .* +# replying to a typical bugzilla email should be valid + +# TODO : +# 1. better way to get the body text (I don't know what dump_entity() is +# actually doing + +use diagnostics; +use strict; +use MIME::Parser; + +push @INC, "../."; # this script lives in contrib +require "globals.pl"; +require "BugzillaEmail.pm"; + +# Create a new MIME parser: +my $parser = new MIME::Parser; + +my $Comment = ""; + +# Create and set the output directory: +# FIXME: There should be a $BUGZILLA_HOME variable (SML) +(-d "../data/mimedump-tmp") or mkdir "../data/mimedump-tmp",0755 or die "mkdir: $!"; +(-w "../data/mimedump-tmp") or die "can't write to directory"; + +$parser->output_dir("../data/mimedump-tmp"); + +# Read the MIME message: +my $entity = $parser->read(\*STDIN) or die "couldn't parse MIME stream"; +$entity->remove_sig(10); # Removes the signature in the last 10 lines + +# Getting values from parsed mail +my $Sender = $entity->get( 'From' ); +$Sender ||= $entity->get( 'Reply-To' ); +my $Message_ID = $entity->get( 'Message-Id' ); + +die (" *** Cant find Sender-adress in sent mail ! ***\n" ) unless defined( $Sender ); +chomp( $Sender ); +chomp( $Message_ID ); + +print "Dealing with the sender $Sender\n"; + +ConnectToDatabase(); + +my $SenderShort = $Sender; +$SenderShort =~ s/^.*?([a-zA-Z0-9_.-]+?\@[a-zA-Z0-9_.-]+\.[a-zA-Z0-9_.-]+).*$/$1/; + +$SenderShort = findUser($SenderShort); + +print "SenderShort is $SenderShort\n"; +if (!defined($SenderShort)) { + $SenderShort = $Sender; + $SenderShort =~ s/^.*?([a-zA-Z0-9_.-]+?\@[a-zA-Z0-9_.-]+\.[a-zA-Z0-9_.-]+).*$/$1/; +} +print "The sendershort is now $SenderShort\n"; + +if (!defined($SenderShort)) { + DealWithError("No such user $SenderShort exists."); +} + +my $Subject = $entity->get('Subject'); +print "The subject is $Subject\n"; + +my ($bugid) = ($Subject =~ /\[Bug ([\d]+)\]/); +print "The bugid is $bugid\n"; + +# make sure the bug exists + +SendSQL("SELECT bug_id FROM bugs WHERE bug_id = $bugid;"); +my $found_id = FetchOneColumn(); +print "Did we find the bug? $found_id-\n"; +if (!defined($found_id)) { + DealWithError("Bug $bugid does not exist"); +} + +# get the user id +SendSQL("SELECT userid FROM profiles WHERE login_name = \'$SenderShort\';"); +my $userid = FetchOneColumn(); +if (!defined($userid)) { + DealWithError("Userid not found for $SenderShort"); +} + +# parse out the text of the message +dump_entity($entity); + +# Get rid of the bug id +$Subject =~ s/\[Bug [\d]+\]//; +#my $Comment = "This is only a test ..."; +my $Body = "Subject: " . $Subject . "\n" . $Comment; + +# shove it in the table +my $long_desc_query = "INSERT INTO longdescs SET bug_id=$found_id, who=$userid, bug_when=NOW(), thetext=" . SqlQuote($Body) . ";"; +SendSQL($long_desc_query); + +system("cd .. ; ./processmail $found_id '$SenderShort'"); + +sub DealWithError { + my ($reason) = @_; + print $reason . "\n"; +} + +# Yanking this wholesale from bug_email, 'cause I know this works. I'll +# figure out what it really does later +#------------------------------ +# +# dump_entity ENTITY, NAME +# +# Recursive routine for parsing a mime coded mail. +# One mail may contain more than one mime blocks, which need to be +# handled. Therefore, this function is called recursively. +# +# It gets the for bugzilla important information from the mailbody and +# stores them into the global attachment-list @attachments. The attachment-list +# is needed in storeAttachments. +# +sub dump_entity { + my ($entity, $name) = @_; + defined($name) or $name = "'anonymous'"; + my $IO; + + + # Output the body: + my @parts = $entity->parts; + if (@parts) { # multipart... + my $i; + foreach $i (0 .. $#parts) { # dump each part... + dump_entity($parts[$i], ("$name, part ".(1+$i))); + } + } else { # single part... + + # Get MIME type, and display accordingly... + my $msg_part = $entity->head->get( 'Content-Disposition' ); + + $msg_part ||= ""; + + my ($type, $subtype) = split('/', $entity->head->mime_type); + my $body = $entity->bodyhandle; + my ($data, $on_disk ); + + if( $msg_part =~ /^attachment/ ) { + # Attached File + my $des = $entity->head->get('Content-Description'); + $des ||= ""; + + if( defined( $body->path )) { # Data is on disk + $on_disk = 1; + $data = $body->path; + + } else { # Data is in core + $on_disk = 0; + $data = $body->as_string; + } +# push ( @attachments, [ $data, $entity->head->mime_type, $on_disk, $des ] ); + } else { + # Real Message + if ($type =~ /^(text|message)$/) { # text: display it... + if ($IO = $body->open("r")) { + $Comment .= $_ while (defined($_ = $IO->getline)); + $IO->close; + } else { # d'oh! + print "$0: couldn't find/open '$name': $!"; + } + } else { print "Oooops - no Body !\n"; } + } + } +} diff --git a/createaccount.cgi b/createaccount.cgi index 0a7a1351a346802c55b66968b3e3f12115b81794..14420a65efd205c19ddd12297b11041dc0cbfcd6 100755 --- a/createaccount.cgi +++ b/createaccount.cgi @@ -53,6 +53,7 @@ if (defined $login) { print "exists. If you have forgotten the password for it, then\n"; print "<a href=query.cgi?GoAheadAndLogIn>click here</a> and use\n"; print "the <b>E-mail me a password</b> button.\n"; + PutFooter(); exit; } PutHeader("Account created"); @@ -63,6 +64,7 @@ if (defined $login) { print "received, you may <a href=query.cgi?GoAheadAndLogIn>click\n"; print "here</a> and log in. Or, you can just <a href=\"\">go back to\n"; print "the top</a>."; + PutFooter(); exit; } @@ -87,3 +89,4 @@ as well. <input type=submit> }; +PutFooter(); diff --git a/createattachment.cgi b/createattachment.cgi index ba4ba25e8b1fc36489d27a8ebf24e0a686e75993..fa370d71034251504bac20181d23c7fcefe55ad8 100755 --- a/createattachment.cgi +++ b/createattachment.cgi @@ -31,6 +31,7 @@ use vars %::COOKIE, %::FILENAME; sub Punt { my ($str) = (@_); print "$str<P>Please hit <b>Back</b> and try again.\n"; + PutFooter(); exit; } @@ -40,6 +41,7 @@ confirm_login(); print "Content-type: text/html\n\n"; my $id = $::FORM{'id'}; +die "invalid id: $id" unless $id=~/^\s*\d+\s*$/; PutHeader("Create an attachment", "Create attachment", "Bug $id"); @@ -108,5 +110,5 @@ What kind of file is this? print "<TD><A HREF=\"show_bug.cgi?id=$id\">Go Back to BUG# $id</A></TABLE>\n"; } -navigation_header(); +PutFooter(); diff --git a/defparams.pl b/defparams.pl index bb266d37af7edbafef13fc1a83e69ad3c23dd73e..41a40faf01cfd16a68c93ed8598975f6e2bad685 100644 --- a/defparams.pl +++ b/defparams.pl @@ -19,7 +19,8 @@ # # Contributor(s): Terry Weissman <terry@mozilla.org> # Dawn Endico <endico@mozilla.org> - +# Dan Mosedale <dmose@mozilla.org> +# Joe Robins <jmrobins@tgix.com> # This file defines all the parameters that we have a GUI to edit within # Bugzilla. @@ -82,7 +83,23 @@ sub check_numeric { return ""; } - +sub check_shadowdb { + my ($value) = (@_); + $value = trim($value); + if ($value eq "") { + return ""; + } + SendSQL("SHOW DATABASES"); + while (MoreSQLData()) { + my $n = FetchOneColumn(); + if (lc($n) eq lc($value)) { + return "The $n database already exists. If that's really the name you want to use for the backup, please CAREFULLY make the existing database go away somehow, and then try again."; + } + } + SendSQL("CREATE DATABASE $value"); + SendSQL("INSERT INTO shadowlog (command) VALUES ('SYNCUP')", 1); + return ""; +} @::param_list = (); @@ -120,11 +137,51 @@ sub check_urlbase { return ""; } +DefParam("preferlists", + "If this is on, Bugzilla will display most selection options as selection lists. If this is off, Bugzilla will use radio buttons and checkboxes instead.", + "b", + 1); + +DefParam("prettyasciimail", + "If this is on, Bugzilla will send email reports formatted (assuming 76 character monospace font display). If this is off, email reports are sent using the old 'one-item-per-line' format.", + "b", + 0); + +DefParam("capitalizelists", + "If this is on, Bugzilla will capitalize list entries, checkboxes, and radio buttons. If this is off, Bugzilla will leave these items untouched.", + "b", + 0); + + DefParam("usequip", "If this is on, Bugzilla displays a silly quip at the beginning of buglists, and lets users add to the list of quips.", "b", 1); +# Added parameter - JMR, 2/16/00 +DefParam("usebuggroups", + "If this is on, Bugzilla will associate a bug group with each product in the database, and use it for querying bugs.", + "b", + 0); + +# Added parameter - JMR, 2/16/00 +DefParam("usebuggroupsentry", + "If this is on, Bugzilla will use product bug groups to restrict who can enter bugs. Requires usebuggroups to be on as well.", + "b", + 0); + +DefParam("shadowdb", + "If non-empty, then this is the name of another database in which Bugzilla will keep a shadow read-only copy of everything. This is done so that long slow read-only operations can be used against this db, and not lock up things for everyone else. Turning on this parameter will create the given database; be careful not to use the name of an existing database with useful data in it!", + "t", + "", + \&check_shadowdb); + +DefParam("queryagainstshadowdb", + "If this is on, and the shadowdb is set, then queries will happen against the shadow database.", + "b", + 0); + + DefParam("usedespot", "If this is on, then we are using the Despot system to control our database of users. Bugzilla won't ever write into the user database, it will let the Despot code maintain that. And Bugzilla will send the user over to Despot URLs if they need to change their password. Also, in that case, Bugzilla will treat the passwords stored in the database as being crypt'd, not plaintext.", "b", @@ -151,6 +208,25 @@ DefParam("headerhtml", "l", ''); +DefParam("footerhtml", + "HTML to add to the bottom of every page. By default it displays the blurbhtml, and %commandmenu%, a menu of useful commands. You probably really want either headerhtml or footerhtml to include %commandmenu%.", + "l", + '<TABLE BORDER="0"><TR><TD BGCOLOR="#000000" VALIGN="TOP"> +<TABLE BORDER="0" CELLPADDING="10" CELLSPACING="0" WIDTH="100%" BGCOLOR="lightyellow"> +<TR><TD> +%blurbhtml% +<BR> +%commandmenu% +</TD></TR></TABLE></TD></TR></TABLE>'); + +DefParam("errorhtml", + "This is what is printed out when a form is improperly filled out. %errormsg% is replaced by the actual error itself; %<i>anythingelse</i>% gets replaced by the definition of that parameter (as defined on this page).", + "l", + qq{<TABLE CELLPADDING=20><TR><TD BGCOLOR="#ff0000"> +<FONT SIZE="+2">%errormsg%</FONT></TD></TR></TABLE> +<P>Please press <B>Back</B> and try again.<P>}); + + DefParam("bannerhtml", "The html that gets emitted at the head of every Bugzilla page. @@ -173,8 +249,41 @@ information about what Bugzilla is and what it can do, see <A HREF=\"http://www.mozilla.org/bugs/\"><B>bug pages</B></A>."); - +DefParam("mybugstemplate", + "This is the URL to use to bring up a simple 'all of my bugs' list for a user. %userid% will get replaced with the login name of a user.", + "t", + "buglist.cgi?bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&email1=%userid%&emailtype1=exact&emailassigned_to1=1&emailreporter1=1"); + + +DefParam("shutdownhtml", + "If this field is non-empty, then Bugzilla will be completely disabled and this text will be displayed instead of all the Bugzilla pages.", + "l", + ""); + + +DefParam("passwordmail", +q{The email that gets sent to people to tell them their password. Within +this text, %mailaddress% gets replaced by the person's email address, +%login% gets replaced by the person's login (usually the same thing), and +%password% gets replaced by their password. %<i>anythingelse</i>% gets +replaced by the definition of that parameter (as defined on this page).}, + "l", + q{From: bugzilla-daemon +To: %mailaddress% +Subject: Your Bugzilla password. + +To use the wonders of Bugzilla, you can use the following: + + E-mail address: %login% + Password: %password% + + To change your password, go to: + %urlbase%userprefs.cgi +}); + + + DefParam("changedmail", q{The email that gets sent to people when a bug changes. Within this text, %to% gets replaced by the assigned-to and reported-by people, @@ -197,6 +306,27 @@ Subject: [Bug %bugid%] %neworchanged% - %summary% %diffs%"); +DefParam("newemailtech", +q{There is now experimental code in Bugzilla to do the email diffs in a +new and exciting way. But this stuff is not very cooked yet. So, right +now, to use it, the maintainer has to turn on this checkbox, and each user +has to then turn on the "New email tech" preference.}, + "b", + 0); + + +DefParam("newchangedmail", +q{The same as 'changedmail', but used for the newemailtech stuff.}, + "l", +"From: bugzilla-daemon +To: %to% +Cc: %cc% +Subject: [Bug %bugid%] %neworchanged% - %summary% + +%urlbase%show_bug.cgi?id=%bugid% + +%diffs%"); + DefParam("whinedays", @@ -326,7 +456,7 @@ DefParam("webdotbase", DefParam("entryheaderhtml", "This is a special header for the bug entry page. The text will be printed after the page header, before the bug entry form. It is meant to be a place to put pointers to intructions on how to enter bugs.", "l", - ''); + '<A HREF="bugwritinghelp.html">Bug writing guidelines</A>'); DefParam("expectbigqueries", "If this is on, then we will tell mysql to <tt>set option SQL_BIG_TABLES=1</tt> before doing queries on bugs. This will be a little slower, but one will not get the error <tt>The table ### is full</tt> for big queries that require a big temporary table.", @@ -334,7 +464,7 @@ DefParam("expectbigqueries", 0); DefParam("emailregexp", - 'This defines the regexp to use for legal email addresses. The default tries to match fully qualified email addresses. Another popular value to put here is <tt>^[^@, ]$</tt>, which means "local usernames, no @ allowed.', + 'This defines the regexp to use for legal email addresses. The default tries to match fully qualified email addresses. Another popular value to put here is <tt>^[^@, ]*$</tt>, which means "local usernames, no @ allowed.', "t", q:^[^@, ]*@[^@, ]*\\.[^@, ]*$:); @@ -350,7 +480,7 @@ DefParam("emailsuffix", DefParam("voteremovedmail", -q{This is a mail message to send to anyone who gets a vote removed from a bug for any reason. %to% gets replaced by a comma-separated list of people who used to be voting for this bug. %bugid% gets replaced by the bug number. %reason% gets replaced by a short reason describing why the vote was removed. %<i>anythingelse</i>% gets replaced by the definition of thatparameter (as defined on this page).}, +q{This is a mail message to send to anyone who gets a vote removed from a bug for any reason. %to% gets replaced by a comma-separated list of people who used to be voting for this bug. %bugid% gets replaced by the bug number. %reason% gets replaced by a short reason describing why the vote was removed. %count% is how many votes got removed.%<i>anythingelse</i>% gets replaced by the definition of that parameter (as defined on this page).}, "l", "From: bugzilla-daemon To: %to% @@ -360,6 +490,8 @@ You used to have a vote on bug %bugid%, but it has been removed. Reason: %reason% +Votes removed: %count% + %urlbase%show_bug.cgi?id=%bugid% "); @@ -369,6 +501,59 @@ DefParam("allowbugdeletion", 0); +DefParam("allowuserdeletion", + q{The pages to edit users can also let you delete a user. But there is no code that goes and cleans up any references to that user in other tables, so such deletions are kinda scary. So, you have to turn on this option before any such deletions will ever happen.}, + "b", + 0); + +DefParam("strictvaluechecks", + "Do stricter integrity checking on both form submission values and values read in from the database.", + "b", + 0); + + +DefParam("browserbugmessage", + "If strictvaluechecks is on, and the bugzilla gets unexpected data from the browser, in addition to displaying the cause of the problem, it will output this HTML as well.", + "l", + "this may indicate a bug in your browser.\n"); + +# +# Parameters to force users to comment their changes for different actions. +DefParam("commentonaccept", + "If this option is on, the user needs to enter a short comment if he accepts the bug", + "b", 0 ); +DefParam("commentonclearresolution", + "If this option is on, the user needs to enter a short comment if the bugs resolution is cleared", + "b", 0 ); +DefParam("commentonconfirm", + "If this option is on, the user needs to enter a short comment when confirming a bug", + "b", 0 ); +DefParam("commentonresolve", + "If this option is on, the user needs to enter a short comment if the bug is resolved", + "b", 0 ); +DefParam("commentonreassign", + "If this option is on, the user needs to enter a short comment if the bug is reassigned", + "b", 0 ); +DefParam("commentonreassignbycomponent", + "If this option is on, the user needs to enter a short comment if the bug is reassigned by component", + "b", 0 ); +DefParam("commentonreopen", + "If this option is on, the user needs to enter a short comment if the bug is reopened", + "b", 0 ); +DefParam("commentonverify", + "If this option is on, the user needs to enter a short comment if the bug is verified", + "b", 0 ); +DefParam("commentonclose", + "If this option is on, the user needs to enter a short comment if the bug is closed", + "b", 0 ); +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/describecomponents.cgi b/describecomponents.cgi index 4aa41aa3c138d9b8bbbee60bd600c3103ad1d51b..e1b646f51986bfd7c612b527dab1a273d85f1fbb 100755 --- a/describecomponents.cgi +++ b/describecomponents.cgi @@ -49,6 +49,7 @@ Product: <SELECT NAME=product> <INPUT TYPE=\"submit\" VALUE=\"Submit\"> </FORM> "; + PutFooter(); exit; } @@ -95,3 +96,5 @@ while (MoreSQLData()) { } print "<tr><td colspan=$cols><hr></td></tr></table>\n"; + +PutFooter(); diff --git a/describekeywords.cgi b/describekeywords.cgi new file mode 100755 index 0000000000000000000000000000000000000000..c80158267605c16a4fa39c6e4c3cc70e7a2bd5ca --- /dev/null +++ b/describekeywords.cgi @@ -0,0 +1,88 @@ +#!/usr/bonsaitools/bin/perl -w +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Bug Tracking System. +# +# The Initial Developer of the Original Code is Terry Weissman. +# Portions created by Terry Weissman are +# Copyright (C) 2000 Terry Weissman. All +# Rights Reserved. +# +# Contributor(s): Terry Weissman <terry@mozilla.org> + +use diagnostics; +use strict; + +require "CGI.pl"; + +ConnectToDatabase(); + +print "Content-type: text/html\n\n"; + +PutHeader("Bugzilla keyword description"); + +my $tableheader = qq{ +<TABLE BORDER=1 CELLPADDING=4 CELLSPACING=0> +<TR BGCOLOR="#6666FF"> +<TH ALIGN="left">Name</TH> +<TH ALIGN="left">Description</TH> +<TH ALIGN="left">Bugs</TH> +</TR> +}; + +print $tableheader; +my $line_count = 0; +my $max_table_size = 50; + +SendSQL("SELECT keyworddefs.name, keyworddefs.description, + COUNT(keywords.bug_id), keywords.bug_id + FROM keyworddefs LEFT JOIN keywords ON keyworddefs.id=keywords.keywordid + GROUP BY keyworddefs.id + ORDER BY keyworddefs.name"); + +while (MoreSQLData()) { + my ($name, $description, $bugs, $onebug) = FetchSQLData(); + if ($bugs && $onebug) { + # This 'onebug' stuff is silly hackery for old versions of + # MySQL that seem to return a count() of 1 even if there are + # no matching. So, we ask for an actual bug number. If it + # can't find any bugs that match the keyword, then we set the + # count to be zero, ignoring what it had responded. + my $q = url_quote($name); + $bugs = qq{<A HREF="buglist.cgi?keywords=$q">$bugs</A>}; + } else { + $bugs = "none"; + } + if ($line_count == $max_table_size) { + print "</table>\n$tableheader"; + $line_count = 0; + } + $line_count++; + print qq{ +<TR> +<TH>$name</TH> +<TD>$description</TD> +<TD ALIGN="right">$bugs</TD> +</TR> +}; +} + +print "</TABLE><P>\n"; + +quietly_check_login(); + +if (UserInGroup("editkeywords")) { + print "<p><a href=editkeywords.cgi>Edit keywords</a><p>\n"; +} + +PutFooter(); diff --git a/doaddcomponent.cgi b/doaddcomponent.cgi deleted file mode 100755 index b26b4a1b01cf1c7e1dbfcb353d1e1d1f93521474..0000000000000000000000000000000000000000 --- a/doaddcomponent.cgi +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bonsaitools/bin/perl -w -# -*- Mode: perl; indent-tabs-mode: nil -*- -# -# The contents of this file are subject to the Mozilla Public -# License Version 1.1 (the "License"); you may not use this file -# except in compliance with the License. You may obtain a copy of -# the License at http://www.mozilla.org/MPL/ -# -# Software distributed under the License is distributed on an "AS -# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or -# implied. See the License for the specific language governing -# rights and limitations under the License. -# -# The Original Code is the Bugzilla Bug Tracking System. -# -# The Initial Developer of the Original Code is Netscape Communications -# Corporation. Portions created by Netscape are -# Copyright (C) 1998 Netscape Communications Corporation. All -# Rights Reserved. -# -# Contributor(s): Sam Ziegler <sam@ziegler.org> -# Terry Weissman <terry@mozilla.org> -# Mark Hamby <mhamby@logicon.com> - -# Code derived from doeditcomponents.cgi - - -use diagnostics; -use strict; - -require "CGI.pl"; - -confirm_login(); - -print "Content-type: text/html\n\n"; - -# foreach my $i (sort(keys %::FORM)) { -# print value_quote("$i $::FORM{$i}") . "<BR>\n"; -# } - -if (!UserInGroup("editcomponents")) { - print "<H1>Sorry, you aren't a member of the 'editcomponents' group.</H1>\n"; - print "And so, you aren't allowed to add components.\n"; - exit; -} - - -PutHeader("Adding new component"); - -unlink "data/versioncache"; -GetVersionTable(); - -my $component = trim($::FORM{"component"}); -my $product = trim($::FORM{"product"}); -my $description = trim($::FORM{"description"}); -my $initialowner = trim($::FORM{"initialowner"}); - -if (!defined $::FORM{"initialqacontact"}) { - # May not be defined if we're not using this field. - $::FORM{'initialqacontact'} = ""; -} -my $initialqacontact = trim($::FORM{"initialqacontact"}); - -if ($component eq "") { - print "You must enter a name for the new component. Please press\n"; - print "<b>Back</b> and try again.\n"; - exit; -} - -# Check to ensure the component doesn't exist already. -SendSQL("SELECT value FROM components WHERE " . - "program = " . SqlQuote($product) . " and " . - "value = " . SqlQuote($component)); -my @row = FetchSQLData(); -if (@row) { - print "<H1>Component already exists</H1>"; - print "The component '$component' already exists\n"; - print "for product '$product'.<P>\n"; - print "<p><a href=query.cgi>Go back to the query page</a>\n"; - exit; -} - -# Check that the email addresses are legitimate. -foreach my $addr ($initialowner, $initialqacontact) { - if ($addr ne "") { - DBNameToIdAndCheck($addr); - } -} - -# Add the new component. -SendSQL("INSERT INTO components ( " . - "value, program, description, initialowner, initialqacontact" . - " ) VALUES ( " . - SqlQuote($component) . "," . - SqlQuote($product) . "," . - SqlQuote($description) . "," . - SqlQuote($initialowner) . "," . - SqlQuote($initialqacontact) . ")" ); - -unlink "data/versioncache"; - -print "OK, done.<p>\n"; -print "<a href=addcomponent.cgi>Edit another new component.</a><p>\n"; -print "<a href=editcomponents.cgi>Edit existing components.</a><p>\n"; -print "<a href=query.cgi>Go back to the query page.</a>\n"; diff --git a/docs/CVS/Entries b/docs/CVS/Entries new file mode 100644 index 0000000000000000000000000000000000000000..178481050188cf00d7d9cd5a11e43ab8fab9294f --- /dev/null +++ b/docs/CVS/Entries @@ -0,0 +1 @@ +D diff --git a/docs/CVS/Repository b/docs/CVS/Repository new file mode 100644 index 0000000000000000000000000000000000000000..f4750cc31dd4f46d94229a7b7163a17d391a05df --- /dev/null +++ b/docs/CVS/Repository @@ -0,0 +1 @@ +mozilla/webtools/bugzilla/docs diff --git a/docs/CVS/Root b/docs/CVS/Root new file mode 100644 index 0000000000000000000000000000000000000000..cdb6f4a0739a0dc53e628026726036377dec3637 --- /dev/null +++ b/docs/CVS/Root @@ -0,0 +1 @@ +:pserver:anonymous@cvs-mirror.mozilla.org:/cvsroot diff --git a/doeditcomponents.cgi b/doeditcomponents.cgi deleted file mode 100755 index cac248db9e6d0601520d0e0a842f5b34d8e52671..0000000000000000000000000000000000000000 --- a/doeditcomponents.cgi +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bonsaitools/bin/perl -w -# -*- Mode: perl; indent-tabs-mode: nil -*- -# -# The contents of this file are subject to the Mozilla Public -# License Version 1.1 (the "License"); you may not use this file -# except in compliance with the License. You may obtain a copy of -# the License at http://www.mozilla.org/MPL/ -# -# Software distributed under the License is distributed on an "AS -# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or -# implied. See the License for the specific language governing -# rights and limitations under the License. -# -# The Original Code is the Bugzilla Bug Tracking System. -# -# The Initial Developer of the Original Code is Netscape Communications -# Corporation. Portions created by Netscape are -# Copyright (C) 1998 Netscape Communications Corporation. All -# Rights Reserved. -# -# Contributor(s): Sam Ziegler <sam@ziegler.org> -# Terry Weissman <terry@mozilla.org> - -# Code derived from doeditowners.cgi - - -use diagnostics; -use strict; - -require "CGI.pl"; - - -# Shut up misguided -w warnings about "used only once": - -use vars @::legal_product; - - -confirm_login(); - -print "Content-type: text/html\n\n"; - -# foreach my $i (sort(keys %::FORM)) { -# print value_quote("$i $::FORM{$i}") . "<BR>\n"; -# } - -if (!UserInGroup("editcomponents")) { - print "<H1>Sorry, you aren't a member of the 'editcomponents' group.</H1>\n"; - print "And so, you aren't allowed to edit the owners.\n"; - exit; -} - - -sub Check { - my ($code1, $code2) = (@_); - if ($code1 ne $code2) { - print "<H1>A race error has occurred.</H1>"; - print "It appears that someone else has been changing the database\n"; - print "while you've been editing it. I'm afraid you will have to\n"; - print "start all over. Sorry! <P>\n"; - print "<p><a href=query.cgi>Go back to the query page</a>\n"; - exit; - } -} - - -my @cmds; - -sub DoOne { - my ($oldvalue, $field, $where, $checkemail) = (@_); - if (!defined $::FORM{$field}) { - print "ERROR -- $field not defined!"; - exit; - } - if ($oldvalue ne $::FORM{$field}) { - my $name = $field; - $name =~ s/^.*-//; - my $table = "products"; - if ($field =~ /^P\d+-C\d+-/) { - $table = "components"; - } - push @cmds, "update $table set $name=" . - SqlQuote($::FORM{$field}) . " where $where"; - print "Changed $name for $where <P>"; - if ($checkemail) { - DBNameToIdAndCheck($::FORM{$field}); - } - } -} - - - -PutHeader("Saving new component info"); - -unlink "data/versioncache"; -GetVersionTable(); - -my $prodcode = "P000"; - -foreach my $product (@::legal_product) { - SendSQL("select description, milestoneurl, disallownew, votesperuser from products where product='$product'"); - my @row = FetchSQLData(); - if (!@row) { - next; - } - my ($description, $milestoneurl, $disallownew, $votesperuser) = (@row); - $prodcode++; - Check($product, $::FORM{"prodcode-$prodcode"}); - - my $where = "product=" . SqlQuote($product); - DoOne($description, "$prodcode-description", $where); - if (Param('usetargetmilestone')) { - DoOne($milestoneurl, "$prodcode-milestoneurl", $where); - } - DoOne($disallownew, "$prodcode-disallownew", $where); - DoOne($votesperuser, "$prodcode-votesperuser", $where); - - SendSQL("select value, initialowner, initialqacontact, description from components where program=" . SqlQuote($product) . " order by value"); - my $c = 0; - while (my @row = FetchSQLData()) { - my ($component, $initialowner, $initialqacontact, $description) = - (@row); - $c++; - my $compcode = $prodcode . "-" . "C$c"; - - Check($component, $::FORM{"compcode-$compcode"}); - - my $where = "program=" . SqlQuote($product) . " and value=" . - SqlQuote($component); - - DoOne($initialowner, "$compcode-initialowner", $where, 1); - if (Param('useqacontact')) { - DoOne($initialqacontact, "$compcode-initialqacontact", $where, - 1); - } - DoOne($description, "$compcode-description", $where); - } - -} - -print "Saving changes.<P>\n"; - -foreach my $cmd (@cmds) { - print "$cmd <BR>"; - SendSQL($cmd); -} - -unlink "data/versioncache"; - -print "OK, done.<p>\n"; -print "<a href=editcomponents.cgi>Edit the components some more.</a><p>\n"; -print "<a href=query.cgi>Go back to the query page.</a>\n"; diff --git a/doeditowners.cgi b/doeditowners.cgi deleted file mode 100755 index c1aefc5d2a7b196e60dd359e6af98047263c72d7..0000000000000000000000000000000000000000 --- a/doeditowners.cgi +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bonsaitools/bin/perl -w -# -*- Mode: perl; indent-tabs-mode: nil -*- -# -# The contents of this file are subject to the Mozilla Public -# License Version 1.1 (the "License"); you may not use this file -# except in compliance with the License. You may obtain a copy of -# the License at http://www.mozilla.org/MPL/ -# -# Software distributed under the License is distributed on an "AS -# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or -# implied. See the License for the specific language governing -# rights and limitations under the License. -# -# The Original Code is the Bugzilla Bug Tracking System. -# -# The Initial Developer of the Original Code is Netscape Communications -# Corporation. Portions created by Netscape are -# Copyright (C) 1998 Netscape Communications Corporation. All -# Rights Reserved. -# -# Contributor(s): Sam Ziegler <sam@ziegler.org> - -use diagnostics; -use strict; - -require "CGI.pl"; - -confirm_login(); - -print "Content-type: text/html\n\n"; - -if (!UserInGroup("editcomponents")) { - print "<H1>Sorry, you aren't a member of the 'editcomponents' group.</H1>\n"; - print "And so, you aren't allowed to edit the owners.\n"; - exit; -} - - -PutHeader("Saving new owners"); - -SendSQL("select program, value, initialowner from components order by program, value"); - -my @line; - -foreach my $key (keys(%::FORM)) { - $::FORM{url_decode($key)} = $::FORM{$key}; -} - -my @updates; -my $curIndex = 0; - -while (@line = FetchSQLData()) { - my $curItem = "$line[0]_$line[1]"; - if (exists $::FORM{$curItem}) { - $::FORM{$curItem} =~ s/\r\n/\n/; - if ($::FORM{$curItem} ne $line[2]) { - print "$line[0] : $line[1] is now owned by $::FORM{$curItem}.<BR>\n"; - $updates[$curIndex++] = "update components set initialowner = '$::FORM{$curItem}' where program = '$line[0]' and value = '$line[1]'"; - } - } -} - -foreach my $update (@updates) { - SendSQL($update); -} - -print "OK, done.<p>\n"; -print "<a href=editowners.cgi>Edit the owners some more.</a><p>\n"; -print "<a href=query.cgi>Go back to the query page.</a>\n"; diff --git a/doeditparams.cgi b/doeditparams.cgi index 5f4eb9e1a2e33fbf770607948fc4a50daf7a52f9..d37bb042a4fa19c34532a0e3c92caf8d15fb82ee 100755 --- a/doeditparams.cgi +++ b/doeditparams.cgi @@ -39,11 +39,12 @@ print "Content-type: text/html\n\n"; if (!UserInGroup("tweakparams")) { print "<H1>Sorry, you aren't a member of the 'tweakparams' group.</H1>\n"; print "And so, you aren't allowed to edit the parameters.\n"; + PutFooter(); exit; } -PutHeader("Saving new parameters"); +PutHeader("Saving new parameters", undef, undef, undef, 1); foreach my $i (@::param_list) { # print "Processing $i...<BR>\n"; @@ -58,6 +59,7 @@ foreach my $i (@::param_list) { if ($ok ne "") { print "New value for $i is invalid: $ok<p>\n"; print "Please hit <b>Back</b> and try again.\n"; + PutFooter(); exit; } } @@ -70,8 +72,12 @@ foreach my $i (@::param_list) { WriteParams(); unlink "data/versioncache"; +print "<PRE>"; +system("./syncshadowdb -v"); +print "</PRE>"; print "OK, done.<p>\n"; print "<a href=editparams.cgi>Edit the params some more.</a><p>\n"; print "<a href=query.cgi>Go back to the query page.</a>\n"; +PutFooter(); diff --git a/doeditvotes.cgi b/doeditvotes.cgi index 5147807299ca6df72299c604220556741a355fa3..3902f91188e7dc25a330609a8d3b10f8463aa1b8 100755 --- a/doeditvotes.cgi +++ b/doeditvotes.cgi @@ -39,7 +39,7 @@ if ($who ne $::FORM{'who'}) { print "The login info got confused. If you want to adjust the votes\n"; print "for <tt>$::COOKIE{'Bugzilla_login'}</tt>, then please\n"; print "<a href=showvotes.cgi?user=$who>click here</a>.<hr>\n"; - navigation_header(); + PutFooter(); exit(); } @@ -48,7 +48,7 @@ my @buglist = grep {/^\d+$/} keys(%::FORM); if (0 == @buglist) { PutHeader("Oops?"); print "Something got confused. Please click <b>Back</b> and try again."; - navigation_header(); + PutFooter(); exit(); } @@ -58,22 +58,32 @@ foreach my $id (@buglist) { PutHeader("Numbers only, please"); print "Only use numeric values for your bug votes.\n"; print "Please click <b>Back</b> and try again.<hr>\n"; - navigation_header(); + PutFooter(); exit(); } } -SendSQL("select bug_id, product from bugs where bug_id = " . - join(" or bug_id = ", @buglist)); +SendSQL("SELECT bugs.bug_id, bugs.product, products.maxvotesperbug " . + "FROM bugs, products " . + "WHERE products.product = bugs.product ". + " AND bugs.bug_id IN (" . join(", ", @buglist) . ")"); my %prodcount; while (MoreSQLData()) { - my ($id, $prod) = (FetchSQLData()); + my ($id, $prod, $max) = (FetchSQLData()); if (!defined $prodcount{$prod}) { $prodcount{$prod} = 0; } $prodcount{$prod} += $::FORM{$id}; + if ($::FORM{$id} > $max) { + PutHeader("Don't overstuff!", "Illegal vote"); + print "You may only use at most $max votes for a single bug in the\n"; + print "<tt>$prod</tt> product, but you are using $::FORM{$id}.\n"; + print "<P>Please click <b>Back</b> and try again.<hr>\n"; + PutFooter(); + exit(); + } } foreach my $prod (keys(%prodcount)) { @@ -81,8 +91,8 @@ foreach my $prod (keys(%prodcount)) { PutHeader("Don't overstuff!", "Illegal vote"); print "You may only use $::prodmaxvotes{$prod} votes for bugs in the\n"; print "<tt>$prod</tt> product, but you are using $prodcount{$prod}.\n"; - print "Please click <b>Back</b> and try again.<hr>\n"; - navigation_header(); + print "<P>Please click <b>Back</b> and try again.<hr>\n"; + PutFooter(); exit(); } } @@ -110,11 +120,13 @@ foreach my $id (keys %affected) { SendSQL("unlock tables"); - PutHeader("Voting tabulated", "Voting tabulated", $::COOKIE{'Bugzilla_login'}); print "Your votes have been recorded.\n"; print qq{<p><a href="showvotes.cgi?user=$who">Review your votes</a><hr>\n}; -navigation_header(); +foreach my $id (keys %affected) { + CheckIfVotedConfirmed($id, $who); +} +PutFooter(); exit(); diff --git a/editcomponents.cgi b/editcomponents.cgi index 1c45a311a4cafcacbf2b039ae1561c00603974ee..d9ff666c4a623a80dfe5bf5c2bf366ace1d05c32 100755 --- a/editcomponents.cgi +++ b/editcomponents.cgi @@ -171,7 +171,7 @@ sub PutTrailer (@) } $num++; } - print "</BODY>\n</HTML>\n"; + PutFooter(); } @@ -358,7 +358,7 @@ if ($action eq 'add') { # if ($action eq 'new') { - PutHeader("Adding new product"); + PutHeader("Adding new component"); CheckProduct($product); # Cleanups and valididy checks @@ -481,10 +481,9 @@ if ($action eq 'del') { print " <TD VALIGN=\"top\">Initial QA contact:</TD>\n"; print " <TD VALIGN=\"top\">$initialqacontact</TD>"; } - SendSQL("SELECT count(bug_id),product,component + SendSQL("SELECT count(bug_id) FROM bugs - GROUP BY product - HAVING product=" . SqlQuote($product) . " + WHERE product=" . SqlQuote($product) . " AND component=" . SqlQuote($component)); print "</TR><TR>\n"; @@ -568,29 +567,34 @@ if ($action eq 'delete') { # so I have to iterate over bugs and delete all the indivial entries # in bugs_activies and attachments. - SendSQL("SELECT bug_id + if (Param("allowbugdeletion")) { + SendSQL("SELECT bug_id FROM bugs WHERE product=" . SqlQuote($product) . " AND component=" . SqlQuote($component)); - while (MoreSQLData()) { - my $bugid = FetchOneColumn(); + while (MoreSQLData()) { + my $bugid = FetchOneColumn(); - my $query = $::db->query("DELETE FROM attachments WHERE bug_id=$bugid") + my $query = + $::db->query("DELETE FROM attachments WHERE bug_id=$bugid") or die "$::db_errstr"; - $query = $::db->query("DELETE FROM bugs_activity WHERE bug_id=$bugid") + $query = + $::db->query("DELETE FROM bugs_activity WHERE bug_id=$bugid") or die "$::db_errstr"; - $query = $::db->query("DELETE FROM dependencies WHERE blocked=$bugid") + $query = + $::db->query("DELETE FROM dependencies WHERE blocked=$bugid") or die "$::db_errstr"; - } - print "Attachments, bug activity and dependencies deleted.<BR>\n"; + } + print "Attachments, bug activity and dependencies deleted.<BR>\n"; - # Deleting the rest is easier: + # Deleting the rest is easier: - SendSQL("DELETE FROM bugs + SendSQL("DELETE FROM bugs WHERE product=" . SqlQuote($product) . " AND component=" . SqlQuote($component)); - print "Bugs deleted.<BR>\n"; + print "Bugs deleted.<BR>\n"; + } SendSQL("DELETE FROM components WHERE program=" . SqlQuote($product) . " diff --git a/editgroups.cgi b/editgroups.cgi new file mode 100755 index 0000000000000000000000000000000000000000..1f329d8ac8a536a610b25383e3e49e5be1413813 --- /dev/null +++ b/editgroups.cgi @@ -0,0 +1,470 @@ +#!/usr/bonsaitools/bin/perl -w +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Bug Tracking System. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are +# Copyright (C) 1998 Netscape Communications Corporation. All +# Rights Reserved. +# +# Contributor(s): Dave Miller <dave@intrec.com> + +# Code derived from editowners.cgi and editusers.cgi + +use diagnostics; +use strict; + +require "CGI.pl"; + +confirm_login(); + +print "Content-type: text/html\n\n"; + +if (!UserInGroup("creategroups")) { + PutHeader("Not Authorized","Edit Groups","","Not Authorized for this function!"); + print "<H1>Sorry, you aren't a member of the 'creategroups' group.</H1>\n"; + print "And so, you aren't allowed to edit the groups.\n"; + print "<p>\n"; + PutFooter(); + exit; +} + +my $action = trim($::FORM{action} || ''); + +# TestGroup: check if the group name exists +sub TestGroup ($) +{ + my $group = shift; + + # does the group exist? + SendSQL("SELECT name + FROM groups + WHERE name=" . SqlQuote($group)); + return FetchOneColumn(); +} + +sub ShowError ($) +{ + my $msgtext = shift; + print "<TABLE BGCOLOR=\"#FF0000\" CELLPADDING=15><TR><TD>"; + print "<B>$msgtext</B>"; + print "</TD></TR></TABLE><P>"; + return 1; +} + +# +# Displays a text like "a.", "a or b.", "a, b or c.", "a, b, c or d." +# + +sub PutTrailer (@) +{ + my (@links) = ("<a href=index.html>Back to the Main Bugs Page</a>", @_); + + my $count = $#links; + my $num = 0; + print "<P>\n"; + foreach (@links) { + print $_; + if ($num == $count) { + print ".\n"; + } + elsif ($num == $count-1) { + print " or "; + } + else { + print ", "; + } + $num++; + } + PutFooter(); +} + +# +# action='' -> No action specified, get a list. +# + +unless ($action) { + PutHeader("Edit Groups","Edit Groups","This lets you edit the groups available to put users in."); + + print "<form method=post action=editgroups.cgi>\n"; + print "<table border=1>\n"; + print "<tr>"; + print "<th>Bit</th>"; + print "<th>Name</th>"; + print "<th>Description</th>"; + print "<th>User RegExp</th>"; + print "<th>Action</th>"; + print "</tr>\n"; + + SendSQL("SELECT bit,name,description,userregexp " . + "FROM groups " . + "WHERE isbuggroup != 0 " . + "ORDER BY bit"); + + while (MoreSQLData()) { + my ($bit, $name, $desc, $regexp) = FetchSQLData(); + print "<tr>\n"; + print "<td valign=middle>$bit</td>\n"; + print "<td><input size=20 name=\"name-$bit\" value=\"$name\">\n"; + print "<input type=hidden name=\"oldname-$bit\" value=\"$name\"></td>\n"; + print "<td><input size=40 name=\"desc-$bit\" value=\"$desc\">\n"; + print "<input type=hidden name=\"olddesc-$bit\" value=\"$desc\"></td>\n"; + print "<td><input size=30 name=\"regexp-$bit\" value=\"$regexp\">\n"; + print "<input type=hidden name=\"oldregexp-$bit\" value=\"$regexp\"></td>\n"; + print "<td align=center valign=middle><a href=\"editgroups.cgi?action=del&group=$bit\">Delete</a></td>\n"; + print "</tr>\n"; + } + + print "<tr>\n"; + print "<td colspan=4></td>\n"; + print "<td><a href=\"editgroups.cgi?action=add\">Add Group</a></td>\n"; + print "</tr>\n"; + print "</table>\n"; + print "<input type=hidden name=\"action\" value=\"update\">"; + print "<input type=submit value=\"Submit changes\">\n"; + print "</form>\n"; + + print "<p>"; + print "<b>Name</b> is what is used with the UserInGroup() function in any +customized cgi files you write that use a given group. It can also be used by +people submitting bugs by email to limit a bug to a certain groupset. It +may not contain any spaces.<p>"; + print "<b>Description</b> is what will be shown in the bug reports to +members of the group where they can choose whether the bug will be restricted +to others in the same group.<p>"; + print "<b>User RegExp</b> is optional, and if filled in, will automatically +grant membership to this group to anyone creating a new account with an +email address that matches this regular expression.<p>"; + print "In addition, the following groups that determine user privileges +exist. You can not edit these, but you need to know they are here, because +you can't duplicate the Names of any of them in your user groups either.<p>"; + + print "<table border=1>\n"; + print "<tr>"; + print "<th>Bit</th>"; + print "<th>Name</th>"; + print "<th>Description</th>"; + print "</tr>\n"; + + SendSQL("SELECT bit,name,description " . + "FROM groups " . + "WHERE isbuggroup = 0 " . + "ORDER BY bit"); + + while (MoreSQLData()) { + my ($bit, $name, $desc) = FetchSQLData(); + print "<tr>\n"; + print "<td>$bit</td>\n"; + print "<td>$name</td>\n"; + print "<td>$desc</td>\n"; + print "</tr>\n"; + } + + print "</table><p>\n"; + + PutFooter(); + exit; +} + +# +# action='add' -> present form for parameters for new group +# +# (next action will be 'new') +# + +if ($action eq 'add') { + PutHeader("Add group"); + + print "<FORM METHOD=POST ACTION=editgroups.cgi>\n"; + print "<TABLE BORDER=1 CELLPADDING=4 CELLSPACING=0><TR>\n"; + print "<th>New Name</th>"; + print "<th>New Description</th>"; + print "<th>New User RegExp</th>"; + print "</tr><tr>"; + print "<td><input size=20 name=\"name\"></td>\n"; + print "<td><input size=40 name=\"desc\"></td>\n"; + print "<td><input size=30 name=\"regexp\"></td>\n"; + print "</TR></TABLE>\n<HR>\n"; + print "<INPUT TYPE=SUBMIT VALUE=\"Add\">\n"; + print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"new\">\n"; + print "</FORM>"; + + print "<p>"; + print "<b>Name</b> is what is used with the UserInGroup() function in any +customized cgi files you write that use a given group. It can also be used by +people submitting bugs by email to limit a bug to a certain groupset. It +may not contain any spaces.<p>"; + print "<b>Description</b> is what will be shown in the bug reports to +members of the group where they can choose whether the bug will be restricted +to others in the same group.<p>"; + print "<b>User RegExp</b> is optional, and if filled in, will automatically +grant membership to this group to anyone creating a new account with an +email address that matches this regular expression.<p>"; + + PutTrailer("<a href=editgroups.cgi>Back to the group list</a>"); + exit; +} + + + +# +# action='new' -> add group entered in the 'action=add' screen +# + +if ($action eq 'new') { + PutHeader("Adding new group"); + + # Cleanups and valididy checks + my $name = trim($::FORM{name} || ''); + my $desc = trim($::FORM{desc} || ''); + my $regexp = trim($::FORM{regexp} || ''); + + unless ($name) { + ShowError("You must enter a name for the new group.<BR>" . + "Please click the <b>Back</b> button and try again."); + PutFooter(); + exit; + } + unless ($desc) { + ShowError("You must enter a description for the new group.<BR>" . + "Please click the <b>Back</b> button and try again."); + PutFooter(); + exit; + } + if (TestGroup($name)) { + ShowError("The group '" . $name . "' already exists.<BR>" . + "Please click the <b>Back</b> button and try again."); + PutFooter(); + exit; + } + + # Major hack for bit values... perl can't handle 64-bit ints, so I can't + # just do the math to get the next available bit number, gotta handle + # them as strings... also, we're actually only going to allow 63 bits + # because that's all that opblessgroupset masks for (the high bit is off + # to avoid signing issues). + + my @bitvals = ('1','2','4','8','16','32','64','128','256','512','1024', + '2048','4096','8192','16384','32768', + + '65535','131072','262144','524288','1048576','2097152', + '4194304','8388608','16777216','33554432','67108864', + '134217728','268435456','536870912','1073741824', + '2147483648', + + '4294967296','8589934592','17179869184','34359738368', + '68719476736','137438953472','274877906944', + '549755813888','1099511627776','2199023255552', + '4398046511104','8796093022208','17592186044416', + '35184372088832','70368744177664','140737488355328', + + '281474976710656','562949953421312','1125899906842624', + '2251799813685248','4503599627370496','9007199254740992', + '18014398509481984','36028797018963968','72057594037927936', + '144115188075855872','288230376151711744', + '576460752303423488','1152921504606846976', + '2305843009213693958','4611686018427387916'); + + # First the next available bit + my $bit = ""; + foreach (@bitvals) { + if ($bit == "") { + SendSQL("SELECT bit FROM groups WHERE bit=" . SqlQuote($_)); + if (!FetchOneColumn()) { $bit = $_; } + } + } + if ($bit == "") { + ShowError("Sorry, you already have the maximum number of groups " . + "defined.<BR><BR>You must delete a group first before you " . + "can add any more.</B>"); + PutTrailer("<a href=editgroups.cgi>Back to the group list</a>"); + exit; + } + + # Add the new group + SendSQL("INSERT INTO groups ( " . + "bit, name, description, isbuggroup, userregexp" . + " ) VALUES ( " . + $bit . "," . + SqlQuote($name) . "," . + SqlQuote($desc) . "," . + "1," . + SqlQuote($regexp) . ")" ); + + print "OK, done.<p>\n"; + print "Your new group was assigned bit #$bit.<p>"; + PutTrailer("<a href=\"editgroups.cgi?action=add\">Add another group</a>", + "<a href=\"editgroups.cgi\">Back to the group list</a>"); + exit; +} + +# +# action='del' -> ask if user really wants to delete +# +# (next action would be 'delete') +# + +if ($action eq 'del') { + PutHeader("Delete group"); + my $bit = trim($::FORM{group} || ''); + unless ($bit) { + ShowError("No group specified.<BR>" . + "Click the <b>Back</b> button and try again."); + PutFooter(); + exit; + } + SendSQL("SELECT bit FROM groups WHERE bit=" . SqlQuote($bit)); + if (!FetchOneColumn()) { + ShowError("That group doesn't exist.<BR>" . + "Click the <b>Back</b> button and try again."); + PutFooter(); + exit; + } + SendSQL("SELECT name,description " . + "FROM groups " . + "WHERE bit = " . SqlQuote($bit)); + + my ($name, $desc) = FetchSQLData(); + print "<table border=1>\n"; + print "<tr>"; + print "<th>Bit</th>"; + print "<th>Name</th>"; + print "<th>Description</th>"; + print "</tr>\n"; + print "<tr>\n"; + print "<td>$bit</td>\n"; + print "<td>$name</td>\n"; + print "<td>$desc</td>\n"; + print "</tr>\n"; + print "</table>\n"; + + print "<H2>Confirmation</H2>\n"; + print "<P>Do you really want to delete this group?<P>\n"; + + print "<FORM METHOD=POST ACTION=editgroups.cgi>\n"; + print "<INPUT TYPE=SUBMIT VALUE=\"Yes, delete\">\n"; + print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"delete\">\n"; + print "<INPUT TYPE=HIDDEN NAME=\"group\" VALUE=\"$bit\">\n"; + print "</FORM>"; + + PutTrailer("<a href=editgroups.cgi>No, go back to the group list</a>"); + exit; +} + +# +# action='delete' -> really delete the group +# + +if ($action eq 'delete') { + PutHeader("Deleting user"); + ShowError("This function has not been implemented yet! (Sorry)<br>" . + "Try again later"); + + print " +Deleting a group is not as easy as it sounds:<p> +<OL> +<LI>All users have to be checked to ensure anyone who is a member of this group is first removed from membership. +<LI>All bugs have to be checked to ensure no bugs are set to use this group. +</OL> +If the above is not done, conflicts may occur if a new group is created that uses a bit number that has already been used in the past.<p> +Deleting a group will be implemented very shortly, stay tuned! +I just figured most people would be more interested in adding and editing +groups for the time being, so I would get that done first, so I could get this out here for people to use. :)<p> +Watch <a href=\"http://bugzilla.mozilla.org/show_bug.cgi?id=25010\">Bug 25010</a> on Mozilla's bugzilla for details. +<p> +"; + + PutTrailer("<a href=editgroups.cgi>Back to group list</a>"); + exit; +} + +# +# action='update' -> update the groups +# + +if ($action eq 'update') { + PutHeader("Updating groups"); + + my $chgs = 0; + + foreach my $b (grep(/^name-\d*$/, keys %::FORM)) { + if ($::FORM{$b}) { + my $v = substr($b, 5); + +# print "Old: '" . $::FORM{"oldname-$v"} . "', '" . $::FORM{"olddesc-$v"} . +# "', '" . $::FORM{"oldregexp-$v"} . "'<br>"; +# print "New: '" . $::FORM{"name-$v"} . "', '" . $::FORM{"desc-$v"} . +# "', '" . $::FORM{"regexp-$v"} . "'<br>"; + + if ($::FORM{"oldname-$v"} ne $::FORM{"name-$v"}) { + $chgs = 1; + SendSQL("SELECT name FROM groups WHERE name=" . + SqlQuote($::FORM{"name-$v"})); + if (!FetchOneColumn()) { + SendSQL("UPDATE groups SET name=" . + SqlQuote($::FORM{"name-$v"}) . + " WHERE bit=" . SqlQuote($v)); + print "Group $v name updated.<br>\n"; + } else { + ShowError("Duplicate name '" . $::FORM{"name-$v"} . + "' specified for group $v.<BR>" . + "Update of group $v name skipped."); + } + } + if ($::FORM{"olddesc-$v"} ne $::FORM{"desc-$v"}) { + $chgs = 1; + SendSQL("SELECT description FROM groups WHERE description=" . + SqlQuote($::FORM{"desc-$v"})); + if (!FetchOneColumn()) { + SendSQL("UPDATE groups SET description=" . + SqlQuote($::FORM{"desc-$v"}) . + " WHERE bit=" . SqlQuote($v)); + print "Group $v description updated.<br>\n"; + } else { + ShowError("Duplicate description '" . $::FORM{"desc-$v"} . + "' specified for group $v.<BR>" . + "Update of group $v description skipped."); + } + } + if ($::FORM{"oldregexp-$v"} ne $::FORM{"regexp-$v"}) { + $chgs = 1; + SendSQL("UPDATE groups SET userregexp=" . + SqlQuote($::FORM{"regexp-$v"}) . + " WHERE bit=" . SqlQuote($v)); + print "Group $v user regexp updated.<br>\n"; + } + } + } + if (!$chgs) { + print "You didn't change anything!<BR>\n"; + print "If you really meant it, hit the <B>Back</B> button and try again.<p>\n"; + } else { + print "Done.<p>\n"; + } + PutTrailer("<a href=editgroups.cgi>Back to the group list</a>"); + exit; +} + +# +# No valid action found +# + +PutHeader("Error"); +print "I don't have a clue what you want.<BR>\n"; + +foreach ( sort keys %::FORM) { + print "$_: $::FORM{$_}<BR>\n"; +} + +PutTrailer("<a href=editgroups.cgi>Try the group list</a>"); diff --git a/editkeywords.cgi b/editkeywords.cgi new file mode 100755 index 0000000000000000000000000000000000000000..da9ec1b1cb6792d8e4220061c9a3d452abbd70e2 --- /dev/null +++ b/editkeywords.cgi @@ -0,0 +1,397 @@ +#!/usr/bonsaitools/bin/perl -w +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Bug Tracking System. +# +# The Initial Developer of the Original Code is Terry Weissman. +# Portions created by Terry Weissman are +# Copyright (C) 2000 Terry Weissman. All +# Rights Reserved. +# +# Contributor(s): Terry Weissman <terry@mozilla.org> + +use diagnostics; +use strict; + +require "CGI.pl"; + +my $localtrailer = "<A HREF=\"editkeywords.cgi\">edit</A> more keywords"; + + +# +# Displays a text like "a.", "a or b.", "a, b or c.", "a, b, c or d." +# + +sub PutTrailer (@) +{ + my (@links) = ("Back to the <A HREF=\"query.cgi\">query page</A>", @_); + + my $count = $#links; + my $num = 0; + print "<P>\n"; + foreach (@links) { + print $_; + if ($num == $count) { + print ".\n"; + } + elsif ($num == $count-1) { + print " or "; + } + else { + print ", "; + } + $num++; + } + PutFooter(); +} + + +# +# Displays the form to edit a keyword's parameters +# + +sub EmitFormElements ($$$) +{ + my ($id, $name, $description) = @_; + + $name = value_quote($name); + $description = value_quote($description); + + print qq{<INPUT TYPE="HIDDEN" NAME=id VALUE=$id>}; + + print " <TR><TH ALIGN=\"right\">Name:</TH>\n"; + print " <TD><INPUT SIZE=64 MAXLENGTH=64 NAME=\"name\" VALUE=\"$name\"></TD>\n"; + print "</TR><TR>\n"; + + print " <TH ALIGN=\"right\">Description:</TH>\n"; + print " <TD><TEXTAREA ROWS=4 COLS=64 WRAP=VIRTUAL NAME=\"description\">$description</TEXTAREA></TD>\n"; + print "</TR>\n"; + +} + + +sub Validate ($$) { + my ($name, $description) = @_; + if ($name eq "") { + print "You must enter a non-blank name for the keyword. Please press\n"; + print "<b>Back</b> and try again.\n"; + PutTrailer($localtrailer); + exit; + } + if ($name =~ /[\s,]/) { + print "You may not use commas or whitespace in a keyword name.\n"; + print "Please press <b>Back</b> and try again.\n"; + PutTrailer($localtrailer); + exit; + } + if ($description eq "") { + print "You must enter a non-blank description of the keyword.\n"; + print "Please press <b>Back</b> and try again.\n"; + PutTrailer($localtrailer); + exit; + } +} + + +# +# Preliminary checks: +# + +confirm_login(); + +print "Content-type: text/html\n\n"; + +unless (UserInGroup("editkeywords")) { + PutHeader("Not allowed"); + print "Sorry, you aren't a member of the 'editkeywords' group.\n"; + print "And so, you aren't allowed to add, modify or delete keywords.\n"; + PutTrailer(); + exit; +} + + +my $action = trim($::FORM{action} || ''); + + +if ($action eq "") { + PutHeader("Select keyword"); + my $tableheader = qq{ +<TABLE BORDER=1 CELLPADDING=4 CELLSPACING=0> +<TR BGCOLOR="#6666FF"> +<TH ALIGN="left">Edit keyword ...</TH> +<TH ALIGN="left">Description</TH> +<TH ALIGN="left">Bugs</TH> +<TH ALIGN="left">Action</TH> +</TR> +}; + print $tableheader; + my $line_count = 0; + my $max_table_size = 50; + + SendSQL("SELECT keyworddefs.id, keyworddefs.name, keyworddefs.description, + COUNT(keywords.bug_id), keywords.bug_id + FROM keyworddefs LEFT JOIN keywords ON keyworddefs.id = keywords.keywordid + GROUP BY keyworddefs.id + ORDER BY keyworddefs.name"); + while (MoreSQLData()) { + my ($id, $name, $description, $bugs, $onebug) = FetchSQLData(); + $description ||= "<FONT COLOR=\"red\">missing</FONT>"; + $bugs ||= 'none'; + if (!$onebug) { + # This is silly hackery for old versions of MySQL that seem to + # return a count() of 1 even if there are no matching. So, we + # ask for an actual bug number. If it can't find any bugs that + # match the keyword, then we set the count to be zero, ignoring + # what it had responded. + $bugs = 'none'; + } + if ($line_count == $max_table_size) { + print "</table>\n$tableheader"; + $line_count = 0; + } + $line_count++; + + print qq{ +<TR> +<TH VALIGN="top"><A HREF="editkeywords.cgi?action=edit&id=$id">$name</TH> +<TD VALIGN="top">$description</TD> +<TD VALIGN="top" ALIGN="right">$bugs</TD> +<TH VALIGN="top"><A HREF="editkeywords.cgi?action=delete&id=$id">Delete</TH> +</TR> +}; + } + print qq{ +<TR> +<TD VALIGN="top" COLSPAN=3>Add a new keyword</TD><TD><A HREF="editkeywords.cgi?action=add">Add</TD> +</TR> +</TABLE> +}; + PutTrailer(); + exit; +} + + +if ($action eq 'add') { + PutHeader("Add keyword"); + print "<FORM METHOD=POST ACTION=editkeywords.cgi>\n"; + print "<TABLE BORDER=0 CELLPADDING=4 CELLSPACING=0>\n"; + + EmitFormElements(-1, '', ''); + + print "</TABLE>\n<HR>\n"; + print "<INPUT TYPE=SUBMIT VALUE=\"Add\">\n"; + print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"new\">\n"; + print "</FORM>"; + + my $other = $localtrailer; + $other =~ s/more/other/; + PutTrailer($other); + exit; +} + +# +# action='new' -> add keyword entered in the 'action=add' screen +# + +if ($action eq 'new') { + PutHeader("Adding new keyword"); + + # Cleanups and valididy checks + + my $name = trim($::FORM{name} || ''); + my $description = trim($::FORM{description} || ''); + + Validate($name, $description); + + SendSQL("SELECT id FROM keyworddefs WHERE name = " . SqlQuote($name)); + + if (FetchOneColumn()) { + print "The keyword '$name' already exists. Please press\n"; + print "<b>Back</b> and try again.\n"; + PutTrailer($localtrailer); + exit; + } + + + # Pick an unused number. Be sure to recycle numbers that may have been + # deleted in the past. This code is potentially slow, but it happens + # rarely enough, and there really aren't ever going to be that many + # keywords anyway. + + SendSQL("SELECT id FROM keyworddefs ORDER BY id"); + + my $newid = 1; + + while (MoreSQLData()) { + my $oldid = FetchOneColumn(); + if ($oldid > $newid) { + last; + } + $newid = $oldid + 1; + } + + # Add the new keyword. + SendSQL("INSERT INTO keyworddefs (id, name, description) VALUES ($newid, " . + SqlQuote($name) . "," . + SqlQuote($description) . ")"); + + # Make versioncache flush + unlink "data/versioncache"; + + print "OK, done.<p>\n"; + PutTrailer($localtrailer); + exit; +} + + + +# +# action='edit' -> present the edit keywords from +# +# (next action would be 'update') +# + +if ($action eq 'edit') { + PutHeader("Edit keyword"); + + my $id = trim($::FORM{id} || 0); + # get data of keyword + SendSQL("SELECT name,description + FROM keyworddefs + WHERE id=$id"); + my ($name, $description) = FetchSQLData(); + if (!$name) { + print "Something screwy is going on. Please try again.\n"; + PutTrailer($localtrailer); + exit; + } + print "<FORM METHOD=POST ACTION=editkeywords.cgi>\n"; + print "<TABLE BORDER=0 CELLPADDING=4 CELLSPACING=0>\n"; + + EmitFormElements($id, $name, $description); + + print "<TR>\n"; + print " <TH ALIGN=\"right\">Bugs:</TH>\n"; + print " <TD>"; + SendSQL("SELECT count(*) + FROM keywords + WHERE keywordid = $id"); + my $bugs = ''; + $bugs = FetchOneColumn() if MoreSQLData(); + print $bugs || 'none'; + + print "</TD>\n</TR></TABLE>\n"; + + print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"update\">\n"; + print "<INPUT TYPE=SUBMIT VALUE=\"Update\">\n"; + + print "</FORM>"; + + my $x = $localtrailer; + $x =~ s/more/other/; + PutTrailer($x); + exit; +} + + +# +# action='update' -> update the keyword +# + +if ($action eq 'update') { + PutHeader("Update keyword"); + + my $id = $::FORM{id}; + my $name = trim($::FORM{name} || ''); + my $description = trim($::FORM{description} || ''); + + Validate($name, $description); + + SendSQL("SELECT id FROM keyworddefs WHERE name = " . SqlQuote($name)); + + my $tmp = FetchOneColumn(); + + if ($tmp && $tmp != $id) { + print "The keyword '$name' already exists. Please press\n"; + print "<b>Back</b> and try again.\n"; + PutTrailer($localtrailer); + exit; + } + + SendSQL("UPDATE keyworddefs SET name = " . SqlQuote($name) . + ", description = " . SqlQuote($description) . + " WHERE id = $id"); + + print "Keyword updated.<BR>\n"; + + # Make versioncache flush + unlink "data/versioncache"; + + PutTrailer($localtrailer); + exit; +} + + +if ($action eq 'delete') { + PutHeader("Delete keyword"); + my $id = $::FORM{id}; + + SendSQL("SELECT name FROM keyworddefs WHERE id=$id"); + my $name = FetchOneColumn(); + + if (!$::FORM{reallydelete}) { + + SendSQL("SELECT count(*) + FROM keywords + WHERE keywordid = $id"); + + my $bugs = FetchOneColumn(); + + if ($bugs) { + + + print qq{ +There are $bugs bugs which have this keyword set. Are you <b>sure</b> you want +to delete the <code>$name</code> keyword? + +<FORM METHOD=POST ACTION=editkeywords.cgi> +<INPUT TYPE=HIDDEN NAME="id" VALUE="$id"> +<INPUT TYPE=HIDDEN NAME="action" VALUE="delete"> +<INPUT TYPE=HIDDEN NAME="reallydelete" VALUE="1"> +<INPUT TYPE=SUBMIT VALUE="Yes, really delete the keyword"> +</FORM> +}; + + PutTrailer($localtrailer); + exit; + } + } + + SendSQL("DELETE FROM keywords WHERE keywordid = $id"); + SendSQL("DELETE FROM keyworddefs WHERE id = $id"); + + print "Keyword $name deleted.\n"; + + # Make versioncache flush + unlink "data/versioncache"; + + PutTrailer($localtrailer); + exit; +} + +PutHeader("Error"); +print "I don't have a clue what you want.<BR>\n"; + +foreach ( sort keys %::FORM) { + print "$_: $::FORM{$_}<BR>\n"; +} diff --git a/editmilestones.cgi b/editmilestones.cgi new file mode 100755 index 0000000000000000000000000000000000000000..fcd81ea0941c74d9997374d97ff201c4c31fc069 --- /dev/null +++ b/editmilestones.cgi @@ -0,0 +1,568 @@ +#!/usr/bonsaitools/bin/perl -w +# -*- Mode: perl; indent-tabs-mode: nil -*- + +# +# This is a script to edit the target milestones. It is largely a copy of +# the editversions.cgi script, since the two fields were set up in a +# very similar fashion. +# +# (basically replace each occurance of 'milestone' with 'version', and +# you'll have the original script) +# +# Matt Masson <matthew@zeroknowledge.com> +# + + +use diagnostics; +use strict; + +require "CGI.pl"; +require "globals.pl"; + + + + +# TestProduct: just returns if the specified product does exists +# CheckProduct: same check, optionally emit an error text +# TestMilestone: just returns if the specified product/version combination exists +# CheckMilestone: same check, optionally emit an error text + +sub TestProduct ($) +{ + my $prod = shift; + + # does the product exist? + SendSQL("SELECT product + FROM products + WHERE product=" . SqlQuote($prod)); + return FetchOneColumn(); +} + +sub CheckProduct ($) +{ + my $prod = shift; + + # do we have a product? + unless ($prod) { + print "Sorry, you haven't specified a product."; + PutTrailer(); + exit; + } + + unless (TestProduct $prod) { + print "Sorry, product '$prod' does not exist."; + PutTrailer(); + exit; + } +} + +sub TestMilestone ($$) +{ + my ($prod,$mile) = @_; + + # does the product exist? + SendSQL("SELECT product,value + FROM milestones + WHERE product=" . SqlQuote($prod) . " and value=" . SqlQuote($mile)); + return FetchOneColumn(); +} + +sub CheckMilestone ($$) +{ + my ($prod,$mile) = @_; + + # do we have the milestone? + unless ($mile) { + print "Sorry, you haven't specified a milestone."; + PutTrailer(); + exit; + } + + CheckProduct($prod); + + unless (TestMilestone $prod,$mile) { + print "Sorry, milestone '$mile' for product '$prod' does not exist."; + PutTrailer(); + exit; + } +} + + +# +# Displays the form to edit a milestone +# + +sub EmitFormElements ($$$) +{ + my ($product, $milestone, $sortkey) = @_; + + print " <TH ALIGN=\"right\">Milestone:</TH>\n"; + print " <TD><INPUT SIZE=64 MAXLENGTH=64 NAME=\"milestone\" VALUE=\"" . + value_quote($milestone) . "\">\n"; + print "</TR><TR>\n"; + print " <TH ALIGN=\"right\">Sortkey:</TH>\n"; + print " <TD><INPUT SIZE=64 MAXLENGTH=64 NAME=\"sortkey\" VALUE=\"" . + value_quote($sortkey) . "\">\n"; + print " <INPUT TYPE=HIDDEN NAME=\"product\" VALUE=\"" . + value_quote($product) . "\"></TD>\n"; +} + + +# +# Displays a text like "a.", "a or b.", "a, b or c.", "a, b, c or d." +# + +sub PutTrailer (@) +{ + my (@links) = ("Back to the <A HREF=\"query.cgi\">query page</A>", @_); + + my $count = $#links; + my $num = 0; + print "<P>\n"; + foreach (@links) { + print $_; + if ($num == $count) { + print ".\n"; + } + elsif ($num == $count-1) { + print " or "; + } + else { + print ", "; + } + $num++; + } + PutFooter(); +} + + + + + + + +# +# Preliminary checks: +# + +confirm_login(); + +print "Content-type: text/html\n\n"; + +unless (UserInGroup("editcomponents")) { + PutHeader("Not allowed"); + print "Sorry, you aren't a member of the 'editcomponents' group.\n"; + print "And so, you aren't allowed to add, modify or delete milestones.\n"; + PutTrailer(); + exit; +} + + +# +# often used variables +# +my $product = trim($::FORM{product} || ''); +my $milestone = trim($::FORM{milestone} || ''); +my $sortkey = trim($::FORM{sortkey} || '0'); +my $action = trim($::FORM{action} || ''); +my $localtrailer; +if ($milestone) { + $localtrailer = "<A HREF=\"editmilestones.cgi?product=" . url_quote($product) . "\">edit</A> more milestones"; +} else { + $localtrailer = "<A HREF=\"editmilestones.cgi\">edit</A> more milestones"; +} + + + +# +# product = '' -> Show nice list of milestones +# + +unless ($product) { + PutHeader("Select product"); + + SendSQL("SELECT products.product,products.description,'xyzzy' + FROM products + GROUP BY products.product + ORDER BY products.product"); + print "<TABLE BORDER=1 CELLPADDING=4 CELLSPACING=0><TR BGCOLOR=\"#6666FF\">\n"; + print " <TH ALIGN=\"left\">Edit milestones of ...</TH>\n"; + print " <TH ALIGN=\"left\">Description</TH>\n"; + print " <TH ALIGN=\"left\">Bugs</TH>\n"; + print "</TR>"; + while ( MoreSQLData() ) { + my ($product, $description, $bugs) = FetchSQLData(); + $description ||= "<FONT COLOR=\"red\">missing</FONT>"; + $bugs ||= "none"; + print "<TR>\n"; + print " <TD VALIGN=\"top\"><A HREF=\"editmilestones.cgi?product=", url_quote($product), "\"><B>$product</B></A></TD>\n"; + print " <TD VALIGN=\"top\">$description</TD>\n"; + print " <TD VALIGN=\"top\">$bugs</TD>\n"; + } + print "</TR></TABLE>\n"; + + PutTrailer(); + exit; +} + + + +# +# action='' -> Show nice list of milestones +# + +unless ($action) { + PutHeader("Select milestone"); + CheckProduct($product); + + SendSQL("SELECT value,sortkey + FROM milestones + WHERE product=" . SqlQuote($product) . " + ORDER BY sortkey,value"); + + print "<TABLE BORDER=1 CELLPADDING=4 CELLSPACING=0><TR BGCOLOR=\"#6666FF\">\n"; + print " <TH ALIGN=\"left\">Edit milestone ...</TH>\n"; + #print " <TH ALIGN=\"left\">Bugs</TH>\n"; + print " <TH ALIGN=\"left\">Sortkey</TH>\n"; + print " <TH ALIGN=\"left\">Action</TH>\n"; + print "</TR>"; + while ( MoreSQLData() ) { + my ($milestone,$sortkey,$bugs) = FetchSQLData(); + $bugs ||= 'none'; + print "<TR>\n"; + print " <TD VALIGN=\"top\"><A HREF=\"editmilestones.cgi?product=", url_quote($product), "&milestone=", url_quote($milestone), "&action=edit\"><B>$milestone</B></A></TD>\n"; + #print " <TD VALIGN=\"top\">$bugs</TD>\n"; + print " <TD VALIGN=\"top\" ALIGN=\"right\">$sortkey</TD>\n"; + print " <TD VALIGN=\"top\"><A HREF=\"editmilestones.cgi?product=", url_quote($product), "&milestone=", url_quote($milestone), "&action=del\"><B>Delete</B></A></TD>\n"; + print "</TR>"; + } + print "<TR>\n"; + print " <TD VALIGN=\"top\" COLSPAN=\"2\">Add a new milestone</TD>\n"; + print " <TD VALIGN=\"top\" ALIGN=\"middle\"><A HREF=\"editmilestones.cgi?product=", url_quote($product) . "&action=add\">Add</A></TD>\n"; + print "</TR></TABLE>\n"; + + PutTrailer(); + exit; +} + + + + +# +# action='add' -> present form for parameters for new milestone +# +# (next action will be 'new') +# + +if ($action eq 'add') { + PutHeader("Add milestone"); + CheckProduct($product); + + #print "This page lets you add a new milestone to a $::bugzilla_name tracked product.\n"; + + print "<FORM METHOD=POST ACTION=editmilestones.cgi>\n"; + print "<TABLE BORDER=0 CELLPADDING=4 CELLSPACING=0><TR>\n"; + + EmitFormElements($product, $milestone, 0); + + print "</TABLE>\n<HR>\n"; + print "<INPUT TYPE=SUBMIT VALUE=\"Add\">\n"; + print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"new\">\n"; + print "</FORM>"; + + my $other = $localtrailer; + $other =~ s/more/other/; + PutTrailer($other); + exit; +} + + + +# +# action='new' -> add milestone entered in the 'action=add' screen +# + +if ($action eq 'new') { + PutHeader("Adding new milestone"); + CheckProduct($product); + + # Cleanups and valididy checks + + unless ($milestone) { + print "You must enter a text for the new milestone. Please press\n"; + print "<b>Back</b> and try again.\n"; + PutTrailer($localtrailer); + exit; + } + if (TestMilestone($product,$milestone)) { + print "The milestone '$milestone' already exists. Please press\n"; + print "<b>Back</b> and try again.\n"; + PutTrailer($localtrailer); + exit; + } + + # Add the new milestone + SendSQL("INSERT INTO milestones ( " . + "value, product, sortkey" . + " ) VALUES ( " . + SqlQuote($milestone) . "," . + SqlQuote($product) . ", $sortkey)"); + + # Make versioncache flush + unlink "data/versioncache"; + + print "OK, done.<p>\n"; + PutTrailer($localtrailer); + exit; +} + + + + +# +# action='del' -> ask if user really wants to delete +# +# (next action would be 'delete') +# + +if ($action eq 'del') { + PutHeader("Delete milestone"); + CheckMilestone($product, $milestone); + + SendSQL("SELECT count(bug_id),product,target_milestone + FROM bugs + GROUP BY product,target_milestone + HAVING product=" . SqlQuote($product) . " + AND target_milestone=" . SqlQuote($milestone)); + my $bugs = FetchOneColumn(); + + SendSQL("SELECT defaultmilestone FROM products " . + "WHERE product=" . SqlQuote($product)); + my $defaultmilestone = FetchOneColumn(); + + print "<TABLE BORDER=1 CELLPADDING=4 CELLSPACING=0>\n"; + print "<TR BGCOLOR=\"#6666FF\">\n"; + print " <TH VALIGN=\"top\" ALIGN=\"left\">Part</TH>\n"; + print " <TH VALIGN=\"top\" ALIGN=\"left\">Value</TH>\n"; + + print "</TR><TR>\n"; + print " <TH ALIGN=\"left\" VALIGN=\"top\">Product:</TH>\n"; + print " <TD VALIGN=\"top\">$product</TD>\n"; + print "</TR><TR>\n"; + print " <TH ALIGN=\"left\" VALIGN=\"top\">Milestone:</TH>\n"; + print " <TD VALIGN=\"top\">$milestone</TD>\n"; + print "</TR><TR>\n"; + print " <TH ALIGN=\"left\" VALIGN=\"top\">Bugs:</TH>\n"; + print " <TD VALIGN=\"top\">", $bugs || 'none' , "</TD>\n"; + print "</TR></TABLE>\n"; + + print "<H2>Confirmation</H2>\n"; + + if ($bugs) { + if (!Param("allowbugdeletion")) { + print "Sorry, there are $bugs bugs outstanding for this milestone. +You must reassign those bugs to another milestone before you can delete this +one."; + PutTrailer($localtrailer); + exit; + } + print "<TABLE BORDER=0 CELLPADDING=20 WIDTH=\"70%\" BGCOLOR=\"red\"><TR><TD>\n", + "There are bugs entered for this milestone! When you delete this ", + "milestone, <B><BLINK>all</BLINK></B> stored bugs will be deleted, too. ", + "You could not even see the bug history for this milestone anymore!\n", + "</TD></TR></TABLE>\n"; + } + + if ($defaultmilestone eq $milestone) { + print "Sorry; this is the default milestone for this product, and " . + "so it can not be deleted."; + PutTrailer($localtrailer); + exit; + } + + print "<P>Do you really want to delete this milestone?<P>\n"; + print "<FORM METHOD=POST ACTION=editmilestones.cgi>\n"; + print "<INPUT TYPE=SUBMIT VALUE=\"Yes, delete\">\n"; + print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"delete\">\n"; + print "<INPUT TYPE=HIDDEN NAME=\"product\" VALUE=\"" . + value_quote($product) . "\">\n"; + print "<INPUT TYPE=HIDDEN NAME=\"milestone\" VALUE=\"" . + value_quote($milestone) . "\">\n"; + print "</FORM>"; + + PutTrailer($localtrailer); + exit; +} + + + +# +# action='delete' -> really delete the milestone +# + +if ($action eq 'delete') { + PutHeader("Deleting milestone"); + CheckMilestone($product,$milestone); + + # lock the tables before we start to change everything: + + SendSQL("LOCK TABLES attachments WRITE, + bugs WRITE, + bugs_activity WRITE, + milestones WRITE, + dependencies WRITE"); + + # According to MySQL doc I cannot do a DELETE x.* FROM x JOIN Y, + # so I have to iterate over bugs and delete all the indivial entries + # in bugs_activies and attachments. + + if (Param("allowbugdeletion")) { + + SendSQL("SELECT bug_id + FROM bugs + WHERE product=" . SqlQuote($product) . " + AND target_milestone=" . SqlQuote($milestone)); + while (MoreSQLData()) { + my $bugid = FetchOneColumn(); + + my $query = + $::db->query("DELETE FROM attachments WHERE bug_id=$bugid") + or die "$::db_errstr"; + $query = + $::db->query("DELETE FROM bugs_activity WHERE bug_id=$bugid") + or die "$::db_errstr"; + $query = + $::db->query("DELETE FROM dependencies WHERE blocked=$bugid") + or die "$::db_errstr"; + } + print "Attachments, bug activity and dependencies deleted.<BR>\n"; + + + # Deleting the rest is easier: + + SendSQL("DELETE FROM bugs + WHERE product=" . SqlQuote($product) . " + AND target_milestone=" . SqlQuote($milestone)); + print "Bugs deleted.<BR>\n"; + } + + SendSQL("DELETE FROM milestones + WHERE product=" . SqlQuote($product) . " + AND value=" . SqlQuote($milestone)); + print "Milestone deleted.<P>\n"; + SendSQL("UNLOCK TABLES"); + + unlink "data/versioncache"; + PutTrailer($localtrailer); + exit; +} + + + +# +# action='edit' -> present the edit milestone form +# +# (next action would be 'update') +# + +if ($action eq 'edit') { + PutHeader("Edit milestone"); + CheckMilestone($product,$milestone); + + SendSQL("SELECT sortkey FROM milestones WHERE product=" . + SqlQuote($product) . " AND value = " . SqlQuote($milestone)); + my $sortkey = FetchOneColumn(); + + print "<FORM METHOD=POST ACTION=editmilestones.cgi>\n"; + print "<TABLE BORDER=0 CELLPADDING=4 CELLSPACING=0><TR>\n"; + + EmitFormElements($product, $milestone, $sortkey); + + print "</TR></TABLE>\n"; + + print "<INPUT TYPE=HIDDEN NAME=\"milestoneold\" VALUE=\"" . + value_quote($milestone) . "\">\n"; + print "<INPUT TYPE=HIDDEN NAME=\"sortkeyold\" VALUE=\"" . + value_quote($sortkey) . "\">\n"; + print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"update\">\n"; + print "<INPUT TYPE=SUBMIT VALUE=\"Update\">\n"; + + print "</FORM>"; + + my $other = $localtrailer; + $other =~ s/more/other/; + PutTrailer($other); + exit; +} + + + +# +# action='update' -> update the milestone +# + +if ($action eq 'update') { + PutHeader("Update milestone"); + + my $milestoneold = trim($::FORM{milestoneold} || ''); + my $sortkeyold = trim($::FORM{sortkeyold} || '0'); + + CheckMilestone($product,$milestoneold); + + SendSQL("LOCK TABLES bugs WRITE, + milestones WRITE, + products WRITE"); + + if ($milestone ne $milestoneold) { + unless ($milestone) { + print "Sorry, I can't delete the milestone text."; + PutTrailer($localtrailer); + SendSQL("UNLOCK TABLES"); + exit; + } + if (TestMilestone($product,$milestone)) { + print "Sorry, milestone '$milestone' is already in use."; + PutTrailer($localtrailer); + SendSQL("UNLOCK TABLES"); + exit; + } + SendSQL("UPDATE bugs + SET target_milestone=" . SqlQuote($milestone) . " + WHERE target_milestone=" . SqlQuote($milestoneold) . " + AND product=" . SqlQuote($product)); + SendSQL("UPDATE milestones + SET value=" . SqlQuote($milestone) . " + WHERE product=" . SqlQuote($product) . " + AND value=" . SqlQuote($milestoneold)); + SendSQL("UPDATE products " . + "SET defaultmilestone = " . SqlQuote($milestone) . + "WHERE product = " . SqlQuote($product) . + " AND defaultmilestone = " . SqlQuote($milestoneold)); + unlink "data/versioncache"; + print "Updated milestone.<BR>\n"; + } + if ($sortkey != $sortkeyold) { + SendSQL("UPDATE milestones SET sortkey=$sortkey + WHERE product=" . SqlQuote($product) . " + AND value=" . SqlQuote($milestoneold)); + unlink "data/versioncache"; + print "Updated sortkey.<BR>\n"; + } + SendSQL("UNLOCK TABLES"); + + PutTrailer($localtrailer); + exit; +} + + + +# +# No valid action found +# + +PutHeader("Error"); +print "I don't have a clue what you want.<BR>\n"; + +foreach ( sort keys %::FORM) { + print "$_: $::FORM{$_}<BR>\n"; +} diff --git a/editowners.cgi b/editowners.cgi deleted file mode 100755 index 2957b75f2e801c1cddea8a6c4866b86059584454..0000000000000000000000000000000000000000 --- a/editowners.cgi +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bonsaitools/bin/perl -w -# -*- Mode: perl; indent-tabs-mode: nil -*- -# -# The contents of this file are subject to the Mozilla Public -# License Version 1.1 (the "License"); you may not use this file -# except in compliance with the License. You may obtain a copy of -# the License at http://www.mozilla.org/MPL/ -# -# Software distributed under the License is distributed on an "AS -# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or -# implied. See the License for the specific language governing -# rights and limitations under the License. -# -# The Original Code is the Bugzilla Bug Tracking System. -# -# The Initial Developer of the Original Code is Netscape Communications -# Corporation. Portions created by Netscape are -# Copyright (C) 1998 Netscape Communications Corporation. All -# Rights Reserved. -# -# Contributor(s): Sam Ziegler <sam@ziegler.org> - -# Code derived from editparams.cgi - -use diagnostics; -use strict; - -require "CGI.pl"; - -confirm_login(); - -print "Content-type: text/html\n\n"; - -if (!UserInGroup("editcomponents")) { - print "<H1>Sorry, you aren't a member of the 'editcomponents' group.</H1>\n"; - print "And so, you aren't allowed to edit the owners.\n"; - exit; -} - - -PutHeader("Edit Component Owners"); - -print "This lets you edit the owners of the program components of bugzilla.\n"; - -print "<form method=post action=doeditowners.cgi><table>\n"; - -my $rowbreak = "<tr><td colspan=2><hr></td></tr>"; - -SendSQL("select program, value, initialowner from components order by program, value"); - -my @line; -my $curProgram = ""; - -while (@line = FetchSQLData()) { - if ($line[0] ne $curProgram) { - print $rowbreak; - print "<tr><th align=right valign=top>$line[0]:</th><td></td></tr>\n"; - $curProgram = $line[0]; - } - print "<tr><td valign = top>$line[1]</td><td><input size=80 "; - print "name=\"$line[0]_$line[1]\" value=\"$line[2]\"></td></tr>\n"; -} - -print "</table>\n"; - -print "<input type=submit value=\"Submit changes\">\n"; - -print "</form>\n"; - -print "<p><a href=query.cgi>Skip all this, and go back to the query page</a>\n"; diff --git a/editparams.cgi b/editparams.cgi index e2273fc2eaed73d7605025e3715e26f9fa2e687f..91ddca7f939a7ede7b1bbc72c306aef9b474633d 100755 --- a/editparams.cgi +++ b/editparams.cgi @@ -38,11 +38,13 @@ print "Content-type: text/html\n\n"; if (!UserInGroup("tweakparams")) { print "<H1>Sorry, you aren't a member of the 'tweakparams' group.</H1>\n"; print "And so, you aren't allowed to edit the parameters.\n"; + PutFooter(); exit; } -PutHeader("Edit parameters"); + +PutHeader("Edit parameters", undef, undef, undef, 1); print "This lets you edit the basic operating parameters of bugzilla.\n"; print "Be careful!\n"; @@ -104,3 +106,4 @@ print "<input type=submit value=\"Submit changes\">\n"; print "</form>\n"; print "<p><a href=query.cgi>Skip all this, and go back to the query page</a>\n"; +PutFooter(); diff --git a/editproducts.cgi b/editproducts.cgi index 0b90c3d62c361a9803598b10e08c8a65abbd7e16..abbf8fde8d5912141947e62a03c4d07849f19f55 100755 --- a/editproducts.cgi +++ b/editproducts.cgi @@ -21,6 +21,7 @@ # Contributor(s): Holger Schurig <holgerschurig@nikocity.de> # Terry Weissman <terry@mozilla.org> # Dawn Endico <endico@mozilla.org> +# Joe Robins <jmrobins@tgix.com> # # Direct any questions on this source code to # @@ -32,7 +33,13 @@ use strict; require "CGI.pl"; require "globals.pl"; +# Shut up misguided -w warnings about "used only once". "use vars" just +# doesn't work for me. +sub sillyness { + my $zz; + $zz = $::unconfirmedstate; +} # TestProduct: just returns if the specified product does exists @@ -72,9 +79,11 @@ sub CheckProduct ($) # Displays the form to edit a products parameters # -sub EmitFormElements ($$$$) +sub EmitFormElements ($$$$$$$$$) { - my ($product, $description, $milestoneurl, $disallownew) = @_; + my ($product, $description, $milestoneurl, $userregexp, $disallownew, + $votesperuser, $maxvotesperbug, $votestoconfirm, $defaultmilestone) + = @_; $product = value_quote($product); $description = value_quote($description); @@ -86,17 +95,45 @@ sub EmitFormElements ($$$$) print " <TH ALIGN=\"right\">Description:</TH>\n"; print " <TD><TEXTAREA ROWS=4 COLS=64 WRAP=VIRTUAL NAME=\"description\">$description</TEXTAREA></TD>\n"; + $defaultmilestone = value_quote($defaultmilestone); if (Param('usetargetmilestone')) { $milestoneurl = value_quote($milestoneurl); print "</TR><TR>\n"; print " <TH ALIGN=\"right\">Milestone URL:</TH>\n"; print " <TD><INPUT TYPE=TEXT SIZE=64 MAXLENGTH=255 NAME=\"milestoneurl\" VALUE=\"$milestoneurl\"></TD>\n"; + + print "</TR><TR>\n"; + print " <TH ALIGN=\"right\">Default milestone:</TH>\n"; + + print " <TD><INPUT TYPE=TEXT SIZE=20 MAXLENGTH=20 NAME=\"defaultmilestone\" VALUE=\"$defaultmilestone\"></TD>\n"; + } else { + print qq{<INPUT TYPE=HIDDEN NAME="defaultmilestone" VALUE="$defaultmilestone">\n}; + } + + # Added -JMR, 2/16/00 + if (Param("usebuggroups")) { + $userregexp = value_quote($userregexp); + print "</TR><TR>\n"; + print " <TH ALIGN=\"right\">User Regexp for Bug Group:</TH>\n"; + print " <TD><INPUT TYPE=TEXT SIZE=64 MAXLENGTH=255 NAME=\"userregexp\" VALUE=\"$userregexp\"></TD>\n"; } print "</TR><TR>\n"; print " <TH ALIGN=\"right\">Closed for bug entry:</TH>\n"; my $closed = $disallownew ? "CHECKED" : ""; print " <TD><INPUT TYPE=CHECKBOX NAME=\"disallownew\" $closed VALUE=\"1\"></TD>\n"; + + print "</TR><TR>\n"; + print " <TH ALIGN=\"right\">Maximum votes per person:</TH>\n"; + print " <TD><INPUT SIZE=5 MAXLENGTH=5 NAME=\"votesperuser\" VALUE=\"$votesperuser\"></TD>\n"; + + print "</TR><TR>\n"; + print " <TH ALIGN=\"right\">Maximum votes a person can put on a single bug:</TH>\n"; + print " <TD><INPUT SIZE=5 MAXLENGTH=5 NAME=\"maxvotesperbug\" VALUE=\"$maxvotesperbug\"></TD>\n"; + + print "</TR><TR>\n"; + print " <TH ALIGN=\"right\">Number of votes a bug in this product needs to automatically get out of the <A HREF=\"bug_status.html#status\">UNCONFIRMED</A> state:</TH>\n"; + print " <TD><INPUT SIZE=5 MAXLENGTH=5 NAME=\"votestoconfirm\" VALUE=\"$votestoconfirm\"></TD>\n"; } @@ -124,7 +161,7 @@ sub PutTrailer (@) } $num++; } - print "</BODY>\n</HTML>\n"; + PutFooter(); } @@ -167,7 +204,8 @@ my $localtrailer = "<A HREF=\"editproducts.cgi\">edit</A> more products"; unless ($action) { PutHeader("Select product"); - SendSQL("SELECT products.product,description,disallownew,COUNT(bug_id) + SendSQL("SELECT products.product,description,disallownew, + votesperuser,maxvotesperbug,votestoconfirm,COUNT(bug_id) FROM products LEFT JOIN bugs ON products.product=bugs.product GROUP BY products.product @@ -176,11 +214,15 @@ unless ($action) { print " <TH ALIGN=\"left\">Edit product ...</TH>\n"; print " <TH ALIGN=\"left\">Description</TH>\n"; print " <TH ALIGN=\"left\">Status</TH>\n"; + print " <TH ALIGN=\"left\">Votes<br>per<br>user</TH>\n"; + print " <TH ALIGN=\"left\">Max<br>Votes<br>per<br>bug</TH>\n"; + print " <TH ALIGN=\"left\">Votes<br>to<br>confirm</TH>\n"; print " <TH ALIGN=\"left\">Bugs</TH>\n"; print " <TH ALIGN=\"left\">Action</TH>\n"; print "</TR>"; while ( MoreSQLData() ) { - my ($product, $description, $disallownew, $bugs) = FetchSQLData(); + my ($product, $description, $disallownew, $votesperuser, + $maxvotesperbug, $votestoconfirm, $bugs) = FetchSQLData(); $description ||= "<FONT COLOR=\"red\">missing</FONT>"; $disallownew = $disallownew ? 'closed' : 'open'; $bugs ||= 'none'; @@ -188,12 +230,15 @@ unless ($action) { print " <TD VALIGN=\"top\"><A HREF=\"editproducts.cgi?action=edit&product=", url_quote($product), "\"><B>$product</B></A></TD>\n"; print " <TD VALIGN=\"top\">$description</TD>\n"; print " <TD VALIGN=\"top\">$disallownew</TD>\n"; - print " <TD VALIGN=\"top\">$bugs</TD>\n"; + print " <TD VALIGN=\"top\" ALIGN=\"right\">$votesperuser</TD>\n"; + print " <TD VALIGN=\"top\" ALIGN=\"right\">$maxvotesperbug</TD>\n"; + print " <TD VALIGN=\"top\" ALIGN=\"right\">$votestoconfirm</TD>\n"; + print " <TD VALIGN=\"top\" ALIGN=\"right\">$bugs</TD>\n"; print " <TD VALIGN=\"top\"><A HREF=\"editproducts.cgi?action=del&product=", url_quote($product), "\">Delete</A></TD>\n"; print "</TR>"; } print "<TR>\n"; - print " <TD VALIGN=\"top\" COLSPAN=4>Add a new product</TD>\n"; + print " <TD VALIGN=\"top\" COLSPAN=7>Add a new product</TD>\n"; print " <TD VALIGN=\"top\" ALIGN=\"middle\"><FONT SIZE =-1><A HREF=\"editproducts.cgi?action=add\">Add</A></FONT></TD>\n"; print "</TR></TABLE>\n"; @@ -218,7 +263,7 @@ if ($action eq 'add') { print "<FORM METHOD=POST ACTION=editproducts.cgi>\n"; print "<TABLE BORDER=0 CELLPADDING=4 CELLSPACING=0><TR>\n"; - EmitFormElements('', '', '', 0); + EmitFormElements('', '', '', '', 0, 0, 10000, 0, "---"); print "</TR><TR>\n"; print " <TH ALIGN=\"right\">Version:</TH>\n"; @@ -244,7 +289,7 @@ if ($action eq 'add') { if ($action eq 'new') { PutHeader("Adding new product"); - # Cleanups and valididy checks + # Cleanups and validity checks unless ($product) { print "You must enter a name for the new product. Please press\n"; @@ -270,23 +315,84 @@ if ($action eq 'new') { my $description = trim($::FORM{description} || ''); my $milestoneurl = trim($::FORM{milestoneurl} || ''); + my $userregexp = trim($::FORM{userregexp} || ''); my $disallownew = 0; $disallownew = 1 if $::FORM{disallownew}; + my $votesperuser = $::FORM{votesperuser}; + $votesperuser ||= 0; + my $maxvotesperbug = $::FORM{maxvotesperbug}; + $maxvotesperbug = 10000 if !defined $maxvotesperbug; + my $votestoconfirm = $::FORM{votestoconfirm}; + $votestoconfirm ||= 0; + my $defaultmilestone = $::FORM{defaultmilestone} || "---"; # Add the new product. SendSQL("INSERT INTO products ( " . - "product, description, milestoneurl, disallownew" . - " ) VALUES ( " . - SqlQuote($product) . "," . - SqlQuote($description) . "," . - SqlQuote($milestoneurl) . "," . - $disallownew . ")" ); + "product, description, milestoneurl, disallownew, votesperuser, " . + "maxvotesperbug, votestoconfirm, defaultmilestone" . + " ) VALUES ( " . + SqlQuote($product) . "," . + SqlQuote($description) . "," . + SqlQuote($milestoneurl) . "," . + $disallownew . "," . + "$votesperuser, $maxvotesperbug, $votestoconfirm, " . + SqlQuote($defaultmilestone) . ")"); SendSQL("INSERT INTO versions ( " . "value, program" . " ) VALUES ( " . SqlQuote($version) . "," . SqlQuote($product) . ")" ); + SendSQL("INSERT INTO milestones (product, value) VALUES (" . + SqlQuote($product) . ", " . SqlQuote($defaultmilestone) . ")"); + + # If we're using bug groups, then we need to create a group for this + # product as well. -JMR, 2/16/00 + if(Param("usebuggroups")) { + # First we need to figure out the bit for this group. We'll simply + # use the next highest bit available. We'll use a minimum bit of 256, + # to leave room for a few more Bugzilla operation groups at the bottom. + SendSQL("SELECT MAX(bit) FROM groups"); + my $bit = FetchOneColumn(); + if($bit < 256) { + $bit = 256; + } else { + $bit = $bit * 2; + } + + # Next we insert into the groups table + SendSQL("INSERT INTO groups " . + "(bit, name, description, isbuggroup, userregexp) " . + "VALUES (" . + $bit . ", " . + SqlQuote($product) . ", " . + SqlQuote($product . " Bugs Access") . ", " . + "1, " . + SqlQuote($userregexp) . ")"); + + # And last, we need to add any existing users that match the regexp + # to the group. + # There may be a better way to do this in MySql, but I need to compare + # the login_names to this regexp, and the only way I can think of to + # do that is to get the list of login_names, and then update them + # one by one if they match. Furthermore, I need to do it with two + # separate loops, since opening a new SQL statement to do the update + # seems to clobber the previous one. + SendSQL("SELECT login_name FROM profiles"); + my @login_list = (); + my $this_login; + while($this_login = FetchOneColumn()) { + push @login_list, $this_login; + } + foreach $this_login (@login_list) { + if($this_login =~ /$userregexp/) { + SendSQL("UPDATE profiles " . + "SET groupset = groupset | " . $bit . " " . + "WHERE login_name = " . SqlQuote($this_login)); + } + } + } + # Make versioncache flush unlink "data/versioncache"; @@ -334,6 +440,23 @@ if ($action eq 'del') { print " <TD VALIGN=\"top\"><A HREF=\"$milestoneurl\">$milestoneurl</A></TD>\n"; } + # Added -JMR, 2/16/00 + if(Param('usebuggroups')) { + # Get the regexp for this product. + SendSQL("SELECT userregexp + FROM groups + WHERE name=" . SqlQuote($product)); + my $userregexp = FetchOneColumn(); + if(!defined $userregexp) { + $userregexp = "<FONT COLOR=\"red\">undefined</FONT>"; + } elsif ($userregexp eq "") { + $userregexp = "<FONT COLOR=\"blue\">blank</FONT>"; + } + print "</TR><TR>\n"; + print " <TD VALIGN=\"top\">User Regexp for Bug Group:</TD>\n"; + print " <TD VALIGN=\"top\">$userregexp</TD>\n"; + } + print "</TR><TR>\n"; print " <TD VALIGN=\"top\">Closed for bugs:</TD>\n"; print " <TD VALIGN=\"top\">$disallownew</TD>\n"; @@ -376,6 +499,29 @@ if ($action eq 'del') { print "<FONT COLOR=\"red\">missing</FONT>"; } + # + # Adding listing for associated target milestones - matthew@zeroknowledge.com + # + if (Param('usetargetmilestone')) { + print "</TD>\n</TR><TR>\n"; + print " <TH ALIGN=\"right\" VALIGN=\"top\"><A HREF=\"editmilestones.cgi?product=", url_quote($product), "\">Edit milestones:</A></TH>\n"; + print " <TD>"; + SendSQL("SELECT value + FROM milestones + WHERE product=" . SqlQuote($product) . " + ORDER BY sortkey,value"); + if(MoreSQLData()) { + my $br = 0; + while ( MoreSQLData() ) { + my ($milestone) = FetchSQLData(); + print "<BR>" if $br; + print $milestone; + $br = 1; + } + } else { + print "<FONT COLOR=\"red\">missing</FONT>"; + } + } print "</TD>\n</TR><TR>\n"; print " <TD VALIGN=\"top\">Bugs:</TD>\n"; @@ -437,33 +583,41 @@ if ($action eq 'delete') { components WRITE, dependencies WRITE, versions WRITE, - products WRITE"); + products WRITE, + groups WRITE, + profiles WRITE, + milestones WRITE"); # According to MySQL doc I cannot do a DELETE x.* FROM x JOIN Y, # so I have to iterate over bugs and delete all the indivial entries # in bugs_activies and attachments. - SendSQL("SELECT bug_id + if (Param("allowbugdeletion")) { + SendSQL("SELECT bug_id FROM bugs WHERE product=" . SqlQuote($product)); - while (MoreSQLData()) { - my $bugid = FetchOneColumn(); + while (MoreSQLData()) { + my $bugid = FetchOneColumn(); - my $query = $::db->query("DELETE FROM attachments WHERE bug_id=$bugid") + my $query = + $::db->query("DELETE FROM attachments WHERE bug_id=$bugid") or die "$::db_errstr"; - $query = $::db->query("DELETE FROM bugs_activity WHERE bug_id=$bugid") + $query = + $::db->query("DELETE FROM bugs_activity WHERE bug_id=$bugid") or die "$::db_errstr"; - $query = $::db->query("DELETE FROM dependencies WHERE blocked=$bugid") + $query = + $::db->query("DELETE FROM dependencies WHERE blocked=$bugid") or die "$::db_errstr"; - } - print "Attachments, bug activity and dependencies deleted.<BR>\n"; + } + print "Attachments, bug activity and dependencies deleted.<BR>\n"; - # Deleting the rest is easier: + # Deleting the rest is easier: - SendSQL("DELETE FROM bugs + SendSQL("DELETE FROM bugs WHERE product=" . SqlQuote($product)); - print "Bugs deleted.<BR>\n"; + print "Bugs deleted.<BR>\n"; + } SendSQL("DELETE FROM components WHERE program=" . SqlQuote($product)); @@ -473,9 +627,40 @@ if ($action eq 'delete') { WHERE program=" . SqlQuote($product)); print "Versions deleted.<P>\n"; + # deleting associated target milestones - matthew@zeroknowledge.com + SendSQL("DELETE FROM milestones + WHERE product=" . SqlQuote($product)); + print "Milestones deleted.<BR>\n"; + SendSQL("DELETE FROM products WHERE product=" . SqlQuote($product)); print "Product '$product' deleted.<BR>\n"; + + # Added -JMR, 2/16/00 + if (Param("usebuggroups")) { + # We need to get the bit of the group from the table, then update the + # groupsets of members of that group and remove the group. + SendSQL("SELECT bit, description FROM groups " . + "WHERE name = " . SqlQuote($product)); + my ($bit, $group_desc) = FetchSQLData(); + + # Make sure there is a group before we try to do any deleting... + if($bit) { + # I'm kludging a bit so that I don't break superuser access; + # I'm merely checking to make sure that the groupset is not + # the superuser groupset in doing this update... + SendSQL("UPDATE profiles " . + "SET groupset = groupset - $bit " . + "WHERE (groupset & $bit) " . + "AND (groupset != 9223372036854710271)"); + print "Users dropped from group '$group_desc'.<BR>\n"; + + SendSQL("DELETE FROM groups " . + "WHERE bit = $bit"); + print "Group '$group_desc' deleted.<BR>\n"; + } + } + SendSQL("UNLOCK TABLES"); unlink "data/versioncache"; @@ -496,15 +681,28 @@ if ($action eq 'edit') { CheckProduct($product); # get data of product - SendSQL("SELECT description,milestoneurl,disallownew + SendSQL("SELECT description,milestoneurl,disallownew, + votesperuser,maxvotesperbug,votestoconfirm,defaultmilestone FROM products WHERE product=" . SqlQuote($product)); - my ($description, $milestoneurl, $disallownew) = FetchSQLData(); + my ($description, $milestoneurl, $disallownew, + $votesperuser, $maxvotesperbug, $votestoconfirm, $defaultmilestone) = + FetchSQLData(); + + my $userregexp = ''; + if(Param("usebuggroups")) { + SendSQL("SELECT userregexp + FROM groups + WHERE name=" . SqlQuote($product)); + $userregexp = FetchOneColumn(); + } print "<FORM METHOD=POST ACTION=editproducts.cgi>\n"; print "<TABLE BORDER=0 CELLPADDING=4 CELLSPACING=0><TR>\n"; - EmitFormElements($product, $description, $milestoneurl, $disallownew); + EmitFormElements($product, $description, $milestoneurl, $userregexp, + $disallownew, $votesperuser, $maxvotesperbug, + $votestoconfirm, $defaultmilestone); print "</TR><TR VALIGN=top>\n"; print " <TH ALIGN=\"right\"><A HREF=\"editcomponents.cgi?product=", url_quote($product), "\">Edit components:</A></TH>\n"; @@ -545,6 +743,29 @@ if ($action eq 'edit') { print "<FONT COLOR=\"red\">missing</FONT>"; } + # + # Adding listing for associated target milestones - matthew@zeroknowledge.com + # + if (Param('usetargetmilestone')) { + print "</TD>\n</TR><TR>\n"; + print " <TH ALIGN=\"right\" VALIGN=\"top\"><A HREF=\"editmilestones.cgi?product=", url_quote($product), "\">Edit milestones:</A></TH>\n"; + print " <TD>"; + SendSQL("SELECT value + FROM milestones + WHERE product=" . SqlQuote($product) . " + ORDER BY sortkey,value"); + if(MoreSQLData()) { + my $br = 0; + while ( MoreSQLData() ) { + my ($milestone) = FetchSQLData(); + print "<BR>" if $br; + print $milestone; + $br = 1; + } + } else { + print "<FONT COLOR=\"red\">missing</FONT>"; + } + } print "</TD>\n</TR><TR>\n"; print " <TH ALIGN=\"right\">Bugs:</TH>\n"; @@ -565,7 +786,16 @@ if ($action eq 'edit') { value_quote($description) . "\">\n"; print "<INPUT TYPE=HIDDEN NAME=\"milestoneurlold\" VALUE=\"" . value_quote($milestoneurl) . "\">\n"; + if(Param("usebuggroups")) { + print "<INPUT TYPE=HIDDEN NAME=\"userregexpold\" VALUE=\"" . + value_quote($userregexp) . "\">\n"; + } print "<INPUT TYPE=HIDDEN NAME=\"disallownewold\" VALUE=\"$disallownew\">\n"; + print "<INPUT TYPE=HIDDEN NAME=\"votesperuserold\" VALUE=\"$votesperuser\">\n"; + print "<INPUT TYPE=HIDDEN NAME=\"maxvotesperbugold\" VALUE=\"$maxvotesperbug\">\n"; + print "<INPUT TYPE=HIDDEN NAME=\"votestoconfirmold\" VALUE=\"$votestoconfirm\">\n"; + $defaultmilestone = value_quote($defaultmilestone); + print "<INPUT TYPE=HIDDEN NAME=\"defaultmilestoneold\" VALUE=\"$defaultmilestone\">\n"; print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"update\">\n"; print "<INPUT TYPE=SUBMIT VALUE=\"Update\">\n"; @@ -586,25 +816,46 @@ if ($action eq 'edit') { if ($action eq 'update') { PutHeader("Update product"); - my $productold = trim($::FORM{productold} || ''); - my $description = trim($::FORM{description} || ''); - my $descriptionold = trim($::FORM{descriptionold} || ''); - my $disallownew = trim($::FORM{disallownew} || ''); - my $disallownewold = trim($::FORM{disallownewold} || ''); - my $milestoneurl = trim($::FORM{milestoneurl} || ''); - my $milestoneurlold = trim($::FORM{milestoneurlold} || ''); + my $productold = trim($::FORM{productold} || ''); + my $description = trim($::FORM{description} || ''); + my $descriptionold = trim($::FORM{descriptionold} || ''); + my $disallownew = trim($::FORM{disallownew} || ''); + my $disallownewold = trim($::FORM{disallownewold} || ''); + my $milestoneurl = trim($::FORM{milestoneurl} || ''); + my $milestoneurlold = trim($::FORM{milestoneurlold} || ''); + my $votesperuser = trim($::FORM{votesperuser} || 0); + my $votesperuserold = trim($::FORM{votesperuserold} || 0); + my $userregexp = trim($::FORM{userregexp} || ''); + my $userregexpold = trim($::FORM{userregexpold} || ''); + my $maxvotesperbug = trim($::FORM{maxvotesperbug} || 0); + my $maxvotesperbugold = trim($::FORM{maxvotesperbugold} || 0); + my $votestoconfirm = trim($::FORM{votestoconfirm} || 0); + my $votestoconfirmold = trim($::FORM{votestoconfirmold} || 0); + my $defaultmilestone = trim($::FORM{defaultmilestone} || '---'); + my $defaultmilestoneold = trim($::FORM{defaultmilestoneold} || '---'); + + my $checkvotes = 0; CheckProduct($productold); + if ($maxvotesperbug !~ /^\d+$/ || $maxvotesperbug <= 0) { + print "Sorry, the max votes per bug must be a positive integer."; + PutTrailer($localtrailer); + exit; + } + # Note that the order of this tests is important. If you change # them, be sure to test for WHERE='$product' or WHERE='$productold' SendSQL("LOCK TABLES bugs WRITE, components WRITE, products WRITE, - versions WRITE"); + versions WRITE, + groups WRITE, + profiles WRITE, + milestones WRITE"); - if ($disallownew != $disallownewold) { + if ($disallownew ne $disallownewold) { $disallownew ||= 0; SendSQL("UPDATE products SET disallownew=$disallownew @@ -615,8 +866,8 @@ if ($action eq 'update') { if ($description ne $descriptionold) { unless ($description) { print "Sorry, I can't delete the description."; - PutTrailer($localtrailer); SendSQL("UNLOCK TABLES"); + PutTrailer($localtrailer); exit; } SendSQL("UPDATE products @@ -632,39 +883,211 @@ if ($action eq 'update') { print "Updated mile stone URL.<BR>\n"; } + # Added -JMR, 2/16/00 + if (Param("usebuggroups") && $userregexp ne $userregexpold) { + # This will take a little bit of work here, since there may not be + # an existing bug group for this product, and we will also have to + # update users groupsets. + # First we find out if there's an existing group for this product, and + # get its bit if there is. + SendSQL("SELECT bit " . + "FROM groups " . + "WHERE name = " . SqlQuote($productold)); + my $bit = FetchOneColumn(); + if($bit) { + # Group exists, so we do an update statement. + SendSQL("UPDATE groups " . + "SET userregexp = " . SqlQuote($userregexp) . " " . + "WHERE name = " . SqlQuote($productold)); + print "Updated user regexp for bug group.<BR>\n"; + } else { + # Group doesn't exist. Let's make it, the same way as we make a + # group for a new product above. + SendSQL("SELECT MAX(bit) FROM groups"); + my $tmp_bit = FetchOneColumn(); + if($tmp_bit < 256) { + $bit = 256; + } else { + $bit = $tmp_bit * 2; + } + SendSQL("INSERT INTO groups " . + "(bit, name, description, isbuggroup, userregexp) " . + "values (" . $bit . ", " . + SqlQuote($productold) . ", " . + SqlQuote($productold . " Bugs Access") . ", " . + "1, " . + SqlQuote($userregexp) . ")"); + print "Created bug group.<BR>\n"; + } + + # And now we have to update the profiles again to add any users who + # match the new regexp to the group. I'll do this the same way as + # when I create a new group above. Note that I'm not taking out + # users who matched the old regexp and not the new one; that would + # be insanely messy. Use the group administration page for that + # instead. + SendSQL("SELECT login_name FROM profiles"); + my @login_list = (); + my $this_login; + while($this_login = FetchOneColumn()) { + push @login_list, $this_login; + } + my $updated_profiles = 0; + foreach $this_login (@login_list) { + if($this_login =~ /$userregexp/) { + SendSQL("UPDATE profiles " . + "SET groupset = groupset | " . $bit . " " . + "WHERE login_name = " . SqlQuote($this_login)); + $updated_profiles = 1; + } + } + if($updated_profiles) { + print "Added users matching regexp to group.<BR>\n"; + } + } + + if ($votesperuser ne $votesperuserold) { + SendSQL("UPDATE products + SET votesperuser=$votesperuser + WHERE product=" . SqlQuote($productold)); + print "Updated votes per user.<BR>\n"; + $checkvotes = 1; + } + + + if ($maxvotesperbug ne $maxvotesperbugold) { + SendSQL("UPDATE products + SET maxvotesperbug=$maxvotesperbug + WHERE product=" . SqlQuote($productold)); + print "Updated max votes per bug.<BR>\n"; + $checkvotes = 1; + } + + + if ($votestoconfirm ne $votestoconfirmold) { + SendSQL("UPDATE products + SET votestoconfirm=$votestoconfirm + WHERE product=" . SqlQuote($productold)); + print "Updated votes to confirm.<BR>\n"; + $checkvotes = 1; + } + + + if ($defaultmilestone ne $defaultmilestoneold) { + SendSQL("SELECT value FROM milestones " . + "WHERE value = " . SqlQuote($defaultmilestone) . + " AND product = " . SqlQuote($productold)); + if (!FetchOneColumn()) { + print "Sorry, the milestone $defaultmilestone must be defined first."; + SendSQL("UNLOCK TABLES"); + PutTrailer($localtrailer); + exit; + } + SendSQL("UPDATE products " . + "SET defaultmilestone = " . SqlQuote($defaultmilestone) . + "WHERE product=" . SqlQuote($productold)); + print "Updated default milestone.<BR>\n"; + } + + my $qp = SqlQuote($product); + my $qpold = SqlQuote($productold); if ($product ne $productold) { unless ($product) { print "Sorry, I can't delete the product name."; - PutTrailer($localtrailer); SendSQL("UNLOCK TABLES"); + PutTrailer($localtrailer); exit; } if (TestProduct($product)) { print "Sorry, product name '$product' is already in use."; - PutTrailer($localtrailer); SendSQL("UNLOCK TABLES"); + PutTrailer($localtrailer); exit; } - SendSQL("UPDATE bugs - SET product=" . SqlQuote($product) . " - WHERE product=" . SqlQuote($productold)); - SendSQL("UPDATE components - SET program=" . SqlQuote($product) . " - WHERE program=" . SqlQuote($productold)); - SendSQL("UPDATE products - SET product=" . SqlQuote($product) . " - WHERE product=" . SqlQuote($productold)); - SendSQL("UPDATE versions - SET program='$product' - WHERE program=" . SqlQuote($productold)); - - unlink "data/versioncache"; + SendSQL("UPDATE bugs SET product=$qp WHERE product=$qpold"); + SendSQL("UPDATE components SET program=$qp WHERE program=$qpold"); + SendSQL("UPDATE products SET product=$qp WHERE product=$qpold"); + SendSQL("UPDATE versions SET program=$qp WHERE program=$qpold"); + SendSQL("UPDATE milestones SET product=$qp WHERE product=$qpold"); + # Need to do an update to groups as well. If there is a corresponding + # bug group, whether usebuggroups is currently set or not, we want to + # update it so it will match in the future. If there is no group, this + # update statement will do nothing, so no harm done. -JMR, 3/8/00 + SendSQL("UPDATE groups " . + "SET name=$qp, " . + "description=".SqlQuote($product." Bugs Access")." ". + "WHERE name=$qpold"); + print "Updated product name.<BR>\n"; } + unlink "data/versioncache"; SendSQL("UNLOCK TABLES"); + if ($checkvotes) { + print "Checking existing votes in this product for anybody who now has too many votes."; + if ($maxvotesperbug < $votesperuser) { + SendSQL("SELECT votes.who, votes.bug_id " . + "FROM votes, bugs " . + "WHERE bugs.bug_id = votes.bug_id " . + " AND bugs.product = $qp " . + " AND votes.count > $maxvotesperbug"); + my @list; + while (MoreSQLData()) { + my ($who, $id) = (FetchSQLData()); + push(@list, [$who, $id]); + } + foreach my $ref (@list) { + my ($who, $id) = (@$ref); + RemoveVotes($id, $who, "The rules for voting on this product has changed;\nyou had too many votes for a single bug."); + my $name = DBID_to_name($who); + print qq{<br>Removed votes for bug <A HREF="show_bug.cgi?id=$id">$id</A> from $name\n}; + } + } + SendSQL("SELECT votes.who, votes.count FROM votes, bugs " . + "WHERE bugs.bug_id = votes.bug_id " . + " AND bugs.product = $qp"); + my %counts; + while (MoreSQLData()) { + my ($who, $count) = (FetchSQLData()); + if (!defined $counts{$who}) { + $counts{$who} = $count; + } else { + $counts{$who} += $count; + } + } + foreach my $who (keys(%counts)) { + if ($counts{$who} > $votesperuser) { + SendSQL("SELECT votes.bug_id FROM votes, bugs " . + "WHERE bugs.bug_id = votes.bug_id " . + " AND bugs.product = $qp " . + " AND votes.who = $who"); + while (MoreSQLData()) { + my $id = FetchSQLData(); + RemoveVotes($id, $who, + "The rules for voting on this product has changed; you had too many\ntotal votes, so all votes have been removed."); + my $name = DBID_to_name($who); + print qq{<br>Removed votes for bug <A HREF="show_bug.cgi?id=$id">$id</A> from $name\n}; + } + } + } + SendSQL("SELECT bug_id FROM bugs " . + "WHERE product = $qp " . + " AND bug_status = '$::unconfirmedstate' " . + " AND votes >= $votestoconfirm"); + my @list; + while (MoreSQLData()) { + push(@list, FetchOneColumn()); + } + foreach my $id (@list) { + SendSQL("SELECT who FROM votes WHERE bug_id = $id"); + my $who = FetchOneColumn(); + CheckIfVotedConfirmed($id, $who); + } + + } + PutTrailer($localtrailer); exit; } diff --git a/editusers.cgi b/editusers.cgi index 1c4343385b4b806738d504a853c4a0b05260ab46..1b5d396a716f03937ac846d194cd21c3075813b3 100755 --- a/editusers.cgi +++ b/editusers.cgi @@ -31,7 +31,16 @@ use strict; require "CGI.pl"; require "globals.pl"; +# Shut up misguided -w warnings about "used only once". "use vars" just +# doesn't work for me. +sub sillyness { + my $zz; + $zz = $::userid; +} + +my $editall; +my $opblessgroupset = '9223372036854775807'; # This is all 64 bits. @@ -69,36 +78,81 @@ sub CheckUser ($) +sub EmitElement ($$) +{ + my ($name, $value) = (@_); + $value = value_quote($value); + if ($editall) { + print qq{<TD><INPUT SIZE=64 MAXLENGTH=255 NAME="$name" VALUE="$value"></TD>\n}; + } else { + print qq{<TD>$value</TD>\n}; + } +} + + # # Displays the form to edit a user parameters # -sub EmitFormElements ($$$$) +sub EmitFormElements ($$$$$$$) { - my ($user, $password, $realname, $groupset) = @_; + my ($user, $password, $realname, $groupset, $blessgroupset, + $emailnotification, $disabledtext) = @_; print " <TH ALIGN=\"right\">Login name:</TH>\n"; - print " <TD><INPUT SIZE=64 MAXLENGTH=255 NAME=\"user\" VALUE=\"$user\"></TD>\n"; + EmitElement("user", $user); print "</TR><TR>\n"; print " <TH ALIGN=\"right\">Real name:</TH>\n"; - print " <TD><INPUT SIZE=64 MAXLENGTH=255 NAME=\"realname\" VALUE=\"$realname\"></TD>\n"; - - print "</TR><TR>\n"; - print " <TH ALIGN=\"right\">Password:</TH>\n"; - print " <TD><INPUT SIZE=16 MAXLENGTH=16 NAME=\"password\" VALUE=\"$password\"></TD>\n"; - - - SendSQL("SELECT bit,name,description - FROM groups - ORDER BY name"); + EmitElement("realname", $realname); + + if ($editall) { + print "</TR><TR>\n"; + print " <TH ALIGN=\"right\">Password:</TH>\n"; + print " <TD><INPUT SIZE=16 MAXLENGTH=16 NAME=\"password\" VALUE=\"$password\"></TD>\n"; + + print "</TR><TR>\n"; + print " <TH ALIGN=\"right\">Email notification:</TH>\n"; + print qq{<TD><SELECT NAME="emailnotification">}; + foreach my $i (["ExcludeSelfChanges", "All qualifying bugs except those which I change"], + ["CConly", "Only those bugs which I am listed on the CC line"], + ["All", "All qualifying bugs"]) { + my ($tag, $desc) = (@$i); + my $selectpart = ""; + if ($tag eq $emailnotification) { + $selectpart = " SELECTED"; + } + print qq{<OPTION$selectpart VALUE="$tag">$desc\n}; + } + print "</SELECT></TD>\n"; + print "</TR><TR>\n"; + print " <TH ALIGN=\"right\">Disable text:</TH>\n"; + print " <TD ROWSPAN=2><TEXTAREA NAME=\"disabledtext\" ROWS=10 COLS=60>" . + value_quote($disabledtext) . "</TEXTAREA>\n"; + print " </TD>\n"; + print "</TR><TR>\n"; + print " <TD VALIGN=\"top\">If non-empty, then the account will\n"; + print "be disabled, and this text should explain why.</TD>\n"; + } + + + SendSQL("SELECT bit,name,description,bit & $groupset != 0, " . + " bit & $blessgroupset " . + "FROM groups " . + "WHERE bit & $opblessgroupset != 0 " . + "ORDER BY name"); while (MoreSQLData()) { - my($bit,$name,$description) = FetchSQLData(); + my ($bit,$name,$description,$checked,$blchecked) = FetchSQLData(); print "</TR><TR>\n"; - $bit = $bit+0; # this strange construct coverts a string to a number print " <TH ALIGN=\"right\">", ucfirst($name), ":</TH>\n"; - my $checked = ($groupset & $bit) ? "CHECKED" : ""; + $checked = ($checked) ? "CHECKED" : ""; print " <TD><INPUT TYPE=CHECKBOX NAME=\"bit_$name\" $checked VALUE=\"$bit\"> $description</TD>\n"; + if ($editall) { + print "</TR><TR>\n"; + print "<TH></TH>"; + $blchecked = ($blchecked) ? "CHECKED" : ""; + print "<TD><INPUT TYPE=CHECKBOX NAME=\"blbit_$name\" $blchecked VALUE=\"$bit\"> Can turn this bit on for other users</TD>\n"; + } } } @@ -129,7 +183,7 @@ sub PutTrailer (@) } $num++; } - print "</BODY></HTML>\n"; + PutFooter(); } @@ -142,12 +196,19 @@ confirm_login(); print "Content-type: text/html\n\n"; -unless (UserInGroup("tweakparams")) { - PutHeader("Not allowed"); - print "Sorry, you aren't a member of the 'tweakparams' group.\n"; - print "And so, you aren't allowed to add, modify or delete users.\n"; - PutTrailer(); - exit; +$editall = UserInGroup("editusers"); + +if (!$editall) { + SendSQL("SELECT blessgroupset FROM profiles WHERE userid = $::userid"); + $opblessgroupset = FetchOneColumn(); + if (!$opblessgroupset) { + PutHeader("Not allowed"); + print "Sorry, you aren't a member of the 'editusers' group, and you\n"; + print "don't have permissions to put people in or out of any group.\n"; + print "And so, you aren't allowed to add, modify or delete users.\n"; + PutTrailer(); + exit; + } } @@ -158,45 +219,100 @@ unless (UserInGroup("tweakparams")) { my $user = trim($::FORM{user} || ''); my $action = trim($::FORM{action} || ''); my $localtrailer = "<A HREF=\"editusers.cgi\">edit</A> more users"; +my $candelete = Param('allowuserdeletion'); # -# action='' -> Show nice list of users +# action='' -> Ask for match string for users. # unless ($action) { + PutHeader("Select match string"); + print qq{ +<FORM METHOD=GET ACTION="editusers.cgi"> +<INPUT TYPE=HIDDEN NAME="action" VALUE="list"> +List users with login name matching: +<INPUT SIZE=32 NAME="matchstr"> +<SELECT NAME="matchtype"> +<OPTION VALUE="substr" SELECTED>case-insensitive substring +<OPTION VALUE="regexp">case-sensitive regexp +<OPTION VALUE="notregexp">not (case-sensitive regexp) +</SELECT> +<BR> +<INPUT TYPE=SUBMIT VALUE="Submit"> +}; + PutTrailer(); + exit; +} + + +# +# action='list' -> Show nice list of matching users +# + +if ($action eq 'list') { PutHeader("Select user"); + my $query = "SELECT login_name,realname,disabledtext " . + "FROM profiles WHERE login_name "; + if ($::FORM{'matchtype'} eq 'substr') { + $query .= "like"; + $::FORM{'matchstr'} = '%' . $::FORM{'matchstr'} . '%'; + } elsif ($::FORM{'matchtype'} eq 'regexp') { + $query .= "regexp"; + } elsif ($::FORM{'matchtype'} eq 'notregexp') { + $query .= "not regexp"; + } else { + die "Unknown match type"; + } + $query .= SqlQuote($::FORM{'matchstr'}) . " ORDER BY login_name"; - SendSQL("SELECT login_name,realname - FROM profiles - ORDER BY login_name"); + SendSQL($query); my $count = 0; my $header = "<TABLE BORDER=1 CELLPADDING=4 CELLSPACING=0><TR BGCOLOR=\"#6666FF\"> <TH ALIGN=\"left\">Edit user ...</TH> <TH ALIGN=\"left\">Real name</TH> -<TH ALIGN=\"left\">Action</TH>\n -</TR>"; +"; + if ($candelete) { + $header .= "<TH ALIGN=\"left\">Action</TH>\n"; + } + $header .= "</TR>\n"; print $header; while ( MoreSQLData() ) { $count++; if ($count % 100 == 0) { print "</table>$header"; } - my ($user, $realname) = FetchSQLData(); + my ($user, $realname, $disabledtext) = FetchSQLData(); + my $s = ""; + my $e = ""; + if ($disabledtext) { + $s = "<STRIKE>"; + $e = "</STRIKE>"; + } $realname ||= "<FONT COLOR=\"red\">missing</FONT>"; print "<TR>\n"; - print " <TD VALIGN=\"top\"><A HREF=\"editusers.cgi?action=edit&user=", url_quote($user), "\"><B>$user</B></A></TD>\n"; - print " <TD VALIGN=\"top\">$realname</TD>\n"; - print " <TD VALIGN=\"top\"><A HREF=\"editusers.cgi?action=del&user=", url_quote($user), "\">Delete</A></TD>\n"; + print " <TD VALIGN=\"top\"><A HREF=\"editusers.cgi?action=edit&user=", url_quote($user), "\"><B>$s$user$e</B></A></TD>\n"; + print " <TD VALIGN=\"top\">$s$realname$e</TD>\n"; + if ($candelete) { + print " <TD VALIGN=\"top\"><A HREF=\"editusers.cgi?action=del&user=", url_quote($user), "\">Delete</A></TD>\n"; + } print "</TR>"; } - print "<TR>\n"; - print " <TD VALIGN=\"top\" COLSPAN=2>Add a new user</TD>\n"; - print " <TD VALIGN=\"top\" ALIGN=\"middle\"><FONT SIZE =-1><A HREF=\"editusers.cgi?action=add\">Add</A></FONT></TD>\n"; - print "</TR></TABLE>\n"; + if ($editall) { + print "<TR>\n"; + my $span = $candelete ? 3 : 2; + print qq{ +<TD VALIGN="top" COLSPAN=$span ALIGN="right"> + <A HREF=\"editusers.cgi?action=add\">Add a new user</A> +</TD> +}; + print "</TR>"; + } + print "</TABLE>\n"; + print "$count users found.\n"; - PutTrailer(); + PutTrailer($localtrailer); exit; } @@ -211,13 +327,16 @@ unless ($action) { if ($action eq 'add') { PutHeader("Add user"); - - #print "This page lets you add a new product to bugzilla.\n"; + if (!$editall) { + print "Sorry, you don't have permissions to add new users."; + PutTrailer(); + exit; + } print "<FORM METHOD=POST ACTION=editusers.cgi>\n"; print "<TABLE BORDER=0 CELLPADDING=4 CELLSPACING=0><TR>\n"; - EmitFormElements('', '', '', 0); + EmitFormElements('', '', '', 0, 0, 'ExcludeSelfChanges', ''); print "</TR></TABLE>\n<HR>\n"; print "<INPUT TYPE=SUBMIT VALUE=\"Add\">\n"; @@ -239,9 +358,16 @@ if ($action eq 'add') { if ($action eq 'new') { PutHeader("Adding new user"); + if (!$editall) { + print "Sorry, you don't have permissions to add new users."; + PutTrailer(); + exit; + } + # Cleanups and valididy checks my $realname = trim($::FORM{realname} || ''); my $password = trim($::FORM{password} || ''); + my $disabledtext = trim($::FORM{disabledtext} || ''); unless ($user) { print "You must enter a name for the new user. Please press\n"; @@ -269,31 +395,25 @@ if ($action eq 'new') { exit; } - my $bits = 0; + my $bits = "0"; foreach (keys %::FORM) { next unless /^bit_/; #print "$_=$::FORM{$_}<br>\n"; - $bits |= $::FORM{$_}; + $bits .= "+ $::FORM{$_}"; } - sub x { - my $sc="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./"; - return substr($sc, int (rand () * 100000) % (length ($sc) + 1), 1); - } - - my $salt = x() . x(); - my $cryptpassword = crypt($password, $salt); - # Add the new user SendSQL("INSERT INTO profiles ( " . - "login_name, password, cryptpassword, realname, groupset" . - " ) VALUES ( " . - SqlQuote($user) . "," . - SqlQuote($password) . "," . - SqlQuote($cryptpassword) . "," . - SqlQuote($realname) . "," . - $bits . ")" ); + "login_name, password, cryptpassword, realname, groupset, " . + "disabledtext" . + " ) VALUES ( " . + SqlQuote($user) . "," . + SqlQuote($password) . "," . + "encrypt(" . SqlQuote($password) . ")," . + SqlQuote($realname) . "," . + $bits . "," . + SqlQuote($disabledtext) . ")" ); #+++ send e-mail away @@ -314,9 +434,18 @@ if ($action eq 'new') { if ($action eq 'del') { PutHeader("Delete user"); + if (!$candelete) { + print "Sorry, deleting users isn't allowed."; + PutTrailer(); + } + if (!$editall) { + print "Sorry, you don't have permissions to delete users."; + PutTrailer(); + exit; + } CheckUser($user); - # display some data about the product + # display some data about the user SendSQL("SELECT realname, groupset, emailnotification, login_name FROM profiles WHERE login_name=" . SqlQuote($user)); @@ -439,6 +568,15 @@ if ($action eq 'del') { if ($action eq 'delete') { PutHeader("Deleting user"); + if (!$candelete) { + print "Sorry, deleting users isn't allowed."; + PutTrailer(); + } + if (!$editall) { + print "Sorry, you don't have permissions to delete users."; + PutTrailer(); + exit; + } CheckUser($user); SendSQL("SELECT userid @@ -469,23 +607,31 @@ if ($action eq 'edit') { CheckUser($user); # get data of user - SendSQL("SELECT password, realname, groupset, emailnotification + SendSQL("SELECT password, realname, groupset, blessgroupset, + emailnotification, disabledtext FROM profiles WHERE login_name=" . SqlQuote($user)); - my ($password, $realname, $groupset, $emailnotification) = FetchSQLData(); + my ($password, $realname, $groupset, $blessgroupset, $emailnotification, + $disabledtext) = FetchSQLData(); print "<FORM METHOD=POST ACTION=editusers.cgi>\n"; print "<TABLE BORDER=0 CELLPADDING=4 CELLSPACING=0><TR>\n"; - EmitFormElements($user, $password, $realname, $groupset); + EmitFormElements($user, $password, $realname, $groupset, $blessgroupset, + $emailnotification, $disabledtext); print "</TR></TABLE>\n"; print "<INPUT TYPE=HIDDEN NAME=\"userold\" VALUE=\"$user\">\n"; - print "<INPUT TYPE=HIDDEN NAME=\"passwordold\" VALUE=\"$password\">\n"; + if ($editall) { + print "<INPUT TYPE=HIDDEN NAME=\"passwordold\" VALUE=\"$password\">\n"; + } print "<INPUT TYPE=HIDDEN NAME=\"realnameold\" VALUE=\"$realname\">\n"; print "<INPUT TYPE=HIDDEN NAME=\"groupsetold\" VALUE=\"$groupset\">\n"; + print "<INPUT TYPE=HIDDEN NAME=\"blessgroupsetold\" VALUE=\"$blessgroupset\">\n"; print "<INPUT TYPE=HIDDEN NAME=\"emailnotificationold\" VALUE=\"$emailnotification\">\n"; + print "<INPUT TYPE=HIDDEN NAME=\"disabledtextold\" VALUE=\"" . + value_quote($disabledtext) . "\">\n"; print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"update\">\n"; print "<INPUT TYPE=SUBMIT VALUE=\"Update\">\n"; @@ -502,7 +648,7 @@ if ($action eq 'edit') { # if ($action eq 'update') { - PutHeader("Update User"); + PutHeader("Updated user"); my $userold = trim($::FORM{userold} || ''); my $realname = trim($::FORM{realname} || ''); @@ -511,13 +657,22 @@ if ($action eq 'update') { my $passwordold = trim($::FORM{passwordold} || ''); my $emailnotification = trim($::FORM{emailnotification} || ''); my $emailnotificationold = trim($::FORM{emailnotificationold} || ''); - my $groupsetold = trim($::FORM{groupsetold} || ''); + my $disabledtext = trim($::FORM{disabledtext} || ''); + my $disabledtextold = trim($::FORM{disabledtextold} || ''); + my $groupsetold = trim($::FORM{groupsetold} || '0'); + my $blessgroupsetold = trim($::FORM{blessgroupsetold} || '0'); - my $groupset = 0; + my $groupset = "0"; foreach (keys %::FORM) { next unless /^bit_/; #print "$_=$::FORM{$_}<br>\n"; - $groupset |= $::FORM{$_}; + $groupset .= " + $::FORM{$_}"; + } + my $blessgroupset = "0"; + foreach (keys %::FORM) { + next unless /^blbit_/; + #print "$_=$::FORM{$_}<br>\n"; + $blessgroupset .= " + $::FORM{$_}"; } CheckUser($userold); @@ -525,37 +680,73 @@ if ($action eq 'update') { # Note that the order of this tests is important. If you change # them, be sure to test for WHERE='$product' or WHERE='$productold' - if ($groupset != $groupsetold) { + if ($groupset ne $groupsetold) { + SendSQL("SELECT groupset FROM profiles WHERE login_name=" . + SqlQuote($userold)); + $groupsetold = FetchOneColumn(); SendSQL("UPDATE profiles - SET groupset=" . $groupset . " + SET groupset = + groupset - (groupset & $opblessgroupset) + $groupset WHERE login_name=" . SqlQuote($userold)); + + # I'm paranoid that someone who I give the ability to bless people + # will start misusing it. Let's log who blesses who (even though + # nothing actually uses this log right now). + my $fieldid = GetFieldID("groupset"); + SendSQL("SELECT userid, groupset FROM profiles WHERE login_name=" . + SqlQuote($userold)); + my $u; + ($u, $groupset) = (FetchSQLData()); + if ($groupset ne $groupsetold) { + SendSQL("INSERT INTO profiles_activity " . + "(userid,who,profiles_when,fieldid,oldvalue,newvalue) " . + "VALUES " . + "($u, $::userid, now(), $fieldid, " . + " $groupsetold, $groupset)"); + } print "Updated permissions.\n"; } -=for me + if ($editall && $blessgroupset ne $blessgroupsetold) { + SendSQL("UPDATE profiles + SET blessgroupset=" . $blessgroupset . " + WHERE login_name=" . SqlQuote($userold)); + print "Updated ability to tweak permissions of other users.\n"; + } - if ($emailnotification ne $emailnotificationold) { + if ($editall && $emailnotification ne $emailnotificationold) { SendSQL("UPDATE profiles - SET emailnotification=" . $emailnotification . " + SET emailnotification=" . SqlQuote($emailnotification) . " WHERE login_name=" . SqlQuote($userold)); print "Updated email notification.<BR>\n"; } -=cut - - if ($password ne $passwordold) { + if ($editall && $password ne $passwordold) { + my $q = SqlQuote($password); SendSQL("UPDATE profiles - SET password=" . SqlQuote($password) . " + SET password= $q, cryptpassword = ENCRYPT($q) WHERE login_name=" . SqlQuote($userold)); print "Updated password.<BR>\n"; } - if ($realname ne $realnameold) { + if ($editall && $realname ne $realnameold) { SendSQL("UPDATE profiles SET realname=" . SqlQuote($realname) . " WHERE login_name=" . SqlQuote($userold)); print "Updated real name.<BR>\n"; } - if ($user ne $userold) { + if ($editall && $disabledtext ne $disabledtextold) { + SendSQL("UPDATE profiles + SET disabledtext=" . SqlQuote($disabledtext) . " + WHERE login_name=" . SqlQuote($userold)); + SendSQL("SELECT userid + FROM profiles + WHERE login_name=" . SqlQuote($user)); + my $userid = FetchOneColumn(); + SendSQL("DELETE FROM logincookies + WHERE userid=" . $userid); + print "Updated disabled text.<BR>\n"; + } + if ($editall && $user ne $userold) { unless ($user) { print "Sorry, I can't delete the user's name."; PutTrailer($localtrailer); diff --git a/editversions.cgi b/editversions.cgi index 8adab915fab5436c3d74e85e7b51ebf4ad052c8d..afc223bd8dfcc55cf1942fffafaecc00a3744112 100755 --- a/editversions.cgi +++ b/editversions.cgi @@ -141,7 +141,7 @@ sub PutTrailer (@) } $num++; } - print "</BODY>\n</HTML>\n"; + PutFooter(); } @@ -427,29 +427,35 @@ if ($action eq 'delete') { # so I have to iterate over bugs and delete all the indivial entries # in bugs_activies and attachments. - SendSQL("SELECT bug_id + if (Param("allowbugdeletion")) { + + SendSQL("SELECT bug_id FROM bugs WHERE product=" . SqlQuote($product) . " AND version=" . SqlQuote($version)); - while (MoreSQLData()) { - my $bugid = FetchOneColumn(); + while (MoreSQLData()) { + my $bugid = FetchOneColumn(); - my $query = $::db->query("DELETE FROM attachments WHERE bug_id=$bugid") + my $query = + $::db->query("DELETE FROM attachments WHERE bug_id=$bugid") or die "$::db_errstr"; - $query = $::db->query("DELETE FROM bugs_activity WHERE bug_id=$bugid") + $query = + $::db->query("DELETE FROM bugs_activity WHERE bug_id=$bugid") or die "$::db_errstr"; - $query = $::db->query("DELETE FROM dependencies WHERE blocked=$bugid") + $query = + $::db->query("DELETE FROM dependencies WHERE blocked=$bugid") or die "$::db_errstr"; - } - print "Attachments, bug activity and dependencies deleted.<BR>\n"; + } + print "Attachments, bug activity and dependencies deleted.<BR>\n"; - # Deleting the rest is easier: + # Deleting the rest is easier: - SendSQL("DELETE FROM bugs + SendSQL("DELETE FROM bugs WHERE product=" . SqlQuote($product) . " AND version=" . SqlQuote($version)); - print "Bugs deleted.<BR>\n"; + print "Bugs deleted.<BR>\n"; + } SendSQL("DELETE FROM versions WHERE program=" . SqlQuote($product) . " diff --git a/enter_bug.cgi b/enter_bug.cgi index d1a8dbf4bff09559b3f1ed959cd1f179b2c5635b..086372d237ebccfde904925ed726a9e1b26a64a3 100755 --- a/enter_bug.cgi +++ b/enter_bug.cgi @@ -12,43 +12,95 @@ # rights and limitations under the License. # # The Original Code is the Bugzilla Bug Tracking System. -# +# # The Initial Developer of the Original Code is Netscape Communications -# Corporation. Portions created by Netscape are -# Copyright (C) 1998 Netscape Communications Corporation. All -# Rights Reserved. -# +# Corporation. Portions created by Netscape are Copyright (C) 1998 +# Netscape Communications Corporation. All Rights Reserved. +# # Contributor(s): Terry Weissman <terry@mozilla.org> +# Dave Miller <dave@intrec.com> +# Joe Robins <jmrobins@tgix.com> + + +######################################################################## +# +# enter_bug.cgi +# ------------- +# Displays bug entry form. Bug fields are specified through popup menus, +# drop-down lists, or text fields. Default for these values can be passed +# in as parameters to the cgi. +# +######################################################################## use diagnostics; use strict; require "CGI.pl"; -# Shut up misguided -w warnings about "used only once": -use vars @::legal_platform, - @::legal_severity, - @::legal_opsys, - @::legal_priority; +# Shut up misguided -w warnings about "used only once". "use vars" just +# doesn't work for me. +sub sillyness { + my $zz; + $zz = $::unconfirmedstate; + $zz = @::legal_opsys; + $zz = @::legal_platform; + $zz = @::legal_priority; + $zz = @::legal_severity; +} + +# I've moved the call to confirm_login up to here, since if we're using bug +# groups to restrict bug entry, we need to know who the user is right from +# the start. If that parameter is turned off, there's still no harm done in +# doing it now instead of a bit later. -JMR, 2/18/00 +# Except that it will cause people without cookies enabled to have to log +# in an extra time. Only do it here if we really need to. -terry, 3/10/00 +if (Param("usebuggroupsentry")) { + confirm_login(); +} if (!defined $::FORM{'product'}) { GetVersionTable(); - my @prodlist = keys %::versions; - if ($#prodlist != 0) { + my @prodlist; + foreach my $p (sort(keys %::versions)) { + if (defined $::proddesc{$p} && $::proddesc{$p} eq '0') { + # Special hack. If we stuffed a "0" into proddesc, that means + # that disallownew was set for this bug, and so we don't want + # to allow people to specify that product here. + next; + } + if(Param("usebuggroupsentry") + && GroupExists($p) + && !UserInGroup($p)) { + # If we're using bug groups to restrict entry on products, and + # this product has a bug group, and the user is not in that + # group, we don't want to include that product in this list. + next; + } + push(@prodlist, $p); + } + if (1 != @prodlist) { print "Content-type: text/html\n\n"; PutHeader("Enter Bug"); print "<H2>First, you must pick a product on which to enter\n"; print "a bug.</H2>\n"; print "<table>"; - foreach my $p (sort (@prodlist)) { + foreach my $p (@prodlist) { if (defined $::proddesc{$p} && $::proddesc{$p} eq '0') { # Special hack. If we stuffed a "0" into proddesc, that means # that disallownew was set for this bug, and so we don't want # to allow people to specify that product here. next; } + if(Param("usebuggroupsentry") + && GroupExists($p) + && !UserInGroup($p)) { + # If we're using bug groups to restrict entry on products, and + # this product has a bug group, and the user is not in that + # group, we don't want to include that product in this list. + next; + } print "<tr><th align=right valign=top><a href=\"enter_bug.cgi?product=" . url_quote($p) . "\">$p</a>:</th>\n"; if (defined $::proddesc{$p}) { print "<td valign=top>$::proddesc{$p}</td>\n"; @@ -56,6 +108,7 @@ if (!defined $::FORM{'product'}) { print "</tr>"; } print "</table>\n"; + PutFooter(); exit; } $::FORM{'product'} = $prodlist[0]; @@ -88,6 +141,7 @@ sub pickplatform { /Mozilla.*\(Windows/ && do {return "PC";}; /Mozilla.*\(Macintosh/ && do {return "Macintosh";}; /Mozilla.*\(Win/ && do {return "PC";}; + /Mozilla.*Windows NT/ && do {return "PC";}; /Mozilla.*Linux.*86/ && do {return "PC";}; /Mozilla.*Linux.*alpha/ && do {return "DEC";}; /Mozilla.*OSF/ && do {return "DEC";}; @@ -155,7 +209,10 @@ sub pickos { /Mozilla.*\(.*;.*; BSD\/OS.*\)/ && do {return "BSDI";}; /Mozilla.*\(Win16.*\)/ && do {return "Windows 3.1";}; /Mozilla.*\(Win95.*\)/ && do {return "Windows 95";}; + /Mozilla.*\(Win98.*\)/ && do {return "Windows 98";}; /Mozilla.*\(WinNT.*\)/ && do {return "Windows NT";}; + /Mozilla.*\(Windows.*NT/ && do {return "Windows NT";}; + /Mozilla.*Windows NT 5.*\)/ && do {return "Windows 2000";}; } } # default @@ -180,14 +237,57 @@ my $platform_popup = make_popup('rep_platform', \@::legal_platform, pickplatform(), 0); my $opsys_popup = make_popup('op_sys', \@::legal_opsys, pickos(), 0); +if (1 == @{$::components{$product}}) { + # Only one component; just pick it. + $::FORM{'component'} = $::components{$product}->[0]; +} + my $component_popup = make_popup('component', $::components{$product}, formvalue('component'), 1); -PutHeader ("Enter Bug"); +PutHeader ("Enter Bug","Enter Bug","This page lets you enter a new bug into Bugzilla."); + +# Modified, -JMR, 2/24,00 +# If the usebuggroupsentry parameter is set, we need to check and make sure +# that the user has permission to enter a bug against this product. +# Modified, -DDM, 3/11/00 +# added GroupExists check so we don't choke on a groupless product +if(Param("usebuggroupsentry") + && GroupExists($product) + && !UserInGroup($product)) { + print "<H1>Permission denied.</H1>\n"; + print "Sorry; you do not have the permissions necessary to enter\n"; + print "a bug against this product.\n"; + print "<P>\n"; + PutFooter(); + exit; +} + +# Modified, -JMR, 2/18/00 +# I'm putting in a select box in order to select whether to restrict this bug to +# the product's bug group or not, if the usebuggroups parameter is set, and if +# this product has a bug group. This box will default to selected, but can be +# turned off if this bug should be world-viewable for some reason. +# +# To do this, I need to (1) get the bit and description for the bug group from +# the database, (2) insert the select box in the giant print statements below, +# and (3) update post_bug.cgi to process the additional input field. + +# Modified, -DDM, 3/11/00 +# Only need the bit here, and not the description. Description is gotten +# when the select boxes for all the groups this user has access to are read +# in later on. +# First we get the bit and description for the group. +my $group_bit=0; +if(Param("usebuggroups") && GroupExists($product)) { + SendSQL("select bit from groups ". + "where name = ".SqlQuote($product)." ". + "and isbuggroup != 0"); + ($group_bit) = FetchSQLData(); +} print " <FORM METHOD=POST ACTION=\"post_bug.cgi\"> -<INPUT TYPE=HIDDEN NAME=bug_status VALUE=NEW> <INPUT TYPE=HIDDEN NAME=reporter VALUE=\"$::COOKIE{'Bugzilla_login'}\"> <INPUT TYPE=HIDDEN NAME=product VALUE=\"" . value_quote($product) . "\"> <TABLE CELLSPACING=2 CELLPADDING=0 BORDER=0>"; @@ -230,7 +330,7 @@ print " <TR>"; if (Param('letsubmitterchoosepriority')) { print " - <TD ALIGN=RIGHT><B><A HREF=\"bug_status.html#priority\">Priority</A>:</B></TD> + <TD ALIGN=RIGHT><B><A HREF=\"bug_status.html#priority\">Resolution<br>Priority</A>:</B></TD> <TD>$priority_popup</TD>"; } else { print '<INPUT TYPE=HIDDEN NAME=priority VALUE="' . @@ -243,6 +343,26 @@ print " <td></td> </TR> <tr><td> <td> <td> <td> <td> <td> </tr> +"; + +if (UserInGroup("editbugs") || UserInGroup("canconfirm")) { + SendSQL("SELECT votestoconfirm FROM products WHERE product = " . + SqlQuote($product)); + if (FetchOneColumn()) { + print qq{ + <TR> + <TD ALIGN="right"><B><A HREF="bug_status.html#status">Initial state:</B></A></TD> + <TD COLSPAN="5"> +}; + print BuildPulldown("bug_status", + [[$::unconfirmedstate], ["NEW"]], + "NEW"); + print "</TD></TR>"; + } +} + + +print " <tr> <TD ALIGN=RIGHT><B><A HREF=\"bug_status.html#assigned_to\">Assigned To:</A></B></TD> <TD colspan=5>$assign_element @@ -272,11 +392,52 @@ print " <td colspan=5><TEXTAREA WRAP=HARD NAME=comment ROWS=10 COLS=80>" . value_quote(formvalue('comment')) . "</TEXTAREA><BR></td> + </tr>"; + +print " + <tr> + <td></td><td colspan=5> +"; + +if ($::usergroupset ne '0') { + SendSQL("SELECT bit, description FROM groups " . + "WHERE bit & $::usergroupset != 0 " . + " AND isbuggroup != 0 ORDER BY description"); + while (MoreSQLData()) { + my ($bit, $description) = (FetchSQLData()); + # Rather than waste time with another Param check and another database + # access, $group_bit will only have a non-zero value if we're using + # bug groups and have one for this product, so I'll check on that + # instead here. -JMR, 2/18/00 + # Moved this check to this location to fix conflict with existing + # select-box patch. Also, if $group_bit is 0, it won't match the + # current group, either, so I'll compare it to the current bit + # instead of checking for non-zero. -DDM, 3/11/00 + my $check = 0; # default selection + if($group_bit == $bit) { + # In addition, we need to handle the possibility that we're coming + # from a bookmark template. We'll simply check if we've got a + # parameter called bit-# passed. If so, then we're coming from a + # template, and we'll use the template value. + $check = formvalue("bit-$bit","1"); + } + print BuildPulldown("bit-$bit", + [["0", + "People not in the \"$description\" group can see this bug"], + ["1", + "Only people in the \"$description\" group can see this bug"]], + $check); + print "<BR>\n"; + } +} + +print " + </td> </tr> <tr> <td></td> <td colspan=5> - <INPUT TYPE=\"submit\" VALUE=\" Commit \"> + <INPUT TYPE=\"submit\" VALUE=\" Commit \" ONCLICK=\"if (this.form.short_desc.value =='') { alert('Please enter a summary sentence for this bug.'); return false; }\"> <INPUT TYPE=\"reset\" VALUE=\"Reset\"> @@ -301,5 +462,7 @@ print " <INPUT TYPE=hidden name=form_name VALUE=enter_bug> </FORM>\n"; +PutFooter(); + print "</BODY></HTML>\n"; diff --git a/globals.pl b/globals.pl index fdb86a4fd34525225cbd442f5d5329a7ec631715..a9925ade1634a262e57633bd69fe29d89810716d 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. @@ -30,11 +31,13 @@ use strict; sub globals_pl_sillyness { my $zz; $zz = @main::chooseone; - $zz = @main::db_errstr; $zz = @main::default_column_list; + $zz = $main::defaultqueryname; $zz = @main::dontchange; + $zz = %main::keywordsbyname; $zz = @main::legal_bug_status; $zz = @main::legal_components; + $zz = @main::legal_keywords; $zz = @main::legal_opsys; $zz = @main::legal_platform; $zz = @main::legal_priority; @@ -46,28 +49,103 @@ sub globals_pl_sillyness { $zz = @main::prodmaxvotes; } +# +# Here are the --LOCAL-- variables defined in 'localconfig' that we'll use +# here +# + +my $db_host = "localhost"; +my $db_name = "bugs"; +my $db_user = "bugs"; +my $db_pass = ""; + +do 'localconfig'; + 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.8'; +$::param{'version'} = '2.10'; $::dontchange = "--do_not_change--"; $::chooseone = "--Choose_one:--"; +$::defaultqueryname = "(Default query)"; +$::unconfirmedstate = "UNCONFIRMED"; +$::dbwritesallowed = 1; sub ConnectToDatabase { + my ($useshadow) = (@_); if (!defined $::db) { - $::db = Mysql->Connect("localhost", "bugs", "bugs", "") + my $name = $db_name; + if ($useshadow && Param("shadowdb") && Param("queryagainstshadowdb")) { + $name = Param("shadowdb"); + $::dbwritesallowed = 0; + } + $::db = Mysql->Connect($db_host, $name, $db_user, $db_pass) || die "Can't connect to database server."; } } +sub ReconnectToShadowDatabase { + if (Param("shadowdb") && Param("queryagainstshadowdb")) { + SendSQL("USE " . Param("shadowdb")); + $::dbwritesallowed = 0; + } +} + +my $shadowchanges = 0; +sub SyncAnyPendingShadowChanges { + if ($shadowchanges) { + system("./syncshadowdb &"); + $shadowchanges = 0; + } +} + + +my $dosqllog = (-e "data/sqllog") && (-w "data/sqllog"); + +sub SqlLog { + if ($dosqllog) { + my ($str) = (@_); + open(SQLLOGFID, ">>data/sqllog") || die "Can't write to data/sqllog"; + if (flock(SQLLOGFID,2)) { # 2 is magic 'exclusive lock' const. + print SQLLOGFID time2str("%D %H:%M:%S $$", time()) . ": $str\n"; + } + flock(SQLLOGFID,8); # '8' is magic 'unlock' const. + close SQLLOGFID; + } +} + sub SendSQL { - my ($str) = (@_); + my ($str, $dontshadow) = (@_); + my $iswrite = ($str =~ /^(INSERT|REPLACE|UPDATE|DELETE)/i); + if ($iswrite && !$::dbwritesallowed) { + die "Evil code attempted to write stuff to the shadow database."; + } + if ($str =~ /^LOCK TABLES/i && $str !~ /shadowlog/ && $::dbwritesallowed) { + $str =~ s/^LOCK TABLES/LOCK TABLES shadowlog WRITE, /i; + } + SqlLog($str); $::currentquery = $::db->query($str) - || die "$str: $::db_errstr"; + || die "$str: " . $::db->errmsg; + SqlLog("Done"); + if (!$dontshadow && $iswrite && Param("shadowdb")) { + my $q = SqlQuote($str); + my $insertid; + if ($str =~ /^(INSERT|REPLACE)/i) { + SendSQL("SELECT LAST_INSERT_ID()"); + $insertid = FetchOneColumn(); + } + SendSQL("INSERT INTO shadowlog (command) VALUES ($q)", 1); + if ($insertid) { + SendSQL("SET LAST_INSERT_ID = $insertid"); + } + $shadowchanges++; + } } sub MoreSQLData { @@ -107,15 +185,30 @@ sub AppendComment { if ($comment =~ /^\s*$/) { # Nothin' but whitespace. return; } - SendSQL("select long_desc from bugs where bug_id = $bugid"); - - my $desc = FetchOneColumn(); - my $now = time2str("%D %H:%M", time()); - $desc .= "\n\n------- Additional Comments From $who $now -------\n"; - $desc .= $comment; - SendSQL("update bugs set long_desc=" . SqlQuote($desc) . - " where bug_id=$bugid"); + + my $whoid = DBNameToIdAndCheck($who); + + SendSQL("INSERT INTO longdescs (bug_id, who, bug_when, thetext) " . + "VALUES($bugid, $whoid, now(), " . SqlQuote($comment) . ")"); + + SendSQL("UPDATE bugs SET delta_ts = now() WHERE bug_id = $bugid"); +} + +sub GetFieldID { + my ($f) = (@_); + SendSQL("SELECT fieldid FROM fielddefs WHERE name = " . SqlQuote($f)); + my $fieldid = FetchOneColumn(); + if (!$fieldid) { + my $q = SqlQuote($f); + SendSQL("REPLACE INTO fielddefs (name, description) VALUES ($q, $q)"); + SendSQL("SELECT LAST_INSERT_ID()"); + $fieldid = FetchOneColumn(); + } + return $fieldid; } + + + sub lsearch { my ($list,$item) = (@_); @@ -166,7 +259,23 @@ sub Version_element { return make_popup("version", $versionlist, $defversion, 1, $onchange); } +sub Milestone_element { + my ($tm, $prod, $onchange) = (@_); + my $tmlist; + if (!defined $::target_milestone{$prod}) { + $tmlist = []; + } else { + $tmlist = $::target_milestone{$prod}; + } + + my $deftm = $tmlist->[0]; + if (lsearch($tmlist, $tm) >= 0) { + $deftm = $tm; + } + + return make_popup("target_milestone", $tmlist, $deftm, 1, $onchange); +} # Generate a string which, when later interpreted by the Perl compiler, will # be the same as the given string. @@ -257,10 +366,15 @@ sub GenerateVersionTable { $carray{$c} = 1; } - my $dotargetmilestone = Param("usetargetmilestone"); + my $dotargetmilestone = 1; # This used to check the param, but there's + # enough code that wants to pretend we're using + # target milestones, even if they don't get + # shown to the user. So we cache all the data + # about them anyway. my $mpart = $dotargetmilestone ? ", milestoneurl" : ""; SendSQL("select product, description, votesperuser, disallownew$mpart from products"); + $::anyvotesallowed = 0; while (@line = FetchSQLData()) { my ($p, $d, $votesperuser, $dis, $u) = (@line); $::proddesc{$p} = $d; @@ -274,13 +388,16 @@ sub GenerateVersionTable { $::milestoneurl{$p} = $u; } $::prodmaxvotes{$p} = $votesperuser; + if ($votesperuser > 0) { + $::anyvotesallowed = 1; + } } my $cols = LearnAboutColumns("bugs"); @::log_columns = @{$cols->{"-list-"}}; - foreach my $i ("bug_id", "creation_ts", "delta_ts", "long_desc") { + foreach my $i ("bug_id", "creation_ts", "delta_ts", "lastdiffed") { my $w = lsearch(\@::log_columns, $i); if ($w >= 0) { splice(@::log_columns, $w, 1); @@ -326,16 +443,40 @@ sub GenerateVersionTable { } print FID GenerateCode('%::proddesc'); print FID GenerateCode('%::prodmaxvotes'); + print FID GenerateCode('$::anyvotesallowed'); if ($dotargetmilestone) { - my $last = Param("nummilestones"); - my $i; - for ($i=1 ; $i<=$last ; $i++) { - push(@::legal_target_milestone, "M$i"); + # reading target milestones in from the database - matthew@zeroknowledge.com + SendSQL("SELECT value, product FROM milestones ORDER BY sortkey, value"); + my @line; + my %tmarray; + @::legal_target_milestone = (); + while(@line = FetchSQLData()) { + my ($tm, $pr) = (@line); + if (!defined $::target_milestone{$pr}) { + $::target_milestone{$pr} = []; + } + push @{$::target_milestone{$pr}}, $tm; + if (!exists $tmarray{$tm}) { + $tmarray{$tm} = 1; + push(@::legal_target_milestone, $tm); + } } + + print FID GenerateCode('%::target_milestone'); print FID GenerateCode('@::legal_target_milestone'); print FID GenerateCode('%::milestoneurl'); } + + SendSQL("SELECT id, name FROM keyworddefs ORDER BY name"); + while (MoreSQLData()) { + my ($id, $name) = FetchSQLData(); + $::keywordsbyname{$name} = $id; + push(@::legal_keywords, $name); + } + print FID GenerateCode('@::legal_keywords'); + print FID GenerateCode('%::keywordsbyname'); + print FID "1;\n"; close FID; rename $tmpname, "data/versioncache" || die "Can't rename $tmpname to versioncache"; @@ -388,7 +529,10 @@ sub InsertNewUser { my $groupset = "0"; while (MoreSQLData()) { my @row = FetchSQLData(); - if ($username =~ m/$row[1]/) { + # Modified -Joe Robins, 2/17/00 + # Making this case insensitive, since usernames are email addresses, + # and could be any case. + if ($username =~ m/$row[1]/i) { $groupset .= "+ $row[0]"; # Silly hack to let MySQL do the math, # not Perl, since we're dealing with 64 # bit ints here, and I don't *think* Perl @@ -402,13 +546,23 @@ sub InsertNewUser { return $password; } +sub DBID_to_real_or_loginname { + my ($id) = (@_); + SendSQL("SELECT login_name,realname FROM profiles WHERE userid = $id"); + my ($l, $r) = FetchSQLData(); + if (!defined $r || $r eq "") { + return $l; + } else { + return "$l ($r)"; + } +} sub DBID_to_name { my ($id) = (@_); if (!defined $::cachedNameArray{$id}) { SendSQL("select login_name from profiles where userid = $id"); my $r = FetchOneColumn(); - if ($r eq "") { + if (!defined $r || $r eq "") { $r = "__UNKNOWN__"; } $::cachedNameArray{$id} = $r; @@ -442,36 +596,179 @@ sub DBNameToIdAndCheck { print "Yikes; couldn't create user $name. Please report problem to " . Param("maintainer") ."\n"; } else { - print "The name <TT>$name</TT> is not a valid username. Please hit\n"; - print "the <B>Back</B> button and try again.\n"; + print "The name <TT>$name</TT> is not a valid username. Either you\n"; + print "misspelled it, or the person has not registered for a\n"; + print "Bugzilla account.\n"; + print "<P>Please hit the <B>Back</B> button and try again.\n"; } exit(0); } -sub GetLongDescription { - my ($id) = (@_); - SendSQL("select long_desc from bugs where bug_id = $id"); - return FetchOneColumn(); +# This routine quoteUrls contains inspirations from the HTML::FromText CPAN +# module by Gareth Rees <garethr@cre.canon.co.uk>. It has been heavily hacked, +# all that is really recognizable from the original is bits of the regular +# expressions. + +sub quoteUrls { + my ($knownattachments, $text) = (@_); + return $text unless $text; + + my $base = Param('urlbase'); + + my $protocol = join '|', + qw(afs cid ftp gopher http https mid news nntp prospero telnet wais); + + my %options = ( metachars => 1, @_ ); + + my $count = 0; + + # Now, quote any "#" characters so they won't confuse stuff later + $text =~ s/#/%#/g; + + # Next, find anything that looks like a URL or an email address and + # pull them out the the text, replacing them with a "##<digits>## + # marker, and writing them into an array. All this confusion is + # necessary so that we don't match on something we've already replaced, + # which can happen if you do multiple s///g operations. + + my @things; + while ($text =~ s%((mailto:)?([\w\.\-\+\=]+\@[\w\-]+(?:\.[\w\-]+)+)\b| + (\b((?:$protocol):[^ \t\n<>"]+[\w/])))%"##$count##"%exo) { + my $item = $&; + + $item = value_quote($item); + + if ($item !~ m/^$protocol:/o && $item !~ /^mailto:/) { + # We must have grabbed this one because it looks like an email + # address. + $item = qq{<A HREF="mailto:$item">$item</A>}; + } else { + $item = qq{<A HREF="$item">$item</A>}; + } + + $things[$count++] = $item; + } + while ($text =~ s/\bbug(\s|%\#)*(\d+)/"##$count##"/ei) { + my $item = $&; + my $num = $2; + $item = value_quote($item); # Not really necessary, since we know + # there's no special chars in it. + $item = qq{<A HREF="show_bug.cgi?id=$num">$item</A>}; + $things[$count++] = $item; + } + while ($text =~ s/\*\*\* This bug has been marked as a duplicate of (\d+) \*\*\*/"##$count##"/ei) { + my $item = $&; + my $num = $1; + $item =~ s@\d+@<A HREF="show_bug.cgi?id=$num">$num</A>@; + $things[$count++] = $item; + } + while ($text =~ s/Created an attachment \(id=(\d+)\)/"##$count##"/e) { + my $item = $&; + my $num = $1; + if ($knownattachments->{$num}) { + $item = qq{<A HREF="showattachment.cgi?attach_id=$num">$item</A>}; + } + $things[$count++] = $item; + } + + $text = value_quote($text); + $text =~ s/\
/\n/g; + + # Stuff everything back from the array. + for (my $i=0 ; $i<$count ; $i++) { + $text =~ s/##$i##/$things[$i]/e; + } + + # And undo the quoting of "#" characters. + $text =~ s/%#/#/g; + + return $text; } +sub GetLongDescriptionAsText { + my ($id, $start, $end) = (@_); + my $result = ""; + my $count = 0; + my ($query) = ("SELECT profiles.login_name, longdescs.bug_when, " . + " longdescs.thetext " . + "FROM longdescs, profiles " . + "WHERE profiles.userid = longdescs.who " . + " AND longdescs.bug_id = $id "); -sub ShowCcList { - my ($num) = (@_); - my @ccids; - my @row; - SendSQL("select who from cc where bug_id = $num"); - while (@row = FetchSQLData()) { - push(@ccids, $row[0]); + if ($start && $start =~ /[1-9]/) { + # If the start is all zeros, then don't do this (because we want to + # not emit a leading "Additional Comments" line in that case.) + $query .= "AND longdescs.bug_when > '$start'"; + $count = 1; } - my @result = (); - foreach my $i (@ccids) { - push @result, DBID_to_name($i); + if ($end) { + $query .= "AND longdescs.bug_when <= '$end'"; + } + + $query .= "ORDER BY longdescs.bug_when"; + SendSQL($query); + while (MoreSQLData()) { + my ($who, $when, $text) = (FetchSQLData()); + if ($count) { + $result .= "\n\n------- Additional Comments From $who " . + time2str("%Y-%m-%d %H:%M", str2time($when)) . " -------\n"; + } + $result .= $text; + $count++; } - return join(',', @result); + return $result; } +sub GetLongDescriptionAsHTML { + my ($id, $start, $end) = (@_); + my $result = ""; + my $count = 0; + my %knownattachments; + SendSQL("SELECT attach_id FROM attachments WHERE bug_id = $id"); + while (MoreSQLData()) { + $knownattachments{FetchOneColumn()} = 1; + } + + my ($query) = ("SELECT profiles.realname, profiles.login_name, longdescs.bug_when, " . + " longdescs.thetext " . + "FROM longdescs, profiles " . + "WHERE profiles.userid = longdescs.who " . + " AND longdescs.bug_id = $id "); + + if ($start && $start =~ /[1-9]/) { + # If the start is all zeros, then don't do this (because we want to + # not emit a leading "Additional Comments" line in that case.) + $query .= "AND longdescs.bug_when > '$start'"; + $count = 1; + } + if ($end) { + $query .= "AND longdescs.bug_when <= '$end'"; + } + + $query .= "ORDER BY longdescs.bug_when"; + SendSQL($query); + while (MoreSQLData()) { + my ($who, $email, $when, $text) = (FetchSQLData()); + if ($count) { + $result .= "<BR><BR><I>------- Additional Comments From "; + if ($who) { + $result .= qq{<A HREF="mailto:$email">$who</A> } . + time2str("%Y-%m-%d %H:%M", str2time($when)) . + " -------</I><BR>\n"; + } else { + $result .= qq{<A HREF="mailto:$email">$email</A> } . + time2str("%Y-%m-%d %H:%M", str2time($when)) . + " -------</I><BR>\n"; + } + } + $result .= "<PRE>" . quoteUrls(\%knownattachments, $text) . "</PRE>\n"; + $count++; + } + + return $result; +} # Fills in a hashtable with info about the columns for the given table in the # database. The hashtable has the following entries: @@ -540,36 +837,82 @@ sub UserInGroup { return 0; } +sub GroupExists { + my ($groupname) = (@_); + ConnectToDatabase(); + SendSQL("select count(*) from groups where name=" . SqlQuote($groupname)); + my $count = FetchOneColumn(); + return $count; +} + +# Determines if the given bug_status string represents an "Opened" bug. This +# routine ought to be paramaterizable somehow, as people tend to introduce +# new states into Bugzilla. + +sub IsOpenedState { + my ($state) = (@_); + if ($state =~ /^(NEW|REOPENED|ASSIGNED)$/ || $state eq $::unconfirmedstate) { + return 1; + } + return 0; +} + sub RemoveVotes { - my ($id, $reason) = (@_); + my ($id, $who, $reason) = (@_); ConnectToDatabase(); - SendSQL("select profiles.login_name from votes, profiles where votes.bug_id = $id and profiles.userid = votes.who"); + my $whopart = ""; + if ($who) { + $whopart = " AND votes.who = $who"; + } + SendSQL("SELECT profiles.login_name, votes.count " . + "FROM votes, profiles " . + "WHERE votes.bug_id = $id " . + "AND profiles.userid = votes.who" . + $whopart); my @list; while (MoreSQLData()) { - push(@list, FetchOneColumn()); + my ($name, $count) = (FetchSQLData()); + push(@list, [$name, $count]); } if (0 < @list) { - if (open(SENDMAIL, "|/usr/lib/sendmail -t")) { - my %substs; - $substs{"to"} = join(',', @list); - $substs{"bugid"} = $id; - $substs{"reason"} = $reason; - print SENDMAIL PerformSubsts(Param("voteremovedmail"), \%substs); - close SENDMAIL; + foreach my $ref (@list) { + my ($name, $count) = (@$ref); + if (open(SENDMAIL, "|/usr/lib/sendmail -t")) { + my %substs; + $substs{"to"} = $name; + $substs{"bugid"} = $id; + $substs{"reason"} = $reason; + $substs{"count"} = $count; + my $msg = PerformSubsts(Param("voteremovedmail"), + \%substs); + print SENDMAIL $msg; + close SENDMAIL; + } } - SendSQL("delete from votes where bug_id = $id"); - SendSQL("update bugs set votes = 0, delta_ts=delta_ts where bug_id = $id"); + SendSQL("DELETE FROM votes WHERE bug_id = $id" . $whopart); + SendSQL("SELECT SUM(count) FROM votes WHERE bug_id = $id"); + my $v = FetchOneColumn(); + $v ||= 0; + SendSQL("UPDATE bugs SET votes = $v, delta_ts = delta_ts " . + "WHERE bug_id = $id"); } } - -sub Param { +sub Param ($) { my ($value) = (@_); if (defined $::param{$value}) { return $::param{$value}; } + + # See if it is a dynamically-determined param (can't be changed by user). + if ($value eq "commandmenu") { + return GetCommandMenu(); + } + if ($value eq "settingsmenu") { + return GetSettingsMenu(); + } # Um, maybe we haven't sourced in the params at all yet. if (stat("data/params")) { # Write down and restore the version # here. That way, we get around @@ -595,7 +938,6 @@ sub Param { die "Can't find param named $value"; } - sub PerformSubsts { my ($str, $substs) = (@_); $str =~ s/%([a-z]*)%/(defined $substs->{$1} ? $substs->{$1} : Param($1))/eg; diff --git a/help.html b/help.html index a16010ca6bc78a4af789d1e798b4839abe78fc51..d84bbfd451c259f461641dfec4d86056fc8ba406 100644 --- a/help.html +++ b/help.html @@ -63,3 +63,4 @@ to case any problem with other browsers. The lynx browser does work, but lynx seems to cache results of a .cgi. You'll sometimes need to press CONTROL-R to reload the screen to see an update. +</html> diff --git a/index.html b/index.html index 5ec3d5f9c7dc94570706347a6a967d53d8882ac8..566a1053b6e560567359c1465cefe3689f2855ca 100644 --- a/index.html +++ b/index.html @@ -75,9 +75,12 @@ But it all boils down to a choice of: <p> <a href="createaccount.cgi">Open a new Bugzilla account</a><br> <a href="relogin.cgi">Forget the currently stored login</a><br> -<a href="changepassword.cgi">Change password or user preferences</a><br> +<a href="userprefs.cgi">Change password or user preferences</a><br> <FORM METHOD=GET ACTION="show_bug.cgi"> <INPUT TYPE=SUBMIT VALUE="Find"> bug # <INPUT NAME=id SIZE=6></FORM> +<SCRIPT LANGUAGE="JavaScript"> +document.forms[0].id.focus() +</SCRIPT> </BODY> </HTML> diff --git a/long_list.cgi b/long_list.cgi index 897847468e10a27a1db2662722618281ed98cea8..e095c59ec8790f8f659b20eea27c0333cdc1c7d7 100755 --- a/long_list.cgi +++ b/long_list.cgi @@ -31,6 +31,7 @@ require "CGI.pl"; sub sillyness { my $zz; + $zz = $::legal_keywords; $zz = $::usergroupset; $zz = %::FORM; } @@ -41,6 +42,8 @@ PutHeader ("Full Text Bug Listing"); ConnectToDatabase(); quietly_check_login(); +GetVersionTable(); + my $generic_query = " select bugs.bug_id, @@ -59,7 +62,8 @@ select bugs.short_desc, bugs.target_milestone, bugs.qa_contact, - bugs.status_whiteboard + bugs.status_whiteboard, + bugs.keywords from bugs,profiles assign,profiles report where assign.userid = bugs.assigned_to and report.userid = bugs.reporter and bugs.groupset & $::usergroupset = bugs.groupset and"; @@ -73,7 +77,7 @@ foreach my $bug (split(/:/, $::FORM{'buglist'})) { my ($id, $product, $version, $platform, $opsys, $status, $severity, $priority, $resolution, $assigned, $reporter, $component, $url, $shortdesc, $target_milestone, $qa_contact, - $status_whiteboard) = (@row); + $status_whiteboard, $keywords) = (@row); print "<IMG SRC=\"1x1.gif\" WIDTH=1 HEIGHT=80 ALIGN=LEFT>\n"; print "<TABLE WIDTH=100%>\n"; print "<TD COLSPAN=4><TR><DIV ALIGN=CENTER><B><FONT =\"+3\">" . @@ -104,12 +108,15 @@ foreach my $bug (split(/:/, $::FORM{'buglist'})) { print "<TR><TD COLSPAN=6><B>URL:</B> "; print "<A HREF=\"" . $url . "\">" . html_quote($url) . "</A>\n"; print "<TR><TD COLSPAN=6><B>Summary:</B> " . html_quote($shortdesc) . "\n"; + if (@::legal_keywords) { + print "<TR><TD><B>Keywords: </B>$keywords</TD></TR>\n"; + } if (Param("usestatuswhiteboard")) { print "<TR><TD COLSPAN=6><B>Status Whiteboard:" . html_quote($status_whiteboard) . "\n"; } print "<TR><TD><B>Description:</B>\n</TABLE>\n"; - print "<PRE>" . html_quote(GetLongDescription($bug)) . "</PRE>\n"; + print GetLongDescriptionAsHTML($bug); print "<HR>\n"; } } diff --git a/new_comment.cgi b/new_comment.cgi index e561292eac67af896bf59b698ee5b1a970360673..e034f3587d565d1dede681989f3cc1e7612c151e 100755 --- a/new_comment.cgi +++ b/new_comment.cgi @@ -29,11 +29,21 @@ foreach $pair (@pairs) ($name, $value) = split(/=/, $pair); $value =~ tr/+/ /; + $value =~ s/^(\s*)//s; + $value =~ s/(\s*)$//s; $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; $FORM{$name} = $value; } -open(COMMENTS, ">>data/comments"); $c=$FORM{"comment"}; +if ( (!defined $c) || ($c eq '') ) { + print "Content-type: text/html\n\n"; + print "<TITLE>Nothing on your mind?</TITLE>"; + print "<H1>Does your mind draw a blank?</H1>"; + print "<H2> Hit back, and try again...</H2>"; + exit 0; +} + +open(COMMENTS, ">>data/comments"); print COMMENTS $FORM{"comment"} . "\n"; close(COMMENTS); print "Content-type: text/html\n\n"; diff --git a/oracle/CVS/Entries b/oracle/CVS/Entries new file mode 100644 index 0000000000000000000000000000000000000000..178481050188cf00d7d9cd5a11e43ab8fab9294f --- /dev/null +++ b/oracle/CVS/Entries @@ -0,0 +1 @@ +D diff --git a/oracle/CVS/Repository b/oracle/CVS/Repository new file mode 100644 index 0000000000000000000000000000000000000000..5802d4b7b6472788bc5aabac3a92d82451e782f0 --- /dev/null +++ b/oracle/CVS/Repository @@ -0,0 +1 @@ +mozilla/webtools/bugzilla/oracle diff --git a/oracle/CVS/Root b/oracle/CVS/Root new file mode 100644 index 0000000000000000000000000000000000000000..cdb6f4a0739a0dc53e628026726036377dec3637 --- /dev/null +++ b/oracle/CVS/Root @@ -0,0 +1 @@ +:pserver:anonymous@cvs-mirror.mozilla.org:/cvsroot diff --git a/post_bug.cgi b/post_bug.cgi index 668baf2e7680c509acac114c9cdb736b2daf3c48..5695e5f0c220f9da705b961864a89980a16c9099 100755 --- a/post_bug.cgi +++ b/post_bug.cgi @@ -19,7 +19,8 @@ # Rights Reserved. # # Contributor(s): Terry Weissman <terry@mozilla.org> - +# Dan Mosedale <dmose@mozilla.org> +# Joe Robins <jmrobins@tgix.com> use diagnostics; use strict; @@ -33,11 +34,22 @@ sub sillyness { my $zz; $zz = $::buffer; $zz = %::COOKIE; + $zz = %::components; + $zz = %::versions; + $zz = @::legal_bug_status; + $zz = @::legal_opsys; + $zz = @::legal_platform; + $zz = @::legal_priority; + $zz = @::legal_product; + $zz = @::legal_severity; + $zz = %::target_milestone; } + confirm_login(); -print "Set-Cookie: PLATFORM=$::FORM{'product'} ; path=/ ; expires=Sun, 30-Jun-2029 00:00:00 GMT\n"; -print "Set-Cookie: VERSION-$::FORM{'product'}=$::FORM{'version'} ; path=/ ; expires=Sun, 30-Jun-2029 00:00:00 GMT\n"; +print "Set-Cookie: PLATFORM=$::FORM{'product'} ; path=/ ; expires=Sun, 30-Jun-2029 00:00:00 GMT\n" if ( exists $::FORM{'product'} ); +print "Set-Cookie: VERSION-$::FORM{'product'}=$::FORM{'version'} ; path=/ ; expires=Sun, 30-Jun-2029 00:00:00 GMT\n" if ( exists $::FORM{'product'} && exists $::FORM{'version'} ); + print "Content-type: text/html\n\n"; if (defined $::FORM{'maketemplate'}) { @@ -49,6 +61,7 @@ if (defined $::FORM{'maketemplate'}) { print "If you put a bookmark <a href=\"$url\">to this link</a>, it will\n"; print "bring up the submit-a-new-bug page with the fields initialized\n"; print "as you've requested.\n"; + PutFooter(); exit; } @@ -57,21 +70,28 @@ PutHeader("Posting Bug -- Please wait", "Posting Bug", "One moment please..."); umask 0; ConnectToDatabase(); +my $product = $::FORM{'product'}; + +if(Param("usebuggroupsentry") && GroupExists($product)) { + if(!UserInGroup($product)) { + print "<H1>Permission denied.</H1>\n"; + print "Sorry; you do not have the permissions necessary to enter\n"; + print "a bug against this product.\n"; + print "<P>\n"; + PutFooter(); + exit; + } +} + if (!defined $::FORM{'component'} || $::FORM{'component'} eq "") { - print "You must choose a component that corresponds to this bug. If\n"; - print "necessary, just guess. But please hit the <B>Back</B> button\n"; - print "and choose a component.\n"; - exit 0 + PuntTryAgain("You must choose a component that corresponds to this bug. " . + "If necessary, just guess."); } - if (!defined $::FORM{'short_desc'} || trim($::FORM{'short_desc'}) eq "") { - print "You must enter a summary for this bug. Please hit the\n"; - print "<B>Back</B> button and try again.\n"; - exit; + PuntTryAgain("You must enter a summary for this bug."); } - my $forceAssignedOK = 0; if ($::FORM{'assigned_to'} eq "") { SendSQL("select initialowner from components where program=" . @@ -88,7 +108,7 @@ $::FORM{'reporter'} = DBNameToIdAndCheck($::FORM{'reporter'}); my @bug_fields = ("reporter", "product", "version", "rep_platform", "bug_severity", "priority", "op_sys", "assigned_to", "bug_status", "bug_file_loc", "short_desc", "component", - "status_whiteboard", "target_milestone"); + "target_milestone"); if (Param("useqacontact")) { SendSQL("select initialqacontact from components where program=" . @@ -101,7 +121,45 @@ if (Param("useqacontact")) { } } +if (exists $::FORM{'bug_status'}) { + if (!UserInGroup("canedit") && !UserInGroup("canconfirm")) { + delete $::FORM{'bug_status'}; + } +} +if (!exists $::FORM{'bug_status'}) { + $::FORM{'bug_status'} = $::unconfirmedstate; + SendSQL("SELECT votestoconfirm FROM products WHERE product = " . + SqlQuote($::FORM{'product'})); + if (!FetchOneColumn()) { + $::FORM{'bug_status'} = "NEW"; + } +} + +if (!exists $::FORM{'target_milestone'}) { + SendSQL("SELECT defaultmilestone FROM products " . + "WHERE product = " . SqlQuote($::FORM{'product'})); + $::FORM{'target_milestone'} = FetchOneColumn(); +} + +if ( Param("strictvaluechecks") ) { + GetVersionTable(); + CheckFormField(\%::FORM, 'reporter'); + CheckFormField(\%::FORM, 'product', \@::legal_product); + CheckFormField(\%::FORM, 'version', \@{$::versions{$::FORM{'product'}}}); + CheckFormField(\%::FORM, 'target_milestone', + \@{$::target_milestone{$::FORM{'product'}}}); + CheckFormField(\%::FORM, 'rep_platform', \@::legal_platform); + CheckFormField(\%::FORM, 'bug_severity', \@::legal_severity); + CheckFormField(\%::FORM, 'priority', \@::legal_priority); + CheckFormField(\%::FORM, 'op_sys', \@::legal_opsys); + CheckFormFieldDefined(\%::FORM, 'assigned_to'); + CheckFormField(\%::FORM, 'bug_status', \@::legal_bug_status); + CheckFormFieldDefined(\%::FORM, 'bug_file_loc'); + CheckFormField(\%::FORM, 'component', + \@{$::components{$::FORM{'product'}}}); + CheckFormFieldDefined(\%::FORM, 'comment'); +} my @used_fields; foreach my $f (@bug_fields) { @@ -109,13 +167,16 @@ foreach my $f (@bug_fields) { push (@used_fields, $f); } } +if (exists $::FORM{'bug_status'} && $::FORM{'bug_status'} ne $::unconfirmedstate) { + push(@used_fields, "everconfirmed"); + $::FORM{'everconfirmed'} = 1; +} -my $query = "insert into bugs (\n" . join(",\n", @used_fields) . ", -creation_ts, long_desc ) -values ( +my $query = "INSERT INTO bugs (\n" . join(",\n", @used_fields) . ", +creation_ts, groupset) +VALUES ( "; - foreach my $field (@used_fields) { $query .= SqlQuote($::FORM{$field}) . ",\n"; } @@ -125,7 +186,20 @@ $comment =~ s/\r\n/\n/g; # Get rid of windows-style line endings. $comment =~ s/\r/\n/g; # Get rid of mac-style line endings. $comment = trim($comment); -$query .= "now(), " . SqlQuote($comment) . " )\n"; +$query .= "now(), 0"; + +foreach my $b (grep(/^bit-\d*$/, keys %::FORM)) { + if ($::FORM{$b}) { + my $v = substr($b, 4); + $query .= " + $v"; # Carefully written so that the math is + # done by MySQL, which can handle 64-bit math, + # and not by Perl, which I *think* can not. + } +} + + + +$query .= ")\n"; my %ccids; @@ -147,6 +221,9 @@ SendSQL($query); SendSQL("select LAST_INSERT_ID()"); my $id = FetchOneColumn(); +SendSQL("INSERT INTO longdescs (bug_id, who, bug_when, thetext) VALUES " . + "($id, $::FORM{'reporter'}, now(), " . SqlQuote($comment) . ")"); + foreach my $person (keys %ccids) { SendSQL("insert into cc (bug_id, who) values ($id, $person)"); } @@ -159,4 +236,5 @@ print "<BR><A HREF=\"createattachment.cgi?id=$id\">Attach a file to this bug</a> navigation_header(); +PutFooter(); exit; diff --git a/process_bug.cgi b/process_bug.cgi index 4bd800f1e0ceb27103d9f3826b1135d00952006b..22dff72e9141f64ca1a83f0aead5e85cd41dd486 100755 --- a/process_bug.cgi +++ b/process_bug.cgi @@ -19,19 +19,31 @@ # Rights Reserved. # # Contributor(s): Terry Weissman <terry@mozilla.org> +# Dan Mosedale <dmose@mozilla.org> use diagnostics; use strict; +my $UserInEditGroupSet = -1; +my $UserInCanConfirmGroupSet = -1; + require "CGI.pl"; +use RelationSet; # Shut up misguided -w warnings about "used only once": use vars %::versions, %::components, - %::COOKIE; + %::COOKIE, + %::keywordsbyname, + %::legal_keywords, + %::legal_opsys, + %::legal_platform, + %::legal_priority, + %::target_milestone, + %::legal_severity; -confirm_login(); +my $whoid = confirm_login(); print "Content-type: text/html\n\n"; @@ -39,16 +51,45 @@ PutHeader ("Bug processed"); GetVersionTable(); +if ( Param("strictvaluechecks") ) { + CheckFormFieldDefined(\%::FORM, 'product'); + CheckFormFieldDefined(\%::FORM, 'version'); + CheckFormFieldDefined(\%::FORM, 'component'); + + # check if target milestone is defined - matthew@zeroknowledge.com + if ( Param("usetargetmilestone") ) { + CheckFormFieldDefined(\%::FORM, 'target_milestone'); + } +} + if ($::FORM{'product'} ne $::dontchange) { + if ( Param("strictvaluechecks") ) { + CheckFormField(\%::FORM, 'product', \@::legal_product); + } my $prod = $::FORM{'product'}; + + # note that when this script is called from buglist.cgi (rather + # than show_bug.cgi), it's possible that the product will be changed + # but that the version and/or component will be set to + # "--dont_change--" but still happen to be correct. in this case, + # the if statement will incorrectly trigger anyway. this is a + # pretty weird case, and not terribly unreasonable behavior, but + # worthy of a comment, perhaps. + # my $vok = lsearch($::versions{$prod}, $::FORM{'version'}) >= 0; my $cok = lsearch($::components{$prod}, $::FORM{'component'}) >= 0; - if (!$vok || !$cok) { - print "<H1>Changing product means changing version and component.</H1>\n"; - print "You have chosen a new product, and now the version and/or\n"; + + my $mok = 1; # so it won't affect the 'if' statement if milestones aren't used + if ( Param("usetargetmilestone") ) { + $mok = lsearch($::target_milestone{$prod}, $::FORM{'target_milestone'}) >= 0; + } + + if (!$vok || !$cok || !$mok) { + print "<H1>Changing product means changing version, target milestone and component.</H1>\n"; + print "You have chosen a new product, and now the version, target milestone and/or\n"; print "component fields are not correct. (Or, possibly, the bug did\n"; - print "not have a valid component or version field in the first place.)\n"; - print "Anyway, please set the version and component now.<p>\n"; + print "not have a valid target milestone, component or version field in the first place.)\n"; + print "Anyway, please set the version, target milestone and component now.<p>\n"; print "<form>\n"; print "<table>\n"; print "<tr>\n"; @@ -58,12 +99,19 @@ if ($::FORM{'product'} ne $::dontchange) { print "<td align=right><b>Version:</b></td>\n"; print "<td>" . Version_element($::FORM{'version'}, $prod) . "</td>\n"; print "</tr><tr>\n"; + + if ( Param("usetargetmilestone") ) { + print "<td align=right><b>Target Milestone:</b></td>\n"; + print "<td>" . Milestone_element($::FORM{'target_milestone'}, $prod) . "</td>\n"; + print "</tr><tr>\n"; + } + print "<td align=right><b>Component:</b></td>\n"; print "<td>" . Component_element($::FORM{'component'}, $prod) . "</td>\n"; print "</tr>\n"; print "</table>\n"; foreach my $i (keys %::FORM) { - if ($i ne 'version' && $i ne 'component') { + if ($i ne 'version' && $i ne 'component' && $i ne 'target_milestone') { print "<input type=hidden name=$i value=\"" . value_quote($::FORM{$i}) . "\">\n"; } @@ -72,17 +120,127 @@ if ($::FORM{'product'} ne $::dontchange) { print "</form>\n"; print "</hr>\n"; print "<a href=query.cgi>Cancel all this and go to the query page.</a>\n"; + PutFooter(); exit; } } +# Checks that the user is allowed to change the given field. Actually, right +# now, the rules are pretty simple, and don't look at the field itself very +# much, but that could be enhanced. + +my $lastbugid = 0; +my $ownerid; +my $reporterid; +my $qacontactid; + +sub CheckCanChangeField { + my ($f, $bugid, $oldvalue, $newvalue) = (@_); + if ($f eq "assigned_to" || $f eq "reporter" || $f eq "qa_contact") { + if ($oldvalue =~ /^\d+$/) { + if ($oldvalue == 0) { + $oldvalue = ""; + } else { + $oldvalue = DBID_to_name($oldvalue); + } + } + } + if ($oldvalue eq $newvalue) { + return 1; + } + if (trim($oldvalue) eq trim($newvalue)) { + return 1; + } + if ($f =~ /^longdesc/) { + return 1; + } + if ($UserInEditGroupSet < 0) { + $UserInEditGroupSet = UserInGroup("editbugs"); + } + if ($UserInEditGroupSet) { + return 1; + } + if ($lastbugid != $bugid) { + SendSQL("SELECT reporter, assigned_to, qa_contact FROM bugs " . + "WHERE bug_id = $bugid"); + ($reporterid, $ownerid, $qacontactid) = (FetchSQLData()); + } + if ($f eq "bug_status" && $newvalue ne $::unconfirmedstate && + IsOpenedState($newvalue)) { + + # Hmm. They are trying to set this bug to some opened state + # that isn't the UNCONFIRMED state. Are they in the right + # group? Or, has it ever been confirmed? If not, then this + # isn't legal. + + if ($UserInCanConfirmGroupSet < 0) { + $UserInCanConfirmGroupSet = UserInGroup("canconfirm"); + } + if ($UserInCanConfirmGroupSet) { + return 1; + } + SendSQL("SELECT everconfirmed FROM bugs WHERE bug_id = $bugid"); + my $everconfirmed = FetchOneColumn(); + if ($everconfirmed) { + return 1; + } + } elsif ($reporterid eq $whoid || $ownerid eq $whoid || + $qacontactid eq $whoid) { + return 1; + } + SendSQL("UNLOCK TABLES"); + $oldvalue = value_quote($oldvalue); + $newvalue = value_quote($newvalue); + print PuntTryAgain(qq{ +Only the owner or submitter of the bug, or a sufficiently +empowered user, may make that change to the $f field. +<TABLE> +<TR><TH ALIGN="right">Old value:</TH><TD>$oldvalue</TD></TR> +<TR><TH ALIGN="right">New value:</TH><TD>$newvalue</TD></TR> +</TABLE> +}); + PutFooter(); + exit(); +} + + + + + + my @idlist; if (defined $::FORM{'id'}) { + + # since this means that we were called from show_bug.cgi, now is a good + # time to do a whole bunch of error checking that can't easily happen when + # we've been called from buglist.cgi, because buglist.cgi only tweaks + # values that have been changed instead of submitting all the new values. + # (XXX those error checks need to happen too, but implementing them + # is more work in the current architecture of this script...) + # + if ( Param('strictvaluechecks') ) { + CheckFormField(\%::FORM, 'rep_platform', \@::legal_platform); + CheckFormField(\%::FORM, 'priority', \@::legal_priority); + CheckFormField(\%::FORM, 'bug_severity', \@::legal_severity); + CheckFormField(\%::FORM, 'component', + \@{$::components{$::FORM{'product'}}}); + CheckFormFieldDefined(\%::FORM, 'bug_file_loc'); + CheckFormFieldDefined(\%::FORM, 'short_desc'); + CheckFormField(\%::FORM, 'product', \@::legal_product); + CheckFormField(\%::FORM, 'version', + \@{$::versions{$::FORM{'product'}}}); + CheckFormField(\%::FORM, 'op_sys', \@::legal_opsys); + CheckFormFieldDefined(\%::FORM, 'longdesclength'); + CheckPosInt($::FORM{'id'}); + } push @idlist, $::FORM{'id'}; } else { foreach my $i (keys %::FORM) { if ($i =~ /^id_/) { + if ( Param('strictvaluechecks') ) { + CheckPosInt(substr($i, 3)); + } push @idlist, substr($i, 3); } } @@ -92,6 +250,8 @@ if (!defined $::FORM{'who'}) { $::FORM{'who'} = $::COOKIE{'Bugzilla_login'}; } +# the common updates to all bugs in @idlist start here +# print "<TITLE>Update Bug " . join(" ", @idlist) . "</TITLE>\n"; if (defined $::FORM{'id'}) { navigation_header(); @@ -106,11 +266,32 @@ sub DoComma { $::comma = ","; } +sub DoConfirm { + if ($UserInEditGroupSet < 0) { + $UserInEditGroupSet = UserInGroup("editbugs"); + } + if ($UserInCanConfirmGroupSet < 0) { + $UserInCanConfirmGroupSet = UserInGroup("canconfirm"); + } + if ($UserInEditGroupSet || $UserInCanConfirmGroupSet) { + DoComma(); + $::query .= "everconfirmed = 1"; + } +} + + sub ChangeStatus { my ($str) = (@_); if ($str ne $::dontchange) { DoComma(); - $::query .= "bug_status = '$str'"; + if (IsOpenedState($str)) { + $::query .= "bug_status = IF(everconfirmed = 1, '$str', '$::unconfirmedstate')"; + } else { + $::query .= "bug_status = '$str'"; + } + $::FORM{'bug_status'} = $str; # Used later for call to + # CheckCanChangeField to make sure this + # is really kosher. } } @@ -122,6 +303,33 @@ sub ChangeResolution { } } +# +# This function checks if there is a comment required for a specific +# function and tests, if the comment was given. +# If comments are required for functions is defined by params. +# +sub CheckonComment( $ ) { + my ($function) = (@_); + + # Param is 1 if comment should be added ! + my $ret = Param( "commenton" . $function ); + + # Allow without comment in case of undefined Params. + $ret = 0 unless ( defined( $ret )); + + if( $ret ) { + if (!defined $::FORM{'comment'} || $::FORM{'comment'} =~ /^\s*$/) { + # No comment - sorry, action not allowed ! + PuntTryAgain("You have to specify a <b>comment</b> on this " . + "change. Please give some words " . + "on the reason for your change."); + } else { + $ret = 0; + } + } + return( ! $ret ); # Return val has to be inverted +} + my $foundbit = 0; foreach my $b (grep(/^bit-\d*$/, keys %::FORM)) { @@ -138,10 +346,9 @@ foreach my $b (grep(/^bit-\d*$/, keys %::FORM)) { } } - -foreach my $field ("rep_platform", "priority", "bug_severity", "url", +foreach my $field ("rep_platform", "priority", "bug_severity", "summary", "component", "bug_file_loc", "short_desc", - "product", "version", "component", "op_sys", + "product", "version", "op_sys", "target_milestone", "status_whiteboard") { if (defined $::FORM{$field}) { if ($::FORM{$field} ne $::dontchange) { @@ -167,35 +374,77 @@ if (defined $::FORM{'qa_contact'}) { ConnectToDatabase(); +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. 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'}) { + $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'); +} SWITCH: for ($::FORM{'knob'}) { /^none$/ && do { last SWITCH; }; - /^accept$/ && do { + /^confirm$/ && CheckonComment( "confirm" ) && do { + DoConfirm(); + ChangeStatus('NEW'); + last SWITCH; + }; + /^accept$/ && CheckonComment( "accept" ) && do { + DoConfirm(); ChangeStatus('ASSIGNED'); last SWITCH; }; - /^clearresolution$/ && do { + /^clearresolution$/ && CheckonComment( "clearresolution" ) && do { ChangeResolution(''); last SWITCH; }; - /^resolve$/ && do { + /^resolve$/ && CheckonComment( "resolve" ) && do { ChangeStatus('RESOLVED'); ChangeResolution($::FORM{'resolution'}); last SWITCH; }; - /^reassign$/ && do { + /^reassign$/ && CheckonComment( "reassign" ) && do { + if ($::FORM{'andconfirm'}) { + DoConfirm(); + } ChangeStatus('NEW'); DoComma(); + if ( Param("strictvaluechecks") ) { + if ( !defined$::FORM{'assigned_to'} || + trim($::FORM{'assigned_to'}) eq "") { + PuntTryAgain("You cannot reassign to a bug to nobody. Unless " . + "you intentionally cleared out the " . + "\"Reassign bug to\" field, " . + Param("browserbugmessage")); + } + } my $newid = DBNameToIdAndCheck($::FORM{'assigned_to'}); $::query .= "assigned_to = $newid"; last SWITCH; }; - /^reassignbycomponent$/ && do { + /^reassignbycomponent$/ && CheckonComment( "reassignbycomponent" ) && do { + if ($::FORM{'product'} eq $::dontchange) { + PuntTryAgain("You must specify a product to help determine the " . + "new owner of these bugs."); + } if ($::FORM{'component'} eq $::dontchange) { - print "You must specify a component whose owner should get\n"; - print "assigned these bugs.\n"; - exit 0 + PuntTryAgain("You must specify a component whose owner should " . + "get assigned these bugs."); + } + if ($::FORM{'compconfirm'}) { + DoConfirm(); } ChangeStatus('NEW'); SendSQL("select initialowner from components where program=" . @@ -205,60 +454,102 @@ SWITCH: for ($::FORM{'knob'}) { my $newid = DBNameToIdAndCheck($newname, 1); DoComma(); $::query .= "assigned_to = $newid"; + if (Param("useqacontact")) { + SendSQL("select initialqacontact from components where program=" . + SqlQuote($::FORM{'product'}) . + " and value=" . SqlQuote($::FORM{'component'})); + my $qacontact = FetchOneColumn(); + if (defined $qacontact && $qacontact ne "") { + my $newqa = DBNameToIdAndCheck($qacontact, 1); + DoComma(); + $::query .= "qa_contact = $newqa"; + } + } last SWITCH; }; - /^reopen$/ && do { + /^reopen$/ && CheckonComment( "reopen" ) && do { ChangeStatus('REOPENED'); + ChangeResolution(''); last SWITCH; }; - /^verify$/ && do { + /^verify$/ && CheckonComment( "verify" ) && do { ChangeStatus('VERIFIED'); last SWITCH; }; - /^close$/ && do { + /^close$/ && CheckonComment( "close" ) && do { ChangeStatus('CLOSED'); last SWITCH; }; - /^duplicate$/ && do { + /^duplicate$/ && CheckonComment( "duplicate" ) && do { ChangeStatus('RESOLVED'); ChangeResolution('DUPLICATE'); + if ( Param('strictvaluechecks') ) { + CheckFormFieldDefined(\%::FORM,'dup_id'); + } my $num = trim($::FORM{'dup_id'}); - if ($num !~ /^[0-9]*$/) { - print "You must specify a bug number of which this bug is a\n"; - print "duplicate. The bug has not been changed.\n"; - exit; + SendSQL("SELECT bug_id FROM bugs WHERE bug_id = " . SqlQuote($num)); + $num = FetchOneColumn(); + if (!$num) { + PuntTryAgain("You must specify a bug number of which this bug " . + "is a duplicate. The bug has not been changed.") } - if ($::FORM{'dup_id'} == $::FORM{'id'}) { - print "Nice try, $::FORM{'who'}. But it doesn't really make sense to mark a\n"; - print "bug as a duplicate of itself, does it?\n"; - exit; + if (!defined($::FORM{'id'}) || $num == $::FORM{'id'}) { + PuntTryAgain("Nice try, $::FORM{'who'}. But it doesn't really ". + "make sense to mark a bug as a duplicate of " . + "itself, does it?"); } - AppendComment($::FORM{'dup_id'}, $::FORM{'who'}, "*** Bug $::FORM{'id'} has been marked as a duplicate of this bug. ***"); - $::FORM{'comment'} .= "\n\n*** This bug has been marked as a duplicate of $::FORM{'dup_id'} ***"; + AppendComment($num, $::FORM{'who'}, "*** Bug $::FORM{'id'} has been marked as a duplicate of this bug. ***"); + if ( Param('strictvaluechecks') ) { + CheckFormFieldDefined(\%::FORM,'comment'); + } + $::FORM{'comment'} .= "\n\n*** This bug has been marked as a duplicate of $num ***"; - print "<TABLE BORDER=1><TD><H2>Notation added to bug $::FORM{'dup_id'}</H2>\n"; - system("./processmail $::FORM{'dup_id'} $::FORM{'who'}"); - print "<TD><A HREF=\"show_bug.cgi?id=$::FORM{'dup_id'}\">Go To BUG# $::FORM{'dup_id'}</A></TABLE>\n"; + print "<TABLE BORDER=1><TD><H2>Notation added to bug $num</H2>\n"; + system("./processmail $num $::FORM{'who'}"); + print "<TD><A HREF=\"show_bug.cgi?id=$num\">Go To BUG# $num</A></TABLE>\n"; last SWITCH; }; # default print "Unknown action $::FORM{'knob'}!\n"; + PutFooter(); exit; } if ($#idlist < 0) { - print "You apparently didn't choose any bugs to modify.\n"; - print "<p>Click <b>Back</b> and try again.\n"; - exit; + PuntTryAgain("You apparently didn't choose any bugs to modify."); +} + + +my @keywordlist; +my %keywordseen; + +if ($::FORM{'keywords'}) { + foreach my $keyword (split(/[\s,]+/, $::FORM{'keywords'})) { + if ($keyword eq '') { + next; + } + my $i = $::keywordsbyname{$keyword}; + if (!$i) { + PuntTryAgain("Unknown keyword named <code>$keyword</code>. " . + "<P>The legal keyword names are " . + "<A HREF=describekeywords.cgi>" . + "listed here</A>."); + } + if (!$keywordseen{$i}) { + push(@keywordlist, $i); + $keywordseen{$i} = 1; + } + } } -if ($::comma eq "") { +my $keywordaction = $::FORM{'keywordaction'} || "makeexact"; + +if ($::comma eq "" && 0 == @keywordlist && $keywordaction ne "makeexact") { if (!defined $::FORM{'comment'} || $::FORM{'comment'} =~ /^\s*$/) { - print "Um, you apparently did not change anything on the selected\n"; - print "bugs. <p>Click <b>Back</b> and try again.\n"; - exit + PuntTryAgain("Um, you apparently did not change anything on the " . + "selected bugs."); } } @@ -288,24 +579,47 @@ sub SnapShotDeps { } -my $whoid = DBNameToIdAndCheck($::FORM{'who'}); my $timestamp; sub LogDependencyActivity { my ($i, $oldstr, $target, $me) = (@_); my $newstr = SnapShotDeps($i, $target, $me); if ($oldstr ne $newstr) { - SendSQL("insert into bugs_activity (bug_id,who,bug_when,field,oldvalue,newvalue) values ($i,$whoid,$timestamp,'$target','$oldstr','$newstr')"); + my $fieldid = GetFieldID($target); + SendSQL("INSERT INTO bugs_activity " . + "(bug_id,who,bug_when,fieldid,oldvalue,newvalue) VALUES " . + "($i,$whoid,$timestamp,$fieldid,'$oldstr','$newstr')"); return 1; } return 0; } +delete $::FORM{'resolution'}; # Make sure we don't test the resolution + # against our permissions; we've already done + # that kind of testing, and this form field + # is actually usually not used. + +# this loop iterates once for each bug to be processed (eg when this script +# is called with multiple bugs selected from buglist.cgi instead of +# show_bug.cgi). +# foreach my $id (@idlist) { my %dependencychanged; - SendSQL("lock tables bugs write, bugs_activity write, cc write, profiles write, dependencies write, votes write"); + my $write = "WRITE"; # Might want to make a param to control + # whether we do LOW_PRIORITY ... + SendSQL("LOCK TABLES bugs $write, bugs_activity $write, cc $write, " . + "profiles $write, dependencies $write, votes $write, " . + "keywords $write, longdescs $write, fielddefs $write, " . + "keyworddefs READ, groups READ, attachments READ"); my @oldvalues = SnapShotBug($id); + my $i = 0; + foreach my $col (@::log_columns) { + if (exists $::FORM{$col}) { + CheckCanChangeField($col, $id, $oldvalues[$i], $::FORM{$col}); + } + $i++; + } if (defined $::FORM{'delta_ts'} && $::FORM{'delta_ts'} ne $delta_ts) { print " @@ -315,13 +629,14 @@ The changes made were: <p> "; DumpBugActivity($id, $delta_ts); - my $longdesc = GetLongDescription($id); + my $longdesc = GetLongDescriptionAsHTML($id); my $longchanged = 0; + if (length($longdesc) > $::FORM{'longdesclength'}) { $longchanged = 1; - print "<P>Added text to the long description:<blockquote><pre>"; - print html_quote(substr($longdesc, $::FORM{'longdesclength'})); - print "</pre></blockquote>\n"; + print "<P>Added text to the long description:<blockquote>"; + print substr($longdesc, $::FORM{'longdesclength'}); + print "</blockquote>\n"; } SendSQL("unlock tables"); print "You have the following choices: <ul>\n"; @@ -337,7 +652,7 @@ The changes made were: print ", except for the changes to the description"; } print qq{.</form>\n<li><a href="show_bug.cgi?id=$id">Throw away my changes, and go revisit bug $id</a></ul>\n}; - navigation_header(); + PutFooter(); exit; } @@ -357,9 +672,7 @@ The changes made were: SqlQuote($i)); my $comp = FetchOneColumn(); if ($comp ne $i) { - print "<H1>$i is not a legal bug number</H1>\n"; - print "<p>Click <b>Back</b> and try again.\n"; - exit; + PuntTryAgain("$i is not a legal bug number"); } if (!exists $seen{$i}) { push(@{$deps{$target}}, $i); @@ -373,11 +686,10 @@ The changes made were: while (MoreSQLData()) { my $t = FetchOneColumn(); if ($t == $id) { - print "<H1>Dependency loop detected!</H1>\n"; - print "The change you are making to dependencies\n"; - print "has caused a circular dependency chain.\n"; - print "<p>Click <b>Back</b> and try again.\n"; - exit; + PuntTryAgain("Dependency loop detected!<P>" . + "The change you are making to " . + "dependencies has caused a circular " . + "dependency chain."); } if (!exists $seen{$t}) { push @stack, $t; @@ -393,35 +705,80 @@ The changes made were: } } + if (@::legal_keywords) { + # There are three kinds of "keywordsaction": makeexact, add, delete. + # For makeexact, we delete everything, and then add our things. + # For add, we delete things we're adding (to make sure we don't + # end up having them twice), and then we add them. + # For delete, we just delete things on the list. + my $changed = 0; + if ($keywordaction eq "makeexact") { + SendSQL("DELETE FROM keywords WHERE bug_id = $id"); + $changed = 1; + } + foreach my $keyword (@keywordlist) { + if ($keywordaction ne "makeexact") { + SendSQL("DELETE FROM keywords + WHERE bug_id = $id AND keywordid = $keyword"); + $changed = 1; + } + if ($keywordaction ne "delete") { + SendSQL("INSERT INTO keywords + (bug_id, keywordid) VALUES ($id, $keyword)"); + $changed = 1; + } + } + if ($changed) { + SendSQL("SELECT keyworddefs.name + FROM keyworddefs, keywords + WHERE keywords.bug_id = $id + AND keyworddefs.id = keywords.keywordid + ORDER BY keyworddefs.name"); + my @list; + while (MoreSQLData()) { + push(@list, FetchOneColumn()); + } + SendSQL("UPDATE bugs SET keywords = " . + SqlQuote(join(', ', @list)) . + " WHERE bug_id = $id"); + } + } + my $query = "$basequery\nwhere bug_id = $id"; # print "<PRE>$query</PRE>\n"; if ($::comma ne "") { SendSQL($query); + SendSQL("select delta_ts from bugs where bug_id = $id"); + } else { + SendSQL("select now()"); } + $timestamp = FetchOneColumn(); if (defined $::FORM{'comment'}) { AppendComment($id, $::FORM{'who'}, $::FORM{'comment'}); } - if (defined $::FORM{'cc'} && ShowCcList($id) ne $::FORM{'cc'}) { - my %ccids; - foreach my $person (split(/[ ,]/, $::FORM{'cc'})) { - if ($person ne "") { - my $cid = DBNameToIdAndCheck($person); - $ccids{$cid} = 1; - } - } - - SendSQL("delete from cc where bug_id = $id"); - foreach my $ccid (keys %ccids) { - SendSQL("insert into cc (bug_id, who) values ($id, $ccid)"); - } + 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)"); } - - SendSQL("select delta_ts from bugs where bug_id = $id"); - $timestamp = FetchOneColumn(); + if (defined $::FORM{'dependson'}) { my $me = "blocked"; @@ -476,8 +833,15 @@ The changes made were: } + # get a snapshot of the newly set values out of the database, + # and then generate any necessary bug activity entries by seeing + # what has changed since before we wrote out the new values. + # my @newvalues = SnapShotBug($id); - foreach my $col (@::log_columns) { + + foreach my $c (@::log_columns) { + my $col = $c; # We modify it, don't want to modify array + # values in place. my $old = shift @oldvalues; my $new = shift @newvalues; if (!defined $old) { @@ -490,15 +854,18 @@ 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; + $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, + RemoveVotes($id, 0, "This bug has been moved to a different product"); } - $col = SqlQuote($col); + $col = GetFieldID($col); $old = SqlQuote($old); $new = SqlQuote($new); - my $q = "insert into bugs_activity (bug_id,who,bug_when,field,oldvalue,newvalue) values ($id,$whoid,$timestamp,$col,$old,$new)"; + my $q = "insert into bugs_activity (bug_id,who,bug_when,fieldid,oldvalue,newvalue) values ($id,$whoid,'$timestamp',$col,$old,$new)"; # puts "<pre>$q</pre>" SendSQL($q); } @@ -506,7 +873,7 @@ The changes made were: print "<TABLE BORDER=1><TD><H2>Changes to bug $id submitted</H2>\n"; SendSQL("unlock tables"); - system("./processmail $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)) { @@ -526,4 +893,5 @@ if (defined $::next_bug) { do "bug_form.pl"; } else { navigation_header(); + PutFooter(); } diff --git a/processmail b/processmail index eedfb033949d4504f06f8de4b2ce9680260c37f3..b813986809e4c5a34aae5eb97339a33402c59f6d 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); @@ -36,6 +39,8 @@ $::lockcount = 0; my $regenerate = 0; my $nametoexclude = ""; +my @forcecc; + sub Lock { if ($::lockcount <= 0) { $::lockcount = 0; @@ -98,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"; } @@ -197,21 +202,50 @@ sub GetBugText { $status_whiteboard = "StatusWhiteboard: $::bug{'status_whiteboard'}\n"; } - $::bug{'long_desc'} = GetLongDescription($id); + $::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); ++============================================================================+ +| @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< | ++----------------------------------------------------------------------------+ +| Bug #: @<<<<<<<<<<< Product: @<<<<<<<<<<<<<<<<<<<<<< | +| Status: @<<<<<<<<<<<<<<<<<< Version: @<<<<<<<<<<<<<<<<<<<<<< | +| Resolution: @<<<<<<<<<<<<<<<<<< Platform: @<<<<<<<<<<<<<<<<<<<<<< | +| Severity: @<<<<<<<<<<<<<<<<<< OS/Version: @<<<<<<<<<<<<<<<<<<<<<< | +| Priority: @<<<<<<<<<<<<<<<<<< Component: @<<<<<<<<<<<<<<<<<<<<<< | ++----------------------------------------------------------------------------+ +| Assigned To: @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< | +| Reported By: @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< | +| ~QA Contact: ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< | +| ~ CC list: ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< | ++----------------------------------------------------------------------------+ +| ~ Milestone: ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< | +|~ Whiteboard: ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< | +| URL: @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< | +|~Dependencies: ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< | ++============================================================================+ +| DESCRIPTION | +END + + my $prettymail = $^A . $::bug{'long_desc'}; + return $prettymail; + - return "Bug\#: $id + } else { + return "Bug\#: $id Product: $::bug{'product'} Version: $::bug{'version'} Platform: $::bug{'rep_platform'} @@ -224,16 +258,18 @@ 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'} "; +} } my $didexclude = 0; my %seen; +my @sentlist; sub fixaddresses { my ($field, $list) = (@_); my @result; @@ -276,15 +312,296 @@ sub Log { Unlock(); } + +sub FormatTriple { + my ($a, $b, $c) = (@_); + $^A = ""; + my $temp = formline << 'END', $a, $b, $c; +^>>>>>>>>>>>>>>>>>>|^<<<<<<<<<<<<<<<<<<<<<<<<<<<|^<<<<<<<<<<<<<<<<<<<<<<<<<<~~ +END + ; # This semicolon appeases my emacs editor macros. :-) + return $^A; +} + +sub FormatDouble { + my ($a, $b) = (@_); + $a .= ":"; + $^A = ""; + my $temp = formline << 'END', $a, $b; +^>>>>>>>>>>>>>>>>>> ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<~~ +END + ; # This semicolon appeases my emacs editor macros. :-) + return $^A; +} + + +sub NewProcessOneBug { + my ($id) = (@_); + + my @headerlist; + my %values; + my %defmailhead; + my %fielddescription; + + my $msg = ""; + + SendSQL("SELECT name, description, mailhead FROM fielddefs " . + "ORDER BY sortkey"); + while (MoreSQLData()) { + my ($field, $description, $mailhead) = (FetchSQLData()); + push(@headerlist, $field); + $defmailhead{$field} = $mailhead; + $fielddescription{$field} = $description; + } + SendSQL("SELECT " . join(',', @::log_columns) . ", lastdiffed, now() " . + "FROM bugs WHERE bug_id = $id"); + my @row = FetchSQLData(); + foreach my $i (@::log_columns) { + $values{$i} = shift(@row); + } + my ($start, $end) = (@row); + 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 " . + "WHERE votes.bug_id = $id AND profiles.userid = votes.who"); + while (MoreSQLData()) { + push(@voterlist, FetchOneColumn()); + } + + $values{'assigned_to'} = DBID_to_name($values{'assigned_to'}); + $values{'reporter'} = DBID_to_name($values{'reporter'}); + if ($values{'qa_contact'}) { + $values{'qa_contact'} = DBID_to_name($values{'qa_contact'}); + } + + my @diffs; + + + SendSQL("SELECT profiles.login_name, fielddefs.description, " . + " bug_when, oldvalue, newvalue " . + "FROM bugs_activity, fielddefs, profiles " . + "WHERE bug_id = $id " . + " AND fielddefs.fieldid = bugs_activity.fieldid " . + " AND profiles.userid = who " . + " AND bug_when > '$start' " . + " AND bug_when <= '$end' " . + "ORDER BY bug_when" + ); + + while (MoreSQLData()) { + my @row = FetchSQLData(); + push(@diffs, \@row); + } + + my $difftext = ""; + my $lastwho = ""; + foreach my $ref (@diffs) { + my ($who, $what, $when, $old, $new) = (@$ref); + if ($who ne $lastwho) { + $lastwho = $who; + $difftext .= "\n$who changed:\n\n"; + $difftext .= FormatTriple("What ", "Old Value", "New Value"); + $difftext .= ('-' x 76) . "\n"; + } + $difftext .= FormatTriple($what, $old, $new); + } + + $difftext = trim($difftext); + + + my $deptext = ""; + + my $resid = + + SendSQL("SELECT bugs_activity.bug_id, fielddefs.name, " . + " oldvalue, newvalue " . + "FROM bugs_activity, dependencies, fielddefs ". + "WHERE bugs_activity.bug_id = dependencies.dependson " . + " AND dependencies.blocked = $id " . + " AND fielddefs.fieldid = bugs_activity.fieldid" . + " AND (fielddefs.name = 'bug_status' " . + " OR fielddefs.name = 'resolution') " . + " AND bug_when > '$start' " . + " AND bug_when <= '$end' " . + "ORDER BY bug_when, bug_id"); + + my $thisdiff = ""; + my $lastbug = ""; + my $interestingchange = 0; + while (MoreSQLData()) { + my ($bug, $what, $old, $new) = (FetchSQLData()); + if ($bug ne $lastbug) { + if ($interestingchange) { + $deptext .= $thisdiff; + } + $lastbug = $bug; + $thisdiff = + "\nThis bug depends on bug $bug, which changed state:\n\n"; + $thisdiff .= FormatTriple("What ", "Old Value", "New Value"); + $thisdiff .= ('-' x 76) . "\n"; + $interestingchange = 0; + } + $thisdiff .= FormatTriple($fielddescription{$what}, $old, $new); + if ($what eq 'bug_status' && IsOpenedState($old) ne IsOpenedState($new)) { + $interestingchange = 1; + } + } + if ($interestingchange) { + $deptext .= $thisdiff; + } + + $deptext = trim($deptext); + + if ($deptext) { + $difftext = trim($difftext . "\n\n" . $deptext); + } + + + my $newcomments = GetLongDescriptionAsText($id, $start, $end); + + my $count = 0; + for my $person ($values{'assigned_to'}, $values{'reporter'}, + split(/,/, $values{'cc'}), + @voterlist, + @forcecc) { + $count++; + + NewProcessOnePerson($person, $count, \@headerlist, \%values, + \%defmailhead, \%fielddescription, $difftext, + $newcomments, $start, $id, 1); + } + + SendSQL("UPDATE bugs SET lastdiffed = '$end', delta_ts = delta_ts " . + "WHERE bug_id = $id"); +} + +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; + + if ($seen{$person}) { + return; + } + + 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() ) { + + NewProcessOnePerson(DBID_to_name($watcher), + $count, \@headerlist, \%values, + \%defmailhead, \%fielddescription, $difftext, + $newcomments, $start, $id, 0); + } + + } + + if (!$newemailtech || !Param('newemailtech')) { + return; + } + $seen{$person} = 1; + + + # 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 -ODeliveryMode=deferred -t") || + die "Can't open sendmail"; + + print SENDMAIL trim($msg) . "\n"; + close SENDMAIL; + push(@sentlist, $person); + +} + + sub ProcessOneBug { - my $i = $_[0]; - 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"; @@ -305,11 +622,13 @@ sub ProcessOneBug { foreach my $v (split(/,/, "$::bug{'cclist'},$::bug{'voterlist'}")) { push @combinedcc, $v; } + push (@combinedcc, (@forcecc)); my $cclist = fixaddresses("cc", \@combinedcc); my $logstr = "Bug $i $verb"; if ($tolist ne "" || $cclist ne "") { my %substs; + $substs{"fullbugreport"} = $text; # added ability to include the full bug report $substs{"to"} = $tolist; $substs{"cc"} = $cclist; $substs{"bugid"} = $i; @@ -326,31 +645,43 @@ sub ProcessOneBug { if (!$regenerate) { # Note: fixaddresses may result in a Cc: only. This seems # harmless. - open(SENDMAIL, "|/usr/lib/sendmail -t") || - die "Can't open sendmail"; + open(SENDMAIL, + "|/usr/lib/sendmail -ODeliveryMode=deferred -t") || + die "Can't open sendmail"; + print SENDMAIL $msg; close SENDMAIL; - $logstr = "$logstr; mail sent to $tolist, $cclist"; - print "<B>Email sent to:</B> $tolist $cclist\n"; - if ($didexclude) { - print "<B>Excluding:</B> $nametoexclude (<a href=changepassword.cgi>change your preferences</a> if you wish not to be excluded)\n"; + foreach my $n (split(/[, ]+/, "$tolist,$cclist")) { + if ($n ne "") { + push(@sentlist, $n); + } } + + $logstr = "$logstr; mail sent to $tolist, $cclist"; } } unlink($diffs); Log($logstr); } + if (@sentlist) { + print "<B>Email sent to:</B> " . join(", ", @sentlist) . "\n"; + if ($didexclude) { + print qq{<B>Excluding:</B> $nametoexclude (<a href="userprefs.cgi?bank=diffs">change your preferences</a> if you wish not to be excluded)\n}; + } + } rename($new, $old) || die "Can't rename $new to $old"; chmod 0666, $old; if ($regenerate) { print "$i "; } %seen = (); + @sentlist = (); } # Code starts here ConnectToDatabase(); +GetVersionTable(); Lock(); if (open(FID, "<data/nomail")) { @@ -360,13 +691,8 @@ if (open(FID, "<data/nomail")) { close FID; } -if (($#ARGV < 0) || ($#ARGV > 1)) { - print "Usage error: processmail {bugid} {nametoexclude}\nOr: processmail regenerate\n"; - exit; -} - # 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"); @@ -383,6 +709,18 @@ if ($ARGV[0] eq "regenerate") { exit; } +if ($#ARGV >= 0 && $ARGV[0] eq "-forcecc") { + shift(@ARGV); + foreach my $i (split(/,/, shift(@ARGV))) { + push(@forcecc, trim($i)); + } +} + +if (($#ARGV < 0) || ($#ARGV > 1)) { + print "Usage error: processmail {bugid} {nametoexclude}\nOr: processmail regenerate\n"; + exit; +} + if ($#ARGV == 1) { $nametoexclude = lc($ARGV[1]); } diff --git a/query.cgi b/query.cgi index c7d49ace89378f8d4f6cb1bfc02557cc62fd99d1..dedd5fb6ea94a23a21971255829150489465a563 100755 --- a/query.cgi +++ b/query.cgi @@ -26,18 +26,26 @@ use strict; require "CGI.pl"; +$::CheckOptionValues = 0; # It's OK if we have some bogus things in the + # pop-up lists here, from a remembered query + # that is no longer quite valid. We don't + # want to crap out in the query page. + # Shut up misguided -w warnings about "used only once": -use vars @::legal_resolution, - @::legal_product, +use vars + @::CheckOptionValues, + @::legal_resolution, @::legal_bug_status, - @::legal_priority, + @::legal_components, + @::legal_keywords, @::legal_opsys, @::legal_platform, - @::legal_components, - @::legal_versions, + @::legal_priority, + @::legal_product, @::legal_severity, @::legal_target_milestone, + @::legal_versions, @::log_columns, %::versions, %::components, @@ -48,73 +56,135 @@ if (defined $::FORM{"GoAheadAndLogIn"}) { # We got here from a login page, probably from relogin.cgi. We better # make sure the password is legit. confirm_login(); +} else { + quietly_check_login(); +} +my $userid = 0; +if (defined $::COOKIE{"Bugzilla_login"}) { + $userid = DBNameToIdAndCheck($::COOKIE{"Bugzilla_login"}); } -if (!defined $::COOKIE{"DEFAULTQUERY"}) { - $::COOKIE{"DEFAULTQUERY"} = Param("defaultquery"); +# Backwards compatability hack -- if there are any of the old QUERY_* +# cookies around, and we are logged in, then move them into the database +# and nuke the cookie. +if ($userid) { + my @oldquerycookies; + foreach my $i (keys %::COOKIE) { + if ($i =~ /^QUERY_(.*)$/) { + push(@oldquerycookies, [$1, $i, $::COOKIE{$i}]); + } + } + if (defined $::COOKIE{'DEFAULTQUERY'}) { + push(@oldquerycookies, [$::defaultqueryname, 'DEFAULTQUERY', + $::COOKIE{'DEFAULTQUERY'}]); + } + if (@oldquerycookies) { + foreach my $ref (@oldquerycookies) { + my ($name, $cookiename, $value) = (@$ref); + if ($value) { + my $qname = SqlQuote($name); + SendSQL("SELECT query FROM namedqueries " . + "WHERE userid = $userid AND name = $qname"); + my $query = FetchOneColumn(); + if (!$query) { + SendSQL("REPLACE INTO namedqueries " . + "(userid, name, query) VALUES " . + "($userid, $qname, " . SqlQuote($value) . ")"); + } + } + print "Set-Cookie: $cookiename= ; path=/ ; expires=Sun, 30-Jun-1980 00:00:00 GMT\n"; + } + } } + + -if (!defined $::buffer || $::buffer eq "") { - $::buffer = $::COOKIE{"DEFAULTQUERY"}; + +if ($::FORM{'nukedefaultquery'}) { + if ($userid) { + SendSQL("DELETE FROM namedqueries " . + "WHERE userid = $userid AND name = '$::defaultqueryname'"); + } + $::buffer = ""; } -use vars qw(%default); -my %type; -foreach my $name ("bug_status", "resolution", "assigned_to", "rep_platform", - "priority", "bug_severity", "product", "reporter", "op_sys", - "component", "version", "chfield", "chfieldfrom", - "chfieldto", "chfieldvalue", - "email1", "emailtype1", "emailreporter1", - "emailassigned_to1", "emailcc1", "emailqa_contact1", - "email2", "emailtype2", "emailreporter2", - "emailassigned_to2", "emailcc2", "emailqa_contact2", - "changedin", "votes", "short_desc", "short_desc_type", - "long_desc", "long_desc_type", "bug_file_loc", - "bug_file_loc_type", "status_whiteboard", - "status_whiteboard_type") { - $default{$name} = ""; - $type{$name} = 0; +my $userdefaultquery; +if ($userid) { + SendSQL("SELECT query FROM namedqueries " . + "WHERE userid = $userid AND name = '$::defaultqueryname'"); + $userdefaultquery = FetchOneColumn(); } +my %default; +my %type; -foreach my $item (split(/\&/, $::buffer)) { - my @el = split(/=/, $item); - my $name = $el[0]; - my $value; - if ($#el > 0) { - $value = url_decode($el[1]); - } else { - $value = ""; +sub ProcessFormStuff { + my ($buf) = (@_); + my $foundone = 0; + foreach my $name ("bug_status", "resolution", "assigned_to", + "rep_platform", "priority", "bug_severity", + "product", "reporter", "op_sys", + "component", "version", "chfield", "chfieldfrom", + "chfieldto", "chfieldvalue", + "email1", "emailtype1", "emailreporter1", + "emailassigned_to1", "emailcc1", "emailqa_contact1", + "emaillongdesc1", + "email2", "emailtype2", "emailreporter2", + "emailassigned_to2", "emailcc2", "emailqa_contact2", + "emaillongdesc2", + "changedin", "votes", "short_desc", "short_desc_type", + "long_desc", "long_desc_type", "bug_file_loc", + "bug_file_loc_type", "status_whiteboard", + "status_whiteboard_type", "bug_id", + "bugidtype", "keywords", "keywords_type") { + $default{$name} = ""; + $type{$name} = 0; } - if (defined $default{$name}) { - if ($default{$name} ne "") { - $default{$name} .= "|$value"; - $type{$name} = 1; + + + foreach my $item (split(/\&/, $buf)) { + my @el = split(/=/, $item); + my $name = $el[0]; + my $value; + if ($#el > 0) { + $value = url_decode($el[1]); } else { - $default{$name} = $value; + $value = ""; + } + if (defined $default{$name}) { + $foundone = 1; + if ($default{$name} ne "") { + $default{$name} .= "|$value"; + $type{$name} = 1; + } else { + $default{$name} = $value; + } } } + return $foundone; } - -if ($default{'chfieldto'} eq "") { - $default{'chfieldto'} = "Now"; -} +if (!ProcessFormStuff($::buffer)) { + # Ah-hah, there was no form stuff specified. Do it again with the + # default query. + if ($userdefaultquery) { + ProcessFormStuff($userdefaultquery); + } else { + ProcessFormStuff(Param("defaultquery")); + } +} -my $namelist = ""; + -foreach my $i (sort (keys %::COOKIE)) { - if ($i =~ /^QUERY_/) { - if ($::COOKIE{$i} ne "") { - my $name = substr($i, 6); - $namelist .= "<OPTION>$name"; - } - } +if ($default{'chfieldto'} eq "") { + $default{'chfieldto'} = "Now"; } + + print "Set-Cookie: BUGLIST= Content-type: text/html\n\n"; @@ -130,19 +200,12 @@ sub GenerateEmailInput { my $assignedto = ($default{"emailassigned_to$id"} eq "1") ? "checked" : ""; my $reporter = ($default{"emailreporter$id"} eq "1") ? "checked" : ""; my $cc = ($default{"emailcc$id"} eq "1") ? "checked" : ""; - - if ($assignedto eq "" && $reporter eq "" && $cc eq "") { - if ($id eq "1") { - $assignedto = "checked"; - } else { - $reporter = "checked"; - } - } + my $longdesc = ($default{"emaillongdesc$id"} eq "1") ? "checked" : ""; my $qapart = ""; + my $qacontact = ""; if (Param("useqacontact")) { - my $qacontact = - ($default{"emailqa_contact$id"} eq "1") ? "checked" : ""; + $qacontact = ($default{"emailqa_contact$id"} eq "1") ? "checked" : ""; $qapart = qq| <tr> <td></td> @@ -152,20 +215,31 @@ sub GenerateEmailInput { </tr> |; } + if ($assignedto eq "" && $reporter eq "" && $cc eq "" && + $qacontact eq "") { + if ($id eq "1") { + $assignedto = "checked"; + } else { + $reporter = "checked"; + } + } - return qq| + + $default{"emailtype$id"} ||= "substring"; + + return qq{ <table border=1 cellspacing=0 cellpadding=0> <tr><td> <table cellspacing=0 cellpadding=0> <tr> <td rowspan=2 valign=top><a href="helpemailquery.html">Email:</a> <input name="email$id" size="30" value="$defstr"> matching as -<SELECT NAME=emailtype$id> -<OPTION VALUE="regexp">regexp -<OPTION VALUE="notregexp">not regexp -<OPTION SELECTED VALUE="substring">substring -<OPTION VALUE="exact">exact -</SELECT> +} . BuildPulldown("emailtype$id", + [["regexp", "regexp"], + ["notregexp", "not regexp"], + ["substring", "substring"], + ["exact", "exact"]], + $default{"emailtype$id"}) . qq{ </td> <td> <input type="checkbox" name="emailassigned_to$id" value=1 $assignedto>Assigned To @@ -179,12 +253,18 @@ sub GenerateEmailInput { <tr> <td align=right>(Will match any of the selected fields)</td> <td> -<input type="checkbox" name="emailcc$id" value=1 $cc>CC +<input type="checkbox" name="emailcc$id" value=1 $cc>CC +</td> +</tr> +<tr> +<td></td> +<td> +<input type="checkbox" name="emaillongdesc$id" value=1 $longdesc>Added comment </td> </tr> </table> </table> -|; +}; } @@ -198,24 +278,18 @@ my $emailinput2 = GenerateEmailInput(2); # javascript my $jscript = << 'ENDSCRIPT'; -<script language="Javascript1.2" type="text/javascript"> +<script language="Javascript1.1" type="text/javascript"> <!-- -function array_push(str) -{ - this[this.length] = str; - return this; -} -Array.prototype.push = array_push; - var cpts = new Array(); var vers = new Array(); -var agt=navigator.userAgent.toLowerCase(); +var tms = new Array(); ENDSCRIPT my $p; my $v; my $c; +my $m; my $i = 0; my $j = 0; @@ -227,17 +301,27 @@ foreach $v (@::legal_versions) { $jscript .= "vers['$v'] = new Array();\n"; } +my $tm; +foreach $tm (@::legal_target_milestone) { + $jscript .= "tms['$tm'] = new Array();\n"; +} for $p (@::legal_product) { if ($::components{$p}) { foreach $c (@{$::components{$p}}) { - $jscript .= "cpts['$c'].push('$p');\n"; + $jscript .= "cpts['$c'][cpts['$c'].length] = '$p';\n"; } } if ($::versions{$p}) { foreach $v (@{$::versions{$p}}) { - $jscript .= "vers['$v'].push('$p');\n"; + $jscript .= "vers['$v'][vers['$v'].length] = '$p';\n"; + } + } + + if ($::target_milestone{$p}) { + foreach $m (@{$::target_milestone{$p}}) { + $jscript .= "tms['$m'][tms['$m'].length] = '$p';\n"; } } } @@ -248,14 +332,11 @@ $jscript .= q{ // Only display versions/components valid for selected product(s) function selectProduct(f) { - var agt=navigator.userAgent.toLowerCase(); // Netscape 4.04 and 4.05 also choke with an "undefined" // error. if someone can figure out how to "define" the // whatever, we'll remove this hack. in the mean time, we'll // assume that v4.00-4.03 also die, so we'll disable the neat // javascript stuff for Netscape 4.05 and earlier. - var agtver = parseFloat(navigator.appVersion); - if (agtver <= 4.05 ) return; var cnt = 0; var i; @@ -277,11 +358,13 @@ function selectProduct(f) { f.component.options.length = 0; for (c in cpts) { + if (typeof(cpts[c]) == 'function') continue; var doit = doall; for (i=0 ; !doit && i<f.product.length ; i++) { if (f.product[i].selected) { var p = f.product[i].value; for (j in cpts[c]) { + if (typeof(cpts[c][j]) == 'function') continue; var p2 = cpts[c][j]; if (p2 == p) { doit = true; @@ -309,11 +392,13 @@ function selectProduct(f) { f.version.options.length = 0; for (v in vers) { + if (typeof(vers[v]) == 'function') continue; var doit = doall; for (i=0 ; !doit && i<f.product.length ; i++) { if (f.product[i].selected) { var p = f.product[i].value; for (j in vers[v]) { + if (typeof(vers[v][j]) == 'function') continue; var p2 = vers[v][j]; if (p2 == p) { doit = true; @@ -331,8 +416,39 @@ function selectProduct(f) { } } + var tmsel = new Array(); + for (i=0 ; i<f.target_milestone.length ; i++) { + if (f.target_milestone[i].selected) { + tmsel[f.target_milestone[i].value] = 1; + } + } + f.target_milestone.options.length = 0; + for (tm in tms) { + if (typeof(tms[v]) == 'function') continue; + var doit = doall; + for (i=0 ; !doit && i<f.product.length ; i++) { + if (f.product[i].selected) { + var p = f.product[i].value; + for (j in tms[tm]) { + if (typeof(tms[tm][j]) == 'function') continue; + var p2 = tms[tm][j]; + if (p2 == p) { + doit = true; + break; + } + } + } + } + if (doit) { + var l = f.target_milestone.length; + f.target_milestone[l] = new Option(tm, tm); + if (tmsel[tm]) { + f.target_milestone[l].selected = true; + } + } + } } // --> @@ -352,58 +468,55 @@ function selectProduct(f) { # set legal_product [concat $default{"product"} [lreplace $legal_product $w $w]] # } -PutHeader("Bugzilla Query Page", "Query Page", "", - q{onLoad="selectProduct(document.forms[0]);"}); +PutHeader("Bugzilla Query Page", "Query", + "This page lets you search the database for recorded bugs.", + q{onLoad="selectProduct(document.forms[0]);"}, + 0, $jscript); push @::legal_resolution, "---"; # Oy, what a hack. -push @::legal_target_milestone, "---"; # Oy, what a hack. - -print $jscript; my @logfields = ("[Bug creation]", @::log_columns); -print " -<FORM METHOD=GET ACTION=\"buglist.cgi\"> +print qq{ +<FORM METHOD=GET ACTION="buglist.cgi"> <table> <tr> -<th align=left><A HREF=\"bug_status.html\">Status</a>:</th> -<th align=left><A HREF=\"bug_status.html\">Resolution</a>:</th> -<th align=left><A HREF=\"bug_status.html#rep_platform\">Platform</a>:</th> -<th align=left><A HREF=\"bug_status.html#op_sys\">OpSys</a>:</th> -<th align=left><A HREF=\"bug_status.html#priority\">Priority</a>:</th> -<th align=left><A HREF=\"bug_status.html#severity\">Severity</a>:</th> +<th align=left><A HREF="bug_status.html">Status</a>:</th> +<th align=left><A HREF="bug_status.html">Resolution</a>:</th> +<th align=left><A HREF="bug_status.html#rep_platform">Platform</a>:</th> +<th align=left><A HREF="bug_status.html#op_sys">OpSys</a>:</th> +<th align=left><A HREF="bug_status.html#priority">Priority</a>:</th> +<th align=left><A HREF="bug_status.html#severity">Severity</a>:</th> +}; + +print " </tr> <tr> <td align=left valign=top> -<SELECT NAME=\"bug_status\" MULTIPLE SIZE=7> -@{[make_options(\@::legal_bug_status, $default{'bug_status'}, $type{'bug_status'})]} -</SELECT> + +@{[make_selection_widget(\"bug_status\",\@::legal_bug_status,$default{'bug_status'}, $type{'bug_status'}, 1)]} + </td> <td align=left valign=top> -<SELECT NAME=\"resolution\" MULTIPLE SIZE=7> -@{[make_options(\@::legal_resolution, $default{'resolution'}, $type{'resolution'})]} -</SELECT> +@{[make_selection_widget(\"resolution\",\@::legal_resolution,$default{'resolution'}, $type{'resolution'}, 1)]} + </td> <td align=left valign=top> -<SELECT NAME=\"rep_platform\" MULTIPLE SIZE=7> -@{[make_options(\@::legal_platform, $default{'rep_platform'}, $type{'rep_platform'})]} -</SELECT> +@{[make_selection_widget(\"rep_platform\",\@::legal_platform,$default{'platform'}, $type{'platform'}, 1)]} + </td> <td align=left valign=top> -<SELECT NAME=\"op_sys\" MULTIPLE SIZE=7> -@{[make_options(\@::legal_opsys, $default{'op_sys'}, $type{'op_sys'})]} -</SELECT> +@{[make_selection_widget(\"op_sys\",\@::legal_opsys,$default{'op_sys'}, $type{'op_sys'}, 1)]} + </td> <td align=left valign=top> -<SELECT NAME=\"priority\" MULTIPLE SIZE=7> -@{[make_options(\@::legal_priority, $default{'priority'}, $type{'priority'})]} -</SELECT> +@{[make_selection_widget(\"priority\",\@::legal_priority,$default{'priority'}, $type{'priority'}, 1)]} + </td> <td align=left valign=top> -<SELECT NAME=\"bug_severity\" MULTIPLE SIZE=7> -@{[make_options(\@::legal_severity, $default{'bug_severity'}, $type{'bug_severity'})]} -</SELECT> +@{[make_selection_widget(\"bug_severity\",\@::legal_severity,$default{'bug_severity'}, $type{'bug_severity'}, 1)]} + </tr> </table> @@ -414,7 +527,32 @@ print " $emailinput1<p> </td></tr><tr><td colspan=2> $emailinput2<p> -</td></tr> +</td></tr>"; + +my $inclselected = "SELECTED"; +my $exclselected = ""; + + +if ($default{'bugidtype'} eq "exclude") { + $inclselected = ""; + $exclselected = "SELECTED"; +} +my $bug_id = value_quote($default{'bug_id'}); + +print qq{ +<TR> +<TD COLSPAN="3"> +<SELECT NAME="bugidtype"> +<OPTION VALUE="include" $inclselected>Only +<OPTION VALUE="exclude" $exclselected>Exclude +</SELECT> +bugs numbered: +<INPUT TYPE="text" NAME="bug_id" VALUE="$bug_id" SIZE=30> +</TD> +</TR> +}; + +print " <tr> <td> Changed in the <NOBR>last <INPUT NAME=changedin SIZE=2 VALUE=\"$default{'changedin'}\"> days.</NOBR> @@ -484,7 +622,7 @@ if (Param("usetargetmilestone")) { print " <td align=left valign=top> <SELECT NAME=\"target_milestone\" MULTIPLE SIZE=5> -@{[make_options(\@::legal_target_milestone, $default{'component'}, $type{'component'})]} +@{[make_options(\@::legal_target_milestone, $default{'target_milestone'}, $type{'target_milestone'})]} </SELECT> </td>"; } @@ -504,6 +642,8 @@ sub StringSearch { } foreach my $i (["substring", "case-insensitive substring"], ["casesubstring", "case-sensitive substring"], + ["allwords", "all words"], + ["anywords", "any words"], ["regexp", "regular expression"], ["notregexp", "not ( regular expression )"]) { my ($n, $d) = (@$i); @@ -526,26 +666,164 @@ print " "; StringSearch("Summary", "short_desc"); -StringSearch("Description", "long_desc"); +StringSearch("A description entry", "long_desc"); StringSearch("URL", "bug_file_loc"); if (Param("usestatuswhiteboard")) { StringSearch("Status whiteboard", "status_whiteboard"); } +if (@::legal_keywords) { + my $def = value_quote($default{'keywords'}); + print qq{ +<TR> +<TD ALIGN="right"><A HREF="describekeywords.cgi">Keywords</A>:</TD> +<TD><INPUT NAME="keywords" SIZE=30 VALUE="$def"></TD> +<TD> +}; + my $type = $default{"keywords_type"}; + if ($type eq "or") { # Backward compatability hack. + $type = "anywords"; + } + print BuildPulldown("keywords_type", + [["anywords", "Any of the listed keywords set"], + ["allwords", "All of the listed keywords set"], + ["nowords", "None of the listed keywords set"]], + $type); + print qq{</TD></TR>}; +} + print " </table> <p> +"; + + +my @fields; +push(@fields, ["noop", "---"]); +ConnectToDatabase(); +SendSQL("SELECT name, description FROM fielddefs ORDER BY sortkey"); +while (MoreSQLData()) { + my ($name, $description) = (FetchSQLData()); + push(@fields, [$name, $description]); +} + +my @types = ( + ["noop", "---"], + ["equals", "equal to"], + ["notequals", "not equal to"], + ["casesubstring", "contains (case-sensitive) substring"], + ["substring", "contains (case-insensitive) substring"], + ["notsubstring", "does not contain (case-insensitive) substring"], + ["regexp", "contains regexp"], + ["notregexp", "does not contain regexp"], + ["lessthan", "less than"], + ["greaterthan", "greater than"], + ["anywords", "any words"], + ["allwords", "all words"], + ["nowords", "none of the words"], + ["changedbefore", "changed before"], + ["changedafter", "changed after"], + ["changedto", "changed to"], + ["changedby", "changed by"], + ); + + +print qq{<A NAME="chart"> </A>\n}; + +foreach my $cmd (grep(/^cmd-/, keys(%::FORM))) { + if ($cmd =~ /^cmd-add(\d+)-(\d+)-(\d+)$/) { + $::FORM{"field$1-$2-$3"} = "xyzzy"; + } +} + +# foreach my $i (sort(keys(%::FORM))) { +# print "$i : " . value_quote($::FORM{$i}) . "<BR>\n"; +# } + + +if (!exists $::FORM{'field0-0-0'}) { + $::FORM{'field0-0-0'} = "xyzzy"; +} + +my $jsmagic = qq{ONCLICK="document.forms[0].action='query.cgi#chart' ; document.forms[0].method='POST' ; return 1;"}; + +my $chart; +for ($chart=0 ; exists $::FORM{"field$chart-0-0"} ; $chart++) { + my @rows; + my $row; + for ($row = 0 ; exists $::FORM{"field$chart-$row-0"} ; $row++) { + my @cols; + my $col; + for ($col = 0 ; exists $::FORM{"field$chart-$row-$col"} ; $col++) { + my $key = "$chart-$row-$col"; + my $deffield = $::FORM{"field$key"} || ""; + my $deftype = $::FORM{"type$key"} || ""; + my $defvalue = value_quote($::FORM{"value$key"} || ""); + my $line = ""; + $line .= "<TD>"; + $line .= BuildPulldown("field$key", \@fields, $deffield); + $line .= BuildPulldown("type$key", \@types, $deftype); + $line .= qq{<INPUT NAME="value$key" VALUE="$defvalue">}; + $line .= "</TD>\n"; + push(@cols, $line); + } + push(@rows, "<TR>" . join(qq{<TD ALIGN="center"> or </TD>\n}, @cols) . + qq{<TD><INPUT TYPE="submit" VALUE="Or" NAME="cmd-add$chart-$row-$col" $jsmagic></TD></TR>}); + } + print qq{ +<HR> +<TABLE> +}; + print join('<TR><TD>And</TD></TR>', @rows); + print qq{ +<TR><TD><INPUT TYPE="submit" VALUE="And" NAME="cmd-add$chart-$row-0" $jsmagic> +}; + my $n = $chart + 1; + if (!exists $::FORM{"field$n-0-0"}) { + print qq{ + +<INPUT TYPE="submit" VALUE="Add another boolean chart" NAME="cmd-add$n-0-0" $jsmagic> + +<NOBR><A HREF="booleanchart.html">What is this stuff?</A></NOBR> +}; + } + print qq{ +</TD> +</TR> +</TABLE> + }; +} +print qq{<HR>}; + +if (!$userid) { + print qq{<INPUT TYPE="hidden" NAME="cmdtype" VALUE="doit">}; +} else { + print " <BR> <INPUT TYPE=radio NAME=cmdtype VALUE=doit CHECKED> Run this query <BR> "; -if ($namelist ne "") { - print " + my @namedqueries; + if ($userid) { + SendSQL("SELECT name FROM namedqueries " . + "WHERE userid = $userid AND name != '$::defaultqueryname' " . + "ORDER BY name"); + while (MoreSQLData()) { + push(@namedqueries, FetchOneColumn()); + } + } + + + + + if (@namedqueries) { + my $namelist = make_options(\@namedqueries); + print qq{ <table cellspacing=0 cellpadding=0><tr> <td><INPUT TYPE=radio NAME=cmdtype VALUE=editnamed> Load the remembered query:</td> <td rowspan=3><select name=namedcmd>$namelist</select> @@ -553,32 +831,49 @@ if ($namelist ne "") { <td><INPUT TYPE=radio NAME=cmdtype VALUE=runnamed> Run the remembered query:</td> </tr><tr> <td><INPUT TYPE=radio NAME=cmdtype VALUE=forgetnamed> Forget the remembered query:</td> -</tr></table>" -} +</tr></table>}; + } -print " + print " <INPUT TYPE=radio NAME=cmdtype VALUE=asdefault> Remember this as the default query <BR> <INPUT TYPE=radio NAME=cmdtype VALUE=asnamed> Remember this query, and name it: <INPUT TYPE=text NAME=newqueryname> <BR> +" +} +print " <NOBR><B>Sort By:</B> <SELECT NAME=\"order\"> - <OPTION>Bug Number - <OPTION SELECTED>\"Importance\" - <OPTION>Assignee -</SELECT></NOBR> -<INPUT TYPE=\"submit\" VALUE=\"Submit query\"> -<INPUT TYPE=\"reset\" VALUE=\"Reset back to the default query\"> -<INPUT TYPE=hidden name=form_name VALUE=query> -<BR>Give me a <A HREF=\"help.html\">clue</A> about how to use this form. -</FORM> +"; + +my $deforder = "'Importance'"; +my @orders = ('Bug Number', $deforder, 'Assignee'); + +if ($::COOKIE{'LASTORDER'}) { + $deforder = "Reuse same sort as last time"; + unshift(@orders, $deforder); +} + +my $defquerytype = $userdefaultquery ? "my" : "the"; +print make_options(\@orders, $deforder); +print "</SELECT></NOBR> +<INPUT TYPE=\"submit\" VALUE=\"Submit query\"> +<INPUT TYPE=\"reset\" VALUE=\"Reset back to $defquerytype default query\"> "; +if ($userdefaultquery) { + print qq{<BR><A HREF="query.cgi?nukedefaultquery=1">Set my default query back to the system default</A>}; +} + +print " +</FORM> +<P>Give me a <A HREF=\"help.html\">clue</A> about how to use this form. +<P> +"; -quietly_check_login(); if (UserInGroup("tweakparams")) { print "<a href=editparams.cgi>Edit Bugzilla operating parameters</a><br>\n"; @@ -586,10 +881,15 @@ if (UserInGroup("tweakparams")) { if (UserInGroup("editcomponents")) { print "<a href=editproducts.cgi>Edit Bugzilla products and components</a><br>\n"; } -if (defined $::COOKIE{"Bugzilla_login"}) { +if (UserInGroup("editkeywords")) { + print "<a href=editkeywords.cgi>Edit Bugzilla keywords</a><br>\n"; +} +if ($userid) { print "<a href=relogin.cgi>Log in as someone besides <b>$::COOKIE{'Bugzilla_login'}</b></a><br>\n"; } -print "<a href=changepassword.cgi>Change your password or preferences.</a><br>\n"; +print "<a href=userprefs.cgi>Change your password or preferences.</a><br>\n"; print "<a href=\"enter_bug.cgi\">Create a new bug.</a><br>\n"; print "<a href=\"createaccount.cgi\">Open a new Bugzilla account</a><br>\n"; print "<a href=\"reports.cgi\">Bug reports</a><br>\n"; + +PutFooter(); diff --git a/relogin.cgi b/relogin.cgi index 6af3dd16f70567f6dda7e72bb96d0ea014060bc6..a179bafb70dab64533c2e9960271d9165c01b7b0 100755 --- a/relogin.cgi +++ b/relogin.cgi @@ -23,6 +23,8 @@ use diagnostics; use strict; +use vars %::COOKIE; + require "CGI.pl"; @@ -40,7 +42,9 @@ do an action that requires a login, you will be prompted for it. <p> "; -navigation_header(); +delete $::COOKIE{"Bugzilla_login"}; + +PutFooter(); exit; diff --git a/reports.cgi b/reports.cgi index 836228f72a34c0179833e8498ad4e382865ecab5..be21d0d6c3192ad4f36eb8aef7c6854a63afeea2 100755 --- a/reports.cgi +++ b/reports.cgi @@ -28,6 +28,7 @@ use diagnostics; use strict; +use GD; eval "use Chart::Lines"; require "CGI.pl"; @@ -63,7 +64,7 @@ else print("<html><head><title>Bug Reports</title></head><body bgcolor=\"#FFFFFF\">"); } -ConnectToDatabase(); +ConnectToDatabase(1); GetVersionTable(); my @myproducts; @@ -96,6 +97,7 @@ else print "<font color=blue>$_</font> : " . ($::FORM{$_} ? $::FORM{$_} : "undef") . "<br>\n"; } + PutFooter() if $::FORM{banner}; exit; } @@ -104,10 +106,11 @@ else print <<FIN; <p> -</body> -</html> FIN +PutFooter() if $::FORM{banner}; + + ################################## # user came in with no form data # ################################## @@ -170,6 +173,7 @@ FIN FIN #Add this above to get a control for showing the SQL query: #<input type=checkbox name=showsql value=1> Show SQL<br> + PutFooter(); } sub most_doomed @@ -203,7 +207,7 @@ and bugs.reporter = report.userid FIN if( $::FORM{'product'} ne "-All-" ) { - $query .= "and bugs.product='$::FORM{'product'}'"; + $query .= "and bugs.product=".SqlQuote($::FORM{'product'}); } $query .= <<FIN; @@ -308,6 +312,7 @@ FIN if ($bugs_count == 0) { print "No bugs found!\n"; + PutFooter() if $::FORM{banner}; exit; } @@ -434,8 +439,14 @@ FIN $prodname =~ s/\//-/gs; + my $testimg = Chart::Lines->new(2,2); + my $x = '$testimg->gif()'; + eval $x; + my $type = ($@ =~ /Can't locate object method/) ? "png" : "gif"; + my $file = join '/', $dir, $prodname; - my $image = "$file.gif"; + my $image = "$file.$type"; + my $url_image = $dir . "/" . url_quote($prodname) . ".$type"; if (! open FILE, $file) { @@ -491,11 +502,11 @@ FIN $img->set (%settings); open IMAGE, ">$image" or die "$image: $!"; - $img->gif (*IMAGE, \@data); + $img->$type (*IMAGE, \@data); close IMAGE; print <<FIN; -<img src="$image"> +<img src="$url_image"> <br clear=left> <br> FIN @@ -524,6 +535,7 @@ $msg <p> FIN + PutFooter() if $::FORM{banner}; exit; } @@ -567,7 +579,7 @@ sub most_doomed_for_milestone my $query; $query = "select distinct assigned_to from bugs where target_milestone=\"$ms\""; if( $::FORM{'product'} ne "-All-" ) { - $query .= "and bugs.product='$::FORM{'product'}'"; + $query .= "and bugs.product=".SqlQuote($::FORM{'product'}); } $query .= <<FIN; and @@ -595,7 +607,7 @@ FIN { my $query = "select count(bug_id) from bugs,profiles where target_milestone=\"$ms\" and userid=assigned_to and userid=\"$person\""; if( $::FORM{'product'} ne "-All-" ) { - $query .= "and bugs.product='$::FORM{'product'}'"; + $query .= "and bugs.product=".SqlQuote($::FORM{'product'}); } $query .= <<FIN; and @@ -691,7 +703,7 @@ sub most_recently_doomed my $query; $query = "select distinct assigned_to from bugs where bugs.bug_status='NEW' and target_milestone='' and bug_severity!='enhancement' and status_whiteboard='' and (product='Browser' or product='MailNews')"; if( $::FORM{'product'} ne "-All-" ) { - $query .= "and bugs.product='$::FORM{'product'}'"; + $query .= "and bugs.product=".SqlQuote($::FORM{'product'}); } # End build up $query string diff --git a/sanitycheck.cgi b/sanitycheck.cgi index 3db3640b7bc2c2d78c49f8b01235b9f0e87c843a..ee6d5e1880e5988200f23737c0feb09454939d30 100755 --- a/sanitycheck.cgi +++ b/sanitycheck.cgi @@ -55,6 +55,36 @@ sub AlertBadVoteCache { $offervotecacherebuild = 1; } +sub CrossCheck { + my $table = shift @_; + my $field = shift @_; + Status("Checking references to $table.$field"); + SendSQL("SELECT DISTINCT $field FROM $table"); + my %valid; + while (MoreSQLData()) { + $valid{FetchOneColumn()} = 1; + } + while (@_) { + my $ref = shift @_; + my $t2 = shift @$ref; + my $f2 = shift @$ref; + my %exceptions; + foreach my $v (@$ref) { + $exceptions{$v} = 1; + } + Status("... from $t2.$f2"); + SendSQL("SELECT DISTINCT $f2 FROM $t2"); + while (MoreSQLData()) { + my $value = FetchOneColumn(); + if (!$valid{$value} && !$exceptions{$value}) { + Alert("Bad value $value found in $t2.$f2"); + } + } + } +} + + + my @row; my @checklist; @@ -80,6 +110,55 @@ if (exists $::FORM{'rebuildvotecache'}) { print "OK, now running sanity checks.<P>\n"; +CrossCheck("keyworddefs", "id", + ["keywords", "keywordid"]); + +CrossCheck("fielddefs", "fieldid", + ["bugs_activity", "fieldid"]); + + +CrossCheck("bugs", "bug_id", + ["bugs_activity", "bug_id"], + ["attachments", "bug_id"], + ["cc", "bug_id"], + ["longdescs", "bug_id"], + ["dependencies", "blocked"], + ["dependencies", "dependson"], + ["votes", "bug_id"], + ["keywords", "bug_id"]); + +CrossCheck("profiles", "userid", + ["bugs", "reporter"], + ["bugs", "assigned_to"], + ["bugs", "qa_contact", 0], + ["attachments", "submitter_id"], + ["bugs_activity", "who"], + ["cc", "who"], + ["votes", "who"], + ["longdescs", "who"], + ["namedqueries", "userid"]); + + +Status("Checking passwords"); +SendSQL("SELECT COUNT(*) FROM profiles WHERE cryptpassword != ENCRYPT(password, left(cryptpassword, 2))"); +my $count = FetchOneColumn(); +if ($count) { + Alert("$count entries have problems in their crypted password."); + if ($::FORM{'rebuildpasswords'}) { + Status("Rebuilding passwords"); + SendSQL("UPDATE profiles + SET cryptpassword = ENCRYPT(password, + left(cryptpassword, 2)) + WHERE cryptpassword != ENCRYPT(password, + left(cryptpassword, 2))"); + Status("Passwords have been rebuilt."); + } else { + print qq{<a href="sanitycheck.cgi?rebuildpasswords=1">Click here to rebuild the crypted passwords</a><p>\n}; + } +} + + + Status("Checking groups"); SendSQL("select bit from groups where bit != pow(2, round(log(bit) / log(2)))"); while (my $bit = FetchOneColumn()) { @@ -115,6 +194,25 @@ foreach my $ref (@checklist) { } } +# Adding check for Target Milestones / products - matthew@zeroknowledge.com +Status("Checking milestone/products"); + +@checklist = (); +SendSQL("select distinct product, target_milestone from bugs"); +while (@row = FetchSQLData()) { + my @copy = @row; + push(@checklist, \@copy); +} + +foreach my $ref (@checklist) { + my ($product, $milestone) = (@$ref); + SendSQL("SELECT count(*) FROM milestones WHERE product = '$product' AND value = '$milestone'"); + if(FetchOneColumn() != 1) { + Alert("Bug(s) found with invalud product/milestone: $product/$milestone"); + } +} + + Status("Checking components/products"); @@ -134,46 +232,34 @@ foreach my $ref (@checklist) { } -Status("Checking profile ids..."); +Status("Checking profile logins"); -SendSQL("select userid,login_name from profiles"); +my $emailregexp = Param("emailregexp"); -my %profid; - -while (@row = FetchSQLData()) { - my ($id, $email) = (@row); - if ($email =~ /^[^@, ]*@[^@, ]*\.[^@, ]*$/) { - $profid{$id} = 1; - } else { - Alert "Bad profile id $id <$email>." - } -} +SendSQL("SELECT userid, login_name FROM profiles " . + "WHERE login_name NOT REGEXP " . SqlQuote($emailregexp)); -undef $profid{0}; +while (my ($id,$email) = (FetchSQLData())) { + Alert "Bad profile email address, id=$id, <$email>." +} -Status("Checking reporter/assigned_to/qa_contact ids"); -SendSQL("select bug_id,reporter,assigned_to,qa_contact,votes from bugs"); +SendSQL("SELECT bug_id,votes,keywords FROM bugs " . + "WHERE votes != 0 OR keywords != ''"); my %votes; my %bugid; +my %keyword; while (@row = FetchSQLData()) { - my($id, $reporter, $assigned_to, $qa_contact, $v) = (@row); - $bugid{$id} = 1; - if (!defined $profid{$reporter}) { - Alert("Bad reporter $reporter in " . BugLink($id)); - } - if (!defined $profid{$assigned_to}) { - Alert("Bad assigned_to $assigned_to in" . BugLink($id)); - } - if ($qa_contact != 0 && !defined $profid{$qa_contact}) { - Alert("Bad qa_contact $qa_contact in" . BugLink($id)); - } + my($id, $v, $k) = (@row); if ($v != 0) { $votes{$id} = $v; } + if ($k) { + $keyword{$id} = $k; + } } Status("Checking cached vote counts"); @@ -199,45 +285,102 @@ if ($offervotecacherebuild) { } -Status("Checking CC table"); +Status("Checking keywords table"); -SendSQL("select bug_id,who from cc"); +my %keywordids; +SendSQL("SELECT id, name FROM keyworddefs"); while (@row = FetchSQLData()) { - my ($id, $cc) = (@row); - if (!defined $profid{$cc}) { - Alert("Bad cc $cc in " . BugLink($id)); + my ($id, $name) = (@row); + if ($keywordids{$id}) { + Alert("Duplicate entry in keyworddefs for id $id"); + } + $keywordids{$id} = 1; + if ($name =~ /[\s,]/) { + Alert("Bogus name in keyworddefs for id $id"); } } -Status("Checking activity table"); - -SendSQL("select bug_id,who from bugs_activity"); - +SendSQL("SELECT bug_id, keywordid FROM keywords ORDER BY bug_id, keywordid"); +my $lastid; +my $lastk; while (@row = FetchSQLData()) { - my ($id, $who) = (@row); - if (!defined $bugid{$id}) { - Alert("Bad bugid " . BugLink($id)); + my ($id, $k) = (@row); + if (!$keywordids{$k}) { + Alert("Bogus keywordids $k found in keywords table"); } - if (!defined $profid{$who}) { - Alert("Bad who $who in " . BugLink($id)); + if (defined $lastid && $id eq $lastid && $k eq $lastk) { + Alert("Duplicate keyword ids found in bug " . BugLink($id)); } + $lastid = $id; + $lastk = $k; } +Status("Checking cached keywords"); -Status("Checking dependency table"); +my %realk; -SendSQL("select blocked, dependson from dependencies"); -while (@row = FetchSQLData()) { - my ($blocked, $dependson) = (@row); - if (!defined $bugid{$blocked}) { - Alert("Bad blocked " . BugLink($blocked)); +if (exists $::FORM{'rebuildkeywordcache'}) { + SendSQL("LOCK TABLES bugs write, keywords read, keyworddefs read"); +} + +SendSQL("SELECT keywords.bug_id, keyworddefs.name " . + "FROM keywords, keyworddefs " . + "WHERE keyworddefs.id = keywords.keywordid " . + "ORDER BY keywords.bug_id, keyworddefs.name"); + +my $lastb; +my @list; +while (1) { + my ($b, $k) = (FetchSQLData()); + if (!defined $b || $b ne $lastb) { + if (@list) { + $realk{$lastb} = join(', ', @list); + } + if (!$b) { + last; + } + $lastb = $b; + @list = (); } - if (!defined $bugid{$dependson}) { - Alert("Bad dependson " . BugLink($dependson)); + push(@list, $k); +} + +my @fixlist; +foreach my $b (keys(%keyword)) { + if (!exists $realk{$b} || $realk{$b} ne $keyword{$b}) { + push(@fixlist, $b); } } +foreach my $b (keys(%realk)) { + if (!exists $keyword{$b}) { + push(@fixlist, $b); + } +} +if (@fixlist) { + @fixlist = sort {$a <=> $b} @fixlist; + Alert("Bug(s) found with incorrect keyword cache: " . + join(', ', @fixlist)); + if (exists $::FORM{'rebuildkeywordcache'}) { + Status("OK, now fixing keyword cache."); + foreach my $b (@fixlist) { + my $k = ''; + if (exists($realk{$b})) { + $k = $realk{$b}; + } + SendSQL("UPDATE bugs SET delta_ts = delta_ts, keywords = " . + SqlQuote($k) . + " WHERE bug_id = $b"); + } + SendSQL("UNLOCK TABLES"); + Status("Keyword cache fixed."); + } else { + print qq{<a href="sanitycheck.cgi?rebuildkeywordcache=1">Click here to rebuild the keyword cache</a><p>\n}; + } +} + Status("Sanity check completed."); +PutFooter(); diff --git a/show_activity.cgi b/show_activity.cgi index 972fbd0c43d98989ebbc2384dc496284c93b845c..d6e388afc1ad3aa39f3d6fc2eae61f5592db4450 100755 --- a/show_activity.cgi +++ b/show_activity.cgi @@ -35,3 +35,5 @@ ConnectToDatabase(); DumpBugActivity($::FORM{'id'}); print "<hr><a href=show_bug.cgi?id=$::FORM{'id'}>Back to bug $::FORM{'id'}</a>\n"; + +PutFooter(); diff --git a/show_bug.cgi b/show_bug.cgi index 909b08d4e75043d94ed6fa6af59fd559ae68c443..149182ae817b7f45e53724adb6dc77aba4921011 100755 --- a/show_bug.cgi +++ b/show_bug.cgi @@ -41,6 +41,7 @@ if (!defined $::FORM{'id'} || $::FORM{'id'} !~ /^\s*\d+\s*$/) { print "<INPUT NAME=id>\n"; print "<INPUT TYPE=\"submit\" VALUE=\"Show Me This Bug\">\n"; print "</FORM>\n"; + PutFooter(); exit; } @@ -53,6 +54,4 @@ print "<HR>\n"; $! = 0; do "bug_form.pl" || die "Error doing bug_form.pl: $!"; -print "</BODY>"; -print "</HTML>\n"; diff --git a/showattachment.cgi b/showattachment.cgi index d5dcfb8df4830889860ac33e873d6f44d3ad148c..22cfa9087a2b3a054052db746a61599dcada8328 100755 --- a/showattachment.cgi +++ b/showattachment.cgi @@ -29,7 +29,7 @@ ConnectToDatabase(); my @row; if (defined $::FORM{'attach_id'}) { - SendSQL("select mimetype, thedata from attachments where attach_id = $::FORM{'attach_id'}"); + SendSQL("select mimetype, thedata from attachments where attach_id =".SqlQuote($::FORM{'attach_id'})); @row = FetchSQLData(); } if (!@row) { diff --git a/showdependencygraph.cgi b/showdependencygraph.cgi index 792a5edabe1bffc77a808ff167aa703b3752acc0..f15534be3d3fe2643d60aa310ba511397f940aa5 100755 --- a/showdependencygraph.cgi +++ b/showdependencygraph.cgi @@ -26,6 +26,7 @@ use strict; require "CGI.pl"; my $id = $::FORM{'id'}; +die "Invalid id: $id" unless $id =~ /^\s*\d+\s*$/; my $urlbase = Param("urlbase"); my %seen; @@ -180,4 +181,4 @@ dependencies</td> </form> "; -navigation_header(); +PutFooter(); diff --git a/showdependencytree.cgi b/showdependencytree.cgi index cf9dcef9987f1593cebfcf2d995414d5112d2e54..74e2778bc4963a54b55a6b41af5bff7ea39c1d64 100755 --- a/showdependencytree.cgi +++ b/showdependencytree.cgi @@ -100,4 +100,4 @@ print "<h1>Bugs that depend on bug $linkedid</h1>"; undef %seen; DumpKids($id, "dependson", "blocked"); -navigation_header(); +PutFooter(); diff --git a/showowners.cgi b/showowners.cgi deleted file mode 100755 index 108c65cb0449b6cab0d8b345bc2797b605fcd10e..0000000000000000000000000000000000000000 --- a/showowners.cgi +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bonsaitools/bin/perl -w -# -*- Mode: perl; indent-tabs-mode: nil -*- -# -# The contents of this file are subject to the Mozilla Public -# License Version 1.1 (the "License"); you may not use this file -# except in compliance with the License. You may obtain a copy of -# the License at http://www.mozilla.org/MPL/ -# -# Software distributed under the License is distributed on an "AS -# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or -# implied. See the License for the specific language governing -# rights and limitations under the License. -# -# The Original Code is the Bugzilla Bug Tracking System. -# -# The Initial Developer of the Original Code is Netscape Communications -# Corporation. Portions created by Netscape are -# Copyright (C) 1998 Netscape Communications Corporation. All -# Rights Reserved. -# -# Contributor(s): Bryce Nesbitt <bryce@nextbus.com> -# -# This program lists all BugZilla users, and lists what modules they -# either own or are default QA for. It is very slow on large databases. - -use diagnostics; -use strict; - -require "CGI.pl"; -require "globals.pl"; - -# Fetch, one row at a time, the product and module. -# Build the contents of the table cell listing each unique -# product just once, with all the modules. -sub FetchAndFormat { - my $result = ""; - my $temp = ""; - my @row = ""; - - while (@row = FetchSQLData()) { - if( $temp ne $row[0] ) { - $result .= " " . $row[0] . ": "; - } else { - $result .= ", "; - } - $temp = $row[0]; - $result .= "<I>" . $row[1] . "</I>"; - } - return( $result ); -} - - -# Start the resulting web page -print "Content-type: text/html\n\n"; -print "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\"> -<html><head><title>BugZilla module owners list</title></head>\n"; -PutHeader("Owner list"); - -ConnectToDatabase(); -GetVersionTable(); - -# Collect all BugZilla user names -SendSQL("select login_name,userid from profiles order by login_name"); -my @list; -my @row; -while (@row = FetchSQLData()) { - push @list, $row[0]; -} - -print "<P>The following is a list of BugZilla users who are the default owner -for at least one module. BugZilla will only assign or Cc: a bug to the exact -name stored in the database. Click on a name to see bugs assigned to that person:</P>\n"; -print "<table border=1>\n"; -print "<tr><td><B>Login name</B></td>\n"; -print "<td><B>Default owner for</B></td><td><B>Default QA for</B></td>\n"; - -# If a user is a initialowner or initialqacontact, list their modules -my $person; -my $nospamperson; -my $firstcell; -my $secondcell; -my @nocell; -foreach $person (@list) { - - my $qperson = SqlQuote($person); - - SendSQL("select program,value from components\ - where initialowner = $qperson order by program,value"); - $firstcell = FetchAndFormat(); - - SendSQL("select program,value from components\ - where initialqacontact = $qperson order by program,value"); - $secondcell = FetchAndFormat(); - - $_ = $person; # Anti-spam - s/@/ @/; # Mangle - $nospamperson = $_; # Email - - if( $firstcell || $secondcell ) { - print "<tr>"; - - print "<td>\n"; - print "<a href=\"buglist.cgi?bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&email1=${person}&emailtype1=substring&emailassigned_to1=1&cmdtype=doit&newqueryname=&order=%22Importance%22&form_name=query\">${nospamperson}</a>\n"; - print "</td>\n"; - - print "<td>"; - print $firstcell; - print "</td>\n"; - - print "<td>"; - print $secondcell; - print "</td>\n"; - - print "</tr>\n"; - } else { - push @nocell, $person; - } -} - -print "<tr>"; -print "<td colspan=3>"; -print "Other valid logins: "; -foreach $person (@nocell) { - $_ = $person; # Anti-spam - s/@/ @/; # Mangle - $nospamperson = $_; # Email - - print "<a href=\"buglist.cgi?bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&email1=${person}&emailtype1=substring&emailassigned_to1=1&cmdtype=doit&newqueryname=&order=%22Importance%22&form_name=query\">${nospamperson}</a>\n"; - print ", "; -} -print "</td>"; -print "</tr>\n"; - -print "</table>\n"; - -# Enhancement ideas -# o Use just one table cell for each person. The table gets unbalanced for installs -# where just a few QA people handle lots of modules -# o Optimize for large systems. Terry notes: -# The problem is that you go query the components table 10,000 times, -# twice for each of the 5,000 logins that we have. Yow! -# -# It would be better to generate your initial list of logins by selecting -# for distinct initialqacontact and initialowner values from the -# components database. Then when you generate the list of "other -# logins", you can query for the whole list of logins and subtract out -# things that were in the components database. diff --git a/showvotes.cgi b/showvotes.cgi index d6384b59e6962eebfece9eabbaaadfd5ca52b4bb..894baafbb16fb567dbb5bad0aa2242afc8c1b1db 100755 --- a/showvotes.cgi +++ b/showvotes.cgi @@ -84,8 +84,7 @@ if (defined $::FORM{'bug_id'}) { if (!defined $status) { next; } - my $opened = ($status eq "NEW" || $status eq "ASSIGNED" || - $status eq "REOPENED"); + my $opened = IsOpenedState($status); my $strike = $opened ? "" : "<strike>"; my $endstrike = $opened ? "" : "</strike>"; $summary = html_quote($summary); @@ -120,5 +119,5 @@ if (defined $::FORM{'bug_id'}) { print qq{<a href="votehelp.html">Help! I don't understand this voting stuff</a>}; -navigation_header(); +PutFooter(); diff --git a/syncshadowdb b/syncshadowdb new file mode 100755 index 0000000000000000000000000000000000000000..f5c4312666960138a084a0bc738cc9f4a96c2909 --- /dev/null +++ b/syncshadowdb @@ -0,0 +1,213 @@ +#!/usr/bonsaitools/bin/perl -w +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Bug Tracking System. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are +# Copyright (C) 1998 Netscape Communications Corporation. All +# Rights Reserved. +# +# Contributor(s): Terry Weissman <terry@mozilla.org> +# David Gardiner <david.gardiner@unisa.edu.au> + +use diagnostics; +use strict; + +require "globals.pl"; +require "defparams.pl"; + +# Shut up misguided -w warnings about "used only once". "use vars" just +# doesn't work for me. + +sub sillyness { + my $zz; + $zz = $::db; + $zz = $::dbwritesallowed; +} + +my $verbose = 0; +my $syncall = 0; + +sub Usage { + print "Usage: syncshadowdb [-v] [-syncall]\n"; + exit; +} + +foreach my $opt (@ARGV) { + if ($opt eq '-v') { + $verbose = 1; + } elsif ($opt eq '-syncall') { + $syncall = 1; + $verbose = 1; + } else { + Usage(); + } +} +$| = 1; + +my $logtostderr = 0; + +sub Verbose ($) { + my ($str) = (@_); + if ($verbose) { + if ($logtostderr) { + print STDERR $str, "\n"; + } else { + print $str, "\n"; + } + } +} + +my $db_name = "bugs"; +require "localconfig"; + +if (!Param("shadowdb")) { + Verbose("We don't have shadow databases turned on; no syncing performed."); + exit; +} + +my $wasusing = Param("queryagainstshadowdb"); + +$::param{'queryagainstshadowdb'} = 1; # Force us to be able to use the + # shadowdb, even if other processes + # are not supposed to. + + +ConnectToDatabase(1); + +Verbose("Aquiring lock."); +SendSQL("SELECT GET_LOCK('synclock', 1)"); +if (!FetchOneColumn()) { + Verbose("Couldn't get the lock to do the shadow database syncing."); + exit; +} + +my $shadowtable = "$db_name.shadowlog"; + +if (!$syncall) { + Verbose("Looking for requests to sync the whole database."); + SendSQL("SELECT id FROM $shadowtable " . + "WHERE reflected = 0 AND command = 'SYNCUP'"); + if (FetchOneColumn()) { + $syncall = 1; + } +} + +if ($syncall) { + Verbose("Syncing up the shadow database by copying entire database in."); + if ($wasusing) { + $::param{'queryagainstshadowdb'} = 0; + WriteParams(); + Verbose("Disabled reading from the shadowdb. Sleeping 10 seconds to let other procs catch up."); + sleep(10); + $::param{'queryagainstshadowdb'} = 1; + } + my @tables; + SendSQL("SHOW TABLES"); + my $query = ""; + while (MoreSQLData()) { + my $table = FetchOneColumn(); + push(@tables, $table); + if ($query) { + $query .= ", $table WRITE"; + } else { + $query = "LOCK TABLES $table WRITE"; + } + } + if (@tables) { + Verbose("Locking entire shadow database"); + SendSQL($query); + foreach my $table (@tables) { + Verbose("Dropping old shadow table $table"); + SendSQL("DROP TABLE $table"); + } + SendSQL("UNLOCK TABLES"); + } + # Carefully lock the whole real database for reading, except for the + # shadowlog table, which we lock for writing. Then dump everything + # into the shadowdb database. Then mark everything in the shadowlog + # as reflected. Only then unlock everything. This sequence causes + # us to be sure not to miss anything or get something twice. + SendSQL("USE $db_name"); + SendSQL("SHOW TABLES"); + @tables = (); + $query = "LOCK TABLES shadowlog WRITE"; + while (MoreSQLData()) { + my $table = FetchOneColumn(); + if ($table ne "shadowlog") { + $query .= ", $table READ"; + push(@tables, $table); + } + } + Verbose("Locking entire database"); + SendSQL($query); + my $tablelist = join(' ', @tables); + my $tempfile = "data/tmpsyncshadow.$$"; + Verbose("Dumping database to a temp file ($tempfile)."); + system("mysqldump -l -e $db_name $tablelist > $tempfile"); + Verbose("Restoring from tempfile into shadowdb"); + my $extra = ""; + if ($verbose) { + $extra = "-v"; + } + open(MYSQL, "cat $tempfile | mysql $extra " . + Param("shadowdb") . "|") || die "Couldn't do db copy"; + my $count = 0; + while (<MYSQL>) { + print "."; + $count++; + if ($count % 70 == 0) { + print "\n"; + } + } + close(MYSQL); + unlink($tempfile); + Verbose(""); + + + $::dbwritesallowed = 1; +# SendSQL("UPDATE shadowlog SET reflected = 1 WHERE reflected = 0", 1); + SendSQL("DELETE FROM shadowlog", 1); + SendSQL("UNLOCK TABLES"); + if ($wasusing) { + Verbose("Reenabling other processes to read from the shadow db"); + $::param{'queryagainstshadowdb'} = 1; + WriteParams(); + } + Verbose("OK, done."); +} + +Verbose("Looking for commands to execute."); +$::dbwritesallowed = 1; + +# Make us low priority, to not block anyone who is trying to actually use +# the shadowdb. Note that this is carefully coded to ignore errors; we want +# to keep going even on older mysqld's that don't have the +# SQL_LOW_PRIORITY_UPDATES option. +$::db->query("SET OPTION SQL_LOW_PRIORITY_UPDATES = 1"); + +while (1) { + SendSQL("SELECT id, command FROM $shadowtable WHERE reflected = 0 " . + "ORDER BY id LIMIT 1"); + my ($id, $command) = (FetchSQLData()); + if (!$id) { + last; + } + Verbose("Executing command in shadow db: $command"); + SendSQL($command, 1); + SendSQL("UPDATE $shadowtable SET reflected = 1 WHERE id = $id", 1); +} + +Verbose("Releasing lock."); +SendSQL("SELECT RELEASE_LOCK('synclock')"); diff --git a/template/CVS/Entries b/template/CVS/Entries new file mode 100644 index 0000000000000000000000000000000000000000..178481050188cf00d7d9cd5a11e43ab8fab9294f --- /dev/null +++ b/template/CVS/Entries @@ -0,0 +1 @@ +D diff --git a/template/CVS/Repository b/template/CVS/Repository new file mode 100644 index 0000000000000000000000000000000000000000..02f5728384e6884c466f95b47edbdfae109d2eb5 --- /dev/null +++ b/template/CVS/Repository @@ -0,0 +1 @@ +mozilla/webtools/bugzilla/template diff --git a/template/CVS/Root b/template/CVS/Root new file mode 100644 index 0000000000000000000000000000000000000000..cdb6f4a0739a0dc53e628026726036377dec3637 --- /dev/null +++ b/template/CVS/Root @@ -0,0 +1 @@ +:pserver:anonymous@cvs-mirror.mozilla.org:/cvsroot diff --git a/userprefs.cgi b/userprefs.cgi new file mode 100755 index 0000000000000000000000000000000000000000..dd739beb83f0bc439a3fa20585ceb3aba5fd9baa --- /dev/null +++ b/userprefs.cgi @@ -0,0 +1,422 @@ +#!/usr/bonsaitools/bin/perl -w +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Bug Tracking System. +# +# 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 { + my $zz; + $zz = $::defaultqueryname; + $zz = $::usergroupset; +} + +my $userid; + + +sub EmitEntry { + my ($description, $entry) = (@_); + print qq{<TR><TH ALIGN="right">$description:</TH><TD>$entry</TD></TR>\n}; +} + +sub Error { + my ($msg) = (@_); + print qq{ +$msg +<P> +Please hit <B>back</B> and try again. +}; + PutFooter(); + exit(); +} + + +sub ShowAccount { + SendSQL("SELECT realname FROM profiles WHERE userid = $userid"); + my ($realname) = (FetchSQLData()); + + $realname = value_quote($realname); + + EmitEntry("Old password", + qq{<input type=password name="oldpwd">}); + EmitEntry("New password", + qq{<input type=password name="pwd1">}); + EmitEntry("Re-enter new password", + qq{<input type=password name="pwd2">}); + EmitEntry("Your real name (optional)", + qq{<INPUT SIZE=35 NAME="realname" VALUE="$realname">}); +} + +sub SaveAccount { + if ($::FORM{'oldpwd'} ne "" + || $::FORM{'pwd1'} ne "" || $::FORM{'pwd2'} ne "") { + my $old = SqlQuote($::FORM{'oldpwd'}); + my $pwd1 = SqlQuote($::FORM{'pwd1'}); + my $pwd2 = SqlQuote($::FORM{'pwd2'}); + SendSQL("SELECT cryptpassword = ENCRYPT($old, LEFT(cryptpassword, 2)) " . + "FROM profiles WHERE userid = $userid"); + if (!FetchOneColumn()) { + Error("You did not enter your old password correctly."); + } + if ($pwd1 ne $pwd2) { + Error("The two passwords you entered did not match."); + } + if ($::FORM{'pwd1'} eq '') { + Error("You must enter a new password."); + } + SendSQL("UPDATE profiles SET password = $pwd1, " . + "cryptpassword = ENCRYPT($pwd1) " . + "WHERE userid = $userid"); + } + SendSQL("UPDATE profiles SET " . + "realname = " . SqlQuote($::FORM{'realname'}) . + " WHERE userid = $userid"); +} + + + +sub ShowDiffs { + SendSQL("SELECT emailnotification, newemailtech FROM profiles " . + "WHERE userid = $userid"); + my ($emailnotification, $newemailtech) = (FetchSQLData()); + my $qacontactpart = ""; + if (Param('useqacontact')) { + $qacontactpart = ", the current QA Contact"; + } + print qq{ +<TR><TD COLSPAN="2"> +Bugzilla will send out email notification of changed bugs to +the current owner, the submitter of the bug$qacontactpart, and anyone on the +CC list. However, you can suppress some of those email notifications. +On which of these bugs would you like email notification of changes? +</TD></TR> +}; + my $entry = + BuildPulldown("emailnotification", + [["ExcludeSelfChanges", + "All qualifying bugs except those which I change"], + ["CConly", + "Only those bugs which I am listed on the CC line"], + ["All", + "All qualifying bugs"]], + $emailnotification); + EmitEntry("Notify me of changes to", $entry); + + if (Param("newemailtech")) { + my $checkedpart = $newemailtech ? "CHECKED" : ""; + print qq{ +<TR><TD COLSPAN="2"><HR></TD></TR> +<TR><TD COLSPAN="2"><FONT COLOR="red">New!</FONT> +Bugzilla has a new email +notification scheme. It is <b>experimental and bleeding edge</b> and will +hopefully evolve into a brave new happy world where all the spam and ugliness +of the old notifications will go away. If you wish to sign up for this (and +risk any bugs), check here. +</TD></TR> +}; + 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 { + my $newemailtech = 0; + if (exists $::FORM{'newemailtech'}) { + $newemailtech = 1; + } + 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"); + + } + } +} + + + +sub ShowFooter { + SendSQL("SELECT mybugslink FROM profiles " . + "WHERE userid = $userid"); + my ($mybugslink) = (FetchSQLData()); + my $entry = + BuildPulldown("mybugslink", + [["1", "should appear"], + ["0", "should not be displayed"]], + $mybugslink); + EmitEntry("The 'My bugs' link at the footer of each page", $entry); + SendSQL("SELECT name, linkinfooter FROM namedqueries " . + "WHERE userid = $userid"); + my $count = 0; + while (MoreSQLData()) { + my ($name, $linkinfooter) = (FetchSQLData()); + if ($name eq $::defaultqueryname) { + next; + } + my $entry = + BuildPulldown("query-$count", + [["0", "should only appear in the query page"], + ["1", "should appear on the footer of every page"]], + $linkinfooter); + EmitEntry("Your query named '$name'", $entry); + my $q = value_quote($name); + print qq{<INPUT TYPE=HIDDEN NAME="name-$count" VALUE="$q">\n}; + $count++; + } + print qq{<INPUT TYPE=HIDDEN NAME="numqueries" VALUE="$count">\n}; + if (!$count) { + print qq{ +<TR><TD COLSPAN="2"> +If you go create remembered queries in the <A HREF="query.cgi">query page</A>, +you can then come to this page and choose to have some of them appear in the +footer of each Bugzilla page. +</TD></TR>}; + } +} + + +sub SaveFooter { + my %old; + SendSQL("SELECT name, linkinfooter FROM namedqueries " . + "WHERE userid = $userid"); + while (MoreSQLData()) { + my ($name, $linkinfooter) = (FetchSQLData()); + $old{$name} = $linkinfooter; + } + + for (my $c=0 ; $c<$::FORM{'numqueries'} ; $c++) { + my $name = $::FORM{"name-$c"}; + if (exists $old{$name}) { + my $new = $::FORM{"query-$c"}; + if ($new ne $old{$name}) { + SendSQL("UPDATE namedqueries SET linkinfooter = $new " . + "WHERE userid = $userid " . + "AND name = " . SqlQuote($name)); + } + } else { + Error("Hmm, the $name query seems to have gone away."); + } + } + SendSQL("UPDATE profiles SET mybugslink = '" . $::FORM{'mybugslink'} . + "' WHERE userid = $userid"); +} + + + +sub ShowPermissions { + print "You have the following permission bits set on your account:\n"; + print "<P><UL>\n"; + my $found = 0; + SendSQL("SELECT description FROM groups " . + "WHERE bit & $::usergroupset != 0 " . + "ORDER BY bit"); + while (MoreSQLData()) { + my ($description) = (FetchSQLData()); + print "<LI>$description\n"; + $found = 1; + } + if ($found == 0) { + print "<LI>(No extra permission bits have been set).\n"; + } + print "</UL>\n"; + SendSQL("SELECT blessgroupset FROM profiles WHERE userid = $userid"); + my $blessgroupset = FetchOneColumn(); + if ($blessgroupset) { + print "And you can turn on or off the following bits for\n"; + print qq{<A HREF="editusers.cgi">other users</A>:\n}; + print "<P><UL>\n"; + SendSQL("SELECT description FROM groups " . + "WHERE bit & $blessgroupset != 0 " . + "ORDER BY bit"); + while (MoreSQLData()) { + my ($description) = (FetchSQLData()); + print "<LI>$description\n"; + } + print "</UL>\n"; + } +} + + + + +###################################################################### +################# Live code (not sub defs) starts here ############### + + +confirm_login(); + +print "Content-type: text/html\n\n"; + +GetVersionTable(); + +PutHeader("Preferences", "Preferences", $::COOKIE{'Bugzilla_login'}); + +# foreach my $k (sort(keys(%::FORM))) { +# print "<pre>" . value_quote($k) . ": " . value_quote($::FORM{$k}) . "\n</pre>"; +# } + +my $bank = $::FORM{'bank'} || "account"; + +my @banklist = ( + ["account", "Account settings", + \&ShowAccount, \&SaveAccount], + ["diffs", "Email settings", + \&ShowDiffs, \&SaveDiffs], + ["footer", "Page footer", + \&ShowFooter, \&SaveFooter], + ["permissions", "Permissions", + \&ShowPermissions, undef] + ); + + +my $numbanks = @banklist; +my $numcols = $numbanks + 2; + +my $headcol = '"lightblue"'; + +print qq{ +<CENTER> +<TABLE CELLSPACING="0" CELLPADDING="10" BORDER=0 WIDTH="100%"> +<TR> +<TH COLSPAN="$numcols" BGCOLOR="lightblue">User preferences</TH> +</TR> +<TR><TD BGCOLOR=$headcol> </TD> +}; + + +my $bankdescription; +my $showfunc; +my $savefunc; + +foreach my $i (@banklist) { + my ($name, $description) = (@$i); + my $color = ""; + if ($name eq $bank) { + print qq{<TD ALIGN="center">$description</TD>}; + my $zz; + ($zz, $bankdescription, $showfunc, $savefunc) = (@$i); + } else { + print qq{<TD ALIGN="center" BGCOLOR="lightblue"><A HREF="userprefs.cgi?bank=$name">$description</A></TD>}; + } +} +print qq{ +<TD BGCOLOR=$headcol> </TD></TR> +</TABLE> +</CENTER> +<P> +}; + + + + +if (defined $bankdescription) { + $userid = DBNameToIdAndCheck($::COOKIE{'Bugzilla_login'}); + + if ($::FORM{'dosave'}) { + &$savefunc; + print "Your changes have been saved."; + } + print qq{ +<H3>$bankdescription</H3> +<FORM METHOD="POST"> +<TABLE> +}; + &$showfunc; + print qq{ +</TABLE> +<INPUT TYPE="hidden" NAME="dosave" VALUE="1"> +<INPUT TYPE="hidden" NAME="bank" VALUE="$bank"> +}; + if ($savefunc) { + print qq{<INPUT TYPE="submit" VALUE="Submit">\n}; + } + print qq{</FORM>\n}; +} else { + print "<P>Please choose from the above links which settings you wish to change.</P>"; +} + + +print "<P>"; + + +PutFooter();