mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-08 12:37:37 +00:00
bb41d6ee37
r=gerv, afranke
1668 lines
53 KiB
Prolog
1668 lines
53 KiB
Prolog
# -*- 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>
|
|
# Dan Mosedale <dmose@mozilla.org>
|
|
# Jake <jake@acutex.net>
|
|
# Bradley Baetz <bbaetz@cs.mcgill.ca>
|
|
# Christopher Aillon <christopher@aillon.com>
|
|
|
|
# Contains some global variables and routines used throughout bugzilla.
|
|
|
|
use diagnostics;
|
|
use strict;
|
|
|
|
# Shut up misguided -w warnings about "used only once". For some reason,
|
|
# "use vars" chokes on me when I try it here.
|
|
|
|
sub globals_pl_sillyness {
|
|
my $zz;
|
|
$zz = @main::SqlStateStack;
|
|
$zz = @main::chooseone;
|
|
$zz = @main::default_column_list;
|
|
$zz = $main::defaultqueryname;
|
|
$zz = @main::dontchange;
|
|
$zz = @main::enterable_products;
|
|
$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;
|
|
$zz = @main::legal_product;
|
|
$zz = @main::legal_severity;
|
|
$zz = @main::legal_target_milestone;
|
|
$zz = @main::legal_versions;
|
|
$zz = @main::milestoneurl;
|
|
$zz = %main::proddesc;
|
|
$zz = @main::prodmaxvotes;
|
|
$zz = $main::superusergroupset;
|
|
$zz = $main::userid;
|
|
$zz = $main::template;
|
|
$zz = $main::vars;
|
|
}
|
|
|
|
#
|
|
# Here are the --LOCAL-- variables defined in 'localconfig' that we'll use
|
|
# here
|
|
#
|
|
|
|
$::db_host = "localhost";
|
|
$::db_port = 3306;
|
|
$::db_name = "bugs";
|
|
$::db_user = "bugs";
|
|
$::db_pass = "";
|
|
|
|
do 'localconfig';
|
|
|
|
use DBI;
|
|
|
|
use Date::Format; # For time2str().
|
|
use Date::Parse; # For str2time().
|
|
#use Carp; # for confess
|
|
use RelationSet;
|
|
|
|
# Some environment variables are not taint safe
|
|
delete @::ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
|
|
|
|
# Contains the version string for the current running Bugzilla.
|
|
$::param{'version'} = '2.15';
|
|
|
|
$::dontchange = "--do_not_change--";
|
|
$::chooseone = "--Choose_one:--";
|
|
$::defaultqueryname = "(Default query)";
|
|
$::unconfirmedstate = "UNCONFIRMED";
|
|
$::dbwritesallowed = 1;
|
|
|
|
# Adding a global variable for the value of the superuser groupset.
|
|
# Joe Robins, 7/5/00
|
|
$::superusergroupset = "9223372036854775807";
|
|
|
|
#sub die_with_dignity {
|
|
# my ($err_msg) = @_;
|
|
# print $err_msg;
|
|
# confess($err_msg);
|
|
#}
|
|
#$::SIG{__DIE__} = \&die_with_dignity;
|
|
|
|
sub ConnectToDatabase {
|
|
my ($useshadow) = (@_);
|
|
if (!defined $::db) {
|
|
my $name = $::db_name;
|
|
if ($useshadow && Param("shadowdb") && Param("queryagainstshadowdb")) {
|
|
$name = Param("shadowdb");
|
|
$::dbwritesallowed = 0;
|
|
}
|
|
$::db = DBI->connect("DBI:mysql:host=$::db_host;database=$name;port=$::db_port", $::db_user, $::db_pass)
|
|
|| die "Bugzilla is currently broken. Please try again later. " .
|
|
"If the problem persists, please contact " . Param("maintainer") .
|
|
". The error you should quote is: " . $DBI::errstr;
|
|
}
|
|
}
|
|
|
|
sub ReconnectToShadowDatabase {
|
|
if (Param("shadowdb") && Param("queryagainstshadowdb")) {
|
|
SendSQL("USE " . Param("shadowdb"));
|
|
$::dbwritesallowed = 0;
|
|
}
|
|
}
|
|
|
|
my $shadowchanges = 0;
|
|
sub SyncAnyPendingShadowChanges {
|
|
if ($shadowchanges) {
|
|
my $pid;
|
|
FORK: {
|
|
if ($pid = fork) { # create a fork
|
|
# parent code runs here
|
|
$shadowchanges = 0;
|
|
return;
|
|
} elsif (defined $pid) {
|
|
# child process code runs here
|
|
exec("./syncshadowdb","--") or die "Unable to exec syncshadowdb: $!";
|
|
# the idea was that passing the second parameter tricks it into
|
|
# using execvp instead of running a shell. Not really necessary since
|
|
# there are no shell meta-characters, but it passes our tinderbox
|
|
# test that way. :) http://bugzilla.mozilla.org/show_bug.cgi?id=21253
|
|
} elsif ($! =~ /No more process/) {
|
|
# recoverable fork error, try again in 5 seconds
|
|
sleep 5;
|
|
redo FORK;
|
|
} else {
|
|
# something weird went wrong
|
|
die "Can't create background process to run syncshadowdb: $!";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
# This is used to manipulate global state used by SendSQL(),
|
|
# MoreSQLData() and FetchSQLData(). It provides a way to do another
|
|
# SQL query without losing any as-yet-unfetched data from an existing
|
|
# query. Just push the current global state, do your new query and fetch
|
|
# any data you need from it, then pop the current global state.
|
|
#
|
|
@::SQLStateStack = ();
|
|
|
|
sub PushGlobalSQLState() {
|
|
push @::SQLStateStack, $::currentquery;
|
|
push @::SQLStateStack, [ @::fetchahead ];
|
|
}
|
|
|
|
sub PopGlobalSQLState() {
|
|
die ("PopGlobalSQLState: stack underflow") if ( $#::SQLStateStack < 1 );
|
|
@::fetchahead = @{pop @::SQLStateStack};
|
|
$::currentquery = pop @::SQLStateStack;
|
|
}
|
|
|
|
sub SavedSQLStates() {
|
|
return ($#::SqlStateStack + 1) / 2;
|
|
}
|
|
|
|
|
|
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.
|
|
|
|
# if we're a subquery (ie there's pushed global state around)
|
|
# indent to indicate the level of subquery-hood
|
|
#
|
|
for (my $i = SavedSQLStates() ; $i > 0 ; $i--) {
|
|
print SQLLOGFID "\t";
|
|
}
|
|
|
|
print SQLLOGFID time2str("%D %H:%M:%S $$", time()) . ": $str\n";
|
|
}
|
|
flock(SQLLOGFID,8); # '8' is magic 'unlock' const.
|
|
close SQLLOGFID;
|
|
}
|
|
}
|
|
|
|
# This is from the perlsec page, slightly modifed to remove a warning
|
|
# From that page:
|
|
# This function makes use of the fact that the presence of
|
|
# tainted data anywhere within an expression renders the
|
|
# entire expression tainted.
|
|
# Don't ask me how it works...
|
|
sub is_tainted {
|
|
return not eval { my $foo = join('',@_), kill 0; 1; };
|
|
}
|
|
|
|
sub SendSQL {
|
|
my ($str, $dontshadow) = (@_);
|
|
|
|
# Don't use DBI's taint stuff yet, because:
|
|
# a) We don't want out vars to be tainted (yet)
|
|
# b) We want to know who called SendSQL...
|
|
# Is there a better way to do b?
|
|
if (is_tainted($str)) {
|
|
die "Attempted to send tainted string '$str' to the database";
|
|
}
|
|
|
|
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;
|
|
}
|
|
# If we are shutdown, we don't want to run queries except in special cases
|
|
if (Param('shutdownhtml')) {
|
|
if ($0 =~ m:[\\/]((do)?editparams.cgi|syncshadowdb)$:) {
|
|
$::ignorequery = 0;
|
|
} else {
|
|
$::ignorequery = 1;
|
|
return;
|
|
}
|
|
}
|
|
SqlLog($str);
|
|
$::currentquery = $::db->prepare($str);
|
|
if (!$::currentquery->execute) {
|
|
my $errstr = $::db->errstr;
|
|
# Cut down the error string to a reasonable.size
|
|
$errstr = substr($errstr, 0, 2000) . ' ... ' . substr($errstr, -2000)
|
|
if length($errstr) > 4000;
|
|
die "$str: " . $errstr;
|
|
}
|
|
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 {
|
|
# $::ignorequery is set in SendSQL
|
|
if ($::ignorequery) {
|
|
return 0;
|
|
}
|
|
if (defined @::fetchahead) {
|
|
return 1;
|
|
}
|
|
if (@::fetchahead = $::currentquery->fetchrow_array) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub FetchSQLData {
|
|
# $::ignorequery is set in SendSQL
|
|
if ($::ignorequery) {
|
|
return;
|
|
}
|
|
if (defined @::fetchahead) {
|
|
my @result = @::fetchahead;
|
|
undef @::fetchahead;
|
|
return @result;
|
|
}
|
|
return $::currentquery->fetchrow_array;
|
|
}
|
|
|
|
|
|
sub FetchOneColumn {
|
|
my @row = FetchSQLData();
|
|
return $row[0];
|
|
}
|
|
|
|
|
|
|
|
@::default_column_list = ("severity", "priority", "platform", "owner",
|
|
"status", "resolution", "summary");
|
|
|
|
sub AppendComment {
|
|
my ($bugid,$who,$comment) = (@_);
|
|
$comment =~ s/\r\n/\n/g; # Get rid of windows-style line endings.
|
|
$comment =~ s/\r/\n/g; # Get rid of mac-style line endings.
|
|
if ($comment =~ /^\s*$/) { # Nothin' but whitespace.
|
|
return;
|
|
}
|
|
|
|
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) = (@_);
|
|
my $count = 0;
|
|
foreach my $i (@$list) {
|
|
if ($i eq $item) {
|
|
return $count;
|
|
}
|
|
$count++;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
sub Product_element {
|
|
my ($prod,$onchange) = (@_);
|
|
return make_popup("product", keys %::versions, $prod, 1, $onchange);
|
|
}
|
|
|
|
sub Component_element {
|
|
my ($comp,$prod,$onchange) = (@_);
|
|
my $componentlist;
|
|
if (! defined $::components{$prod}) {
|
|
$componentlist = [];
|
|
} else {
|
|
$componentlist = $::components{$prod};
|
|
}
|
|
my $defcomponent;
|
|
if ($comp ne "" && lsearch($componentlist, $comp) >= 0) {
|
|
$defcomponent = $comp;
|
|
} else {
|
|
$defcomponent = $componentlist->[0];
|
|
}
|
|
return make_popup("component", $componentlist, $defcomponent, 1, "");
|
|
}
|
|
|
|
sub Version_element {
|
|
my ($vers, $prod, $onchange) = (@_);
|
|
my $versionlist;
|
|
if (!defined $::versions{$prod}) {
|
|
$versionlist = [];
|
|
} else {
|
|
$versionlist = $::versions{$prod};
|
|
}
|
|
my $defversion = $versionlist->[0];
|
|
if (lsearch($versionlist,$vers) >= 0) {
|
|
$defversion = $vers;
|
|
}
|
|
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.
|
|
|
|
sub PerlQuote {
|
|
my ($str) = (@_);
|
|
return SqlQuote($str);
|
|
|
|
# The below was my first attempt, but I think just using SqlQuote makes more
|
|
# sense...
|
|
# $result = "'";
|
|
# $length = length($str);
|
|
# for (my $i=0 ; $i<$length ; $i++) {
|
|
# my $c = substr($str, $i, 1);
|
|
# if ($c eq "'" || $c eq '\\') {
|
|
# $result .= '\\';
|
|
# }
|
|
# $result .= $c;
|
|
# }
|
|
# $result .= "'";
|
|
# return $result;
|
|
}
|
|
|
|
|
|
# Given the name of a global variable, generate Perl code that, if later
|
|
# executed, would restore the variable to its current value.
|
|
|
|
sub GenerateCode {
|
|
my ($name) = (@_);
|
|
my $result = $name . " = ";
|
|
if ($name =~ /^\$/) {
|
|
my $value = eval($name);
|
|
if (ref($value) eq "ARRAY") {
|
|
$result .= "[" . GenerateArrayCode($value) . "]";
|
|
} else {
|
|
$result .= PerlQuote(eval($name));
|
|
}
|
|
} elsif ($name =~ /^@/) {
|
|
my @value = eval($name);
|
|
$result .= "(" . GenerateArrayCode(\@value) . ")";
|
|
} elsif ($name =~ '%') {
|
|
$result = "";
|
|
foreach my $k (sort { uc($a) cmp uc($b)} eval("keys $name")) {
|
|
$result .= GenerateCode("\$" . substr($name, 1) .
|
|
"{" . PerlQuote($k) . "}");
|
|
}
|
|
return $result;
|
|
} else {
|
|
die "Can't do $name -- unacceptable variable type.";
|
|
}
|
|
$result .= ";\n";
|
|
return $result;
|
|
}
|
|
|
|
sub GenerateArrayCode {
|
|
my ($ref) = (@_);
|
|
my @list;
|
|
foreach my $i (@$ref) {
|
|
push @list, PerlQuote($i);
|
|
}
|
|
return join(',', @list);
|
|
}
|
|
|
|
|
|
|
|
sub GenerateVersionTable {
|
|
ConnectToDatabase();
|
|
SendSQL("select value, program from versions order by value");
|
|
my @line;
|
|
my %varray;
|
|
my %carray;
|
|
while (@line = FetchSQLData()) {
|
|
my ($v,$p1) = (@line);
|
|
if (!defined $::versions{$p1}) {
|
|
$::versions{$p1} = [];
|
|
}
|
|
push @{$::versions{$p1}}, $v;
|
|
$varray{$v} = 1;
|
|
}
|
|
SendSQL("select value, program from components order by value");
|
|
while (@line = FetchSQLData()) {
|
|
my ($c,$p) = (@line);
|
|
if (!defined $::components{$p}) {
|
|
$::components{$p} = [];
|
|
}
|
|
my $ref = $::components{$p};
|
|
push @$ref, $c;
|
|
$carray{$c} = 1;
|
|
}
|
|
|
|
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 ORDER BY product");
|
|
$::anyvotesallowed = 0;
|
|
while (@line = FetchSQLData()) {
|
|
my ($p, $d, $votesperuser, $dis, $u) = (@line);
|
|
$::proddesc{$p} = $d;
|
|
if (!$dis) {
|
|
push @::enterable_products, $p;
|
|
}
|
|
if ($dotargetmilestone) {
|
|
$::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", "lastdiffed") {
|
|
my $w = lsearch(\@::log_columns, $i);
|
|
if ($w >= 0) {
|
|
splice(@::log_columns, $w, 1);
|
|
}
|
|
}
|
|
@::log_columns = (sort(@::log_columns));
|
|
|
|
@::legal_priority = SplitEnumType($cols->{"priority,type"});
|
|
@::legal_severity = SplitEnumType($cols->{"bug_severity,type"});
|
|
@::legal_platform = SplitEnumType($cols->{"rep_platform,type"});
|
|
@::legal_opsys = SplitEnumType($cols->{"op_sys,type"});
|
|
@::legal_bug_status = SplitEnumType($cols->{"bug_status,type"});
|
|
@::legal_resolution = SplitEnumType($cols->{"resolution,type"});
|
|
|
|
# 'settable_resolution' is the list of resolutions that may be set
|
|
# directly by hand in the bug form. Start with the list of legal
|
|
# resolutions and remove 'MOVED' and 'DUPLICATE' because setting
|
|
# bugs to those resolutions requires a special process.
|
|
#
|
|
@::settable_resolution = @::legal_resolution;
|
|
my $w = lsearch(\@::settable_resolution, "DUPLICATE");
|
|
if ($w >= 0) {
|
|
splice(@::settable_resolution, $w, 1);
|
|
}
|
|
my $z = lsearch(\@::settable_resolution, "MOVED");
|
|
if ($z >= 0) {
|
|
splice(@::settable_resolution, $z, 1);
|
|
}
|
|
|
|
my @list = sort { uc($a) cmp uc($b)} keys(%::versions);
|
|
@::legal_product = @list;
|
|
mkdir("data", 0777);
|
|
chmod 0777, "data";
|
|
my $tmpname = "data/versioncache.$$";
|
|
open(FID, ">$tmpname") || die "Can't create $tmpname";
|
|
|
|
print FID "#\n";
|
|
print FID "# DO NOT EDIT!\n";
|
|
print FID "# This file is automatically generated at least once every\n";
|
|
print FID "# hour by the GenerateVersionTable() sub in globals.pl.\n";
|
|
print FID "# Any changes you make will be overwritten.\n";
|
|
print FID "#\n";
|
|
|
|
print FID GenerateCode('@::log_columns');
|
|
print FID GenerateCode('%::versions');
|
|
|
|
foreach my $i (@list) {
|
|
if (!defined $::components{$i}) {
|
|
$::components{$i} = [];
|
|
}
|
|
}
|
|
@::legal_versions = sort {uc($a) cmp uc($b)} keys(%varray);
|
|
print FID GenerateCode('@::legal_versions');
|
|
print FID GenerateCode('%::components');
|
|
@::legal_components = sort {uc($a) cmp uc($b)} keys(%carray);
|
|
print FID GenerateCode('@::legal_components');
|
|
foreach my $i('product', 'priority', 'severity', 'platform', 'opsys',
|
|
'bug_status', 'resolution') {
|
|
print FID GenerateCode('@::legal_' . $i);
|
|
}
|
|
print FID GenerateCode('@::settable_resolution');
|
|
print FID GenerateCode('%::proddesc');
|
|
print FID GenerateCode('@::enterable_products');
|
|
print FID GenerateCode('%::prodmaxvotes');
|
|
print FID GenerateCode('$::anyvotesallowed');
|
|
|
|
if ($dotargetmilestone) {
|
|
# 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();
|
|
push(@::legal_keywords, $name);
|
|
$name = lc($name);
|
|
$::keywordsbyname{$name} = $id;
|
|
}
|
|
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";
|
|
chmod 0666, "data/versioncache";
|
|
}
|
|
|
|
|
|
sub GetKeywordIdFromName {
|
|
my ($name) = (@_);
|
|
$name = lc($name);
|
|
return $::keywordsbyname{$name};
|
|
}
|
|
|
|
|
|
|
|
|
|
# Returns the modification time of a file.
|
|
|
|
sub ModTime {
|
|
my ($filename) = (@_);
|
|
my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
|
|
$atime,$mtime,$ctime,$blksize,$blocks)
|
|
= stat($filename);
|
|
return $mtime;
|
|
}
|
|
|
|
|
|
|
|
# This proc must be called before using legal_product or the versions array.
|
|
|
|
$::VersionTableLoaded = 0;
|
|
sub GetVersionTable {
|
|
return if $::VersionTableLoaded;
|
|
my $mtime = ModTime("data/versioncache");
|
|
if (!defined $mtime || $mtime eq "") {
|
|
$mtime = 0;
|
|
}
|
|
if (time() - $mtime > 3600) {
|
|
GenerateVersionTable();
|
|
}
|
|
require 'data/versioncache';
|
|
if (!defined %::versions) {
|
|
GenerateVersionTable();
|
|
do 'data/versioncache';
|
|
|
|
if (!defined %::versions) {
|
|
die "Can't generate file data/versioncache";
|
|
}
|
|
}
|
|
$::VersionTableLoaded = 1;
|
|
}
|
|
|
|
|
|
sub InsertNewUser {
|
|
my ($username, $realname) = (@_);
|
|
|
|
# Generate a new random password for the user.
|
|
my $password = GenerateRandomPassword();
|
|
my $cryptpassword = Crypt($password);
|
|
|
|
# Determine what groups the user should be in by default
|
|
# and add them to those groups.
|
|
PushGlobalSQLState();
|
|
SendSQL("select bit, userregexp from groups where userregexp != ''");
|
|
my $groupset = "0";
|
|
while (MoreSQLData()) {
|
|
my @row = FetchSQLData();
|
|
# 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
|
|
# does that.
|
|
}
|
|
}
|
|
|
|
# Insert the new user record into the database.
|
|
$username = SqlQuote($username);
|
|
$realname = SqlQuote($realname);
|
|
$cryptpassword = SqlQuote($cryptpassword);
|
|
SendSQL("INSERT INTO profiles (login_name, realname, cryptpassword, groupset)
|
|
VALUES ($username, $realname, $cryptpassword, $groupset)");
|
|
PopGlobalSQLState();
|
|
|
|
# Return the password to the calling code so it can be included
|
|
# in an email sent to the user.
|
|
return $password;
|
|
}
|
|
|
|
# Removes all entries from logincookies for $userid, except for the
|
|
# optional $keep, which refers the logincookies.cookie primary key.
|
|
# (This is useful so that a user changing their password stays logged in)
|
|
sub InvalidateLogins {
|
|
my ($userid, $keep) = @_;
|
|
|
|
my $remove = "DELETE FROM logincookies WHERE userid = $userid";
|
|
if (defined $keep) {
|
|
$remove .= " AND cookie != " . SqlQuote($keep);
|
|
}
|
|
SendSQL($remove);
|
|
}
|
|
|
|
sub GenerateRandomPassword {
|
|
my ($size) = @_;
|
|
|
|
# Generated passwords are eight characters long by default.
|
|
$size ||= 8;
|
|
|
|
# The list of characters that can appear in a randomly generated password.
|
|
# Note that users can put any character into a password they choose themselves.
|
|
my @pwchars = (0..9, 'A'..'Z', 'a'..'z', '-', '_', '!', '@', '#', '$', '%', '^', '&', '*');
|
|
|
|
# The number of characters in the list.
|
|
my $pwcharslen = scalar(@pwchars);
|
|
|
|
# Generate the password.
|
|
my $password = "";
|
|
for ( my $i=0 ; $i<$size ; $i++ ) {
|
|
$password .= $pwchars[rand($pwcharslen)];
|
|
}
|
|
|
|
# Return the password.
|
|
return $password;
|
|
}
|
|
|
|
sub SelectVisible {
|
|
my ($query, $userid, $usergroupset) = @_;
|
|
|
|
# Run the SQL $query with the additional restriction that
|
|
# the bugs can be seen by $userid. $usergroupset is provided
|
|
# as an optimisation when this is already known, eg from CGI.pl
|
|
# If not present, it will be obtained from the db.
|
|
# Assumes that 'bugs' is mentioned as a table name. You should
|
|
# also make sure that bug_id is qualified bugs.bug_id!
|
|
# Your query must have a WHERE clause. This is unlikely to be a problem.
|
|
|
|
# Also, note that mySQL requires aliases for tables to be locked, as well
|
|
# This means that if you change the name from selectVisible_cc (or add
|
|
# additional tables), you will need to update anywhere which does a
|
|
# LOCK TABLE, and then calls routines which call this
|
|
|
|
$usergroupset = 0 unless $userid;
|
|
|
|
unless (defined($usergroupset)) {
|
|
PushGlobalSQLState();
|
|
SendSQL("SELECT groupset FROM profiles WHERE userid = $userid");
|
|
$usergroupset = FetchOneColumn();
|
|
PopGlobalSQLState();
|
|
}
|
|
|
|
# Users are authorized to access bugs if they are a member of all
|
|
# groups to which the bug is restricted. User group membership and
|
|
# bug restrictions are stored as bits within bitsets, so authorization
|
|
# can be determined by comparing the intersection of the user's
|
|
# bitset with the bug's bitset. If the result matches the bug's bitset
|
|
# the user is a member of all groups to which the bug is restricted
|
|
# and is authorized to access the bug.
|
|
|
|
# A user is also authorized to access a bug if she is the reporter,
|
|
# or member of the cc: list of the bug and the bug allows users in those
|
|
# roles to see the bug. The boolean fields reporter_accessible and
|
|
# cclist_accessible identify whether or not those roles can see the bug.
|
|
|
|
# Bit arithmetic is performed by MySQL instead of Perl because bitset
|
|
# fields in the database are 64 bits wide (BIGINT), and Perl installations
|
|
# may or may not support integers larger than 32 bits. Using bitsets
|
|
# and doing bitset arithmetic is probably not cross-database compatible,
|
|
# however, so these mechanisms are likely to change in the future.
|
|
|
|
my $replace = " ";
|
|
|
|
if ($userid) {
|
|
$replace .= "LEFT JOIN cc selectVisible_cc ON
|
|
bugs.bug_id = selectVisible_cc.bug_id AND
|
|
selectVisible_cc.who = $userid "
|
|
}
|
|
|
|
$replace .= "WHERE ((bugs.groupset & $usergroupset) = bugs.groupset ";
|
|
|
|
if ($userid) {
|
|
# There is a mysql bug affecting v3.22 and 3.23 (at least), where this will
|
|
# cause all rows to be returned! We work arround this by adding an not isnull
|
|
# test to the JOINed cc table. See http://lists.mysql.com/cgi-ez/ezmlm-cgi?9:mss:11417
|
|
# Its needed, even though it shouldn't be
|
|
$replace .= "OR (bugs.reporter_accessible = 1 AND bugs.reporter = $userid)
|
|
OR (bugs.cclist_accessible = 1 AND selectVisible_cc.who = $userid AND not isnull(selectVisible_cc.who))";
|
|
}
|
|
|
|
$replace .= ") AND ";
|
|
|
|
$query =~ s/\sWHERE\s/$replace/i;
|
|
|
|
return $query;
|
|
}
|
|
|
|
sub CanSeeBug {
|
|
# Note that we pass in the usergroupset, since this is known
|
|
# in most cases (ie viewing bugs). Maybe make this an optional
|
|
# parameter?
|
|
|
|
my ($id, $userid, $usergroupset) = @_;
|
|
|
|
# Query the database for the bug, retrieving a boolean value that
|
|
# represents whether or not the user is authorized to access the bug.
|
|
|
|
PushGlobalSQLState();
|
|
SendSQL(SelectVisible("SELECT bugs.bug_id FROM bugs WHERE bugs.bug_id = $id",
|
|
$userid, $usergroupset));
|
|
|
|
my $ret = defined(FetchSQLData());
|
|
PopGlobalSQLState();
|
|
|
|
return $ret;
|
|
}
|
|
|
|
sub ValidatePassword {
|
|
# Determines whether or not a password is valid (i.e. meets Bugzilla's
|
|
# requirements for length and content). If the password is valid, the
|
|
# function returns boolean false. Otherwise it returns an error message
|
|
# (synonymous with boolean true) that can be displayed to the user.
|
|
|
|
# If a second password is passed in, this function also verifies that
|
|
# the two passwords match.
|
|
|
|
my ($password, $matchpassword) = @_;
|
|
|
|
if ( length($password) < 3 ) {
|
|
return "The password is less than three characters long. It must be at least three characters.";
|
|
} elsif ( length($password) > 16 ) {
|
|
return "The password is more than 16 characters long. It must be no more than 16 characters.";
|
|
} elsif ( $matchpassword && $password ne $matchpassword ) {
|
|
return "The two passwords do not match.";
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
sub Crypt {
|
|
# Crypts a password, generating a random salt to do it.
|
|
# Random salts are generated because the alternative is usually
|
|
# to use the first two characters of the password itself, and since
|
|
# the salt appears in plaintext at the beginning of the crypted
|
|
# password string this has the effect of revealing the first two
|
|
# characters of the password to anyone who views the crypted version.
|
|
|
|
my ($password) = @_;
|
|
|
|
# The list of characters that can appear in a salt. Salts and hashes
|
|
# are both encoded as a sequence of characters from a set containing
|
|
# 64 characters, each one of which represents 6 bits of the salt/hash.
|
|
# The encoding is similar to BASE64, the difference being that the
|
|
# BASE64 plus sign (+) is replaced with a forward slash (/).
|
|
my @saltchars = (0..9, 'A'..'Z', 'a'..'z', '.', '/');
|
|
|
|
# Generate the salt. We use an 8 character (48 bit) salt for maximum
|
|
# security on systems whose crypt uses MD5. Systems with older
|
|
# versions of crypt will just use the first two characters of the salt.
|
|
my $salt = '';
|
|
for ( my $i=0 ; $i < 8 ; ++$i ) {
|
|
$salt .= $saltchars[rand(64)];
|
|
}
|
|
|
|
# Crypt the password.
|
|
my $cryptedpassword = crypt($password, $salt);
|
|
|
|
# Return the crypted password.
|
|
return $cryptedpassword;
|
|
}
|
|
|
|
|
|
sub DBID_to_real_or_loginname {
|
|
my ($id) = (@_);
|
|
PushGlobalSQLState();
|
|
SendSQL("SELECT login_name,realname FROM profiles WHERE userid = $id");
|
|
my ($l, $r) = FetchSQLData();
|
|
PopGlobalSQLState();
|
|
if (!defined $r || $r eq "") {
|
|
return $l;
|
|
} else {
|
|
return "$l ($r)";
|
|
}
|
|
}
|
|
|
|
sub DBID_to_name {
|
|
my ($id) = (@_);
|
|
# $id should always be a positive integer
|
|
if ($id =~ m/^([1-9][0-9]*)$/) {
|
|
$id = $1;
|
|
} else {
|
|
$::cachedNameArray{$id} = "__UNKNOWN__";
|
|
}
|
|
if (!defined $::cachedNameArray{$id}) {
|
|
PushGlobalSQLState();
|
|
SendSQL("select login_name from profiles where userid = $id");
|
|
my $r = FetchOneColumn();
|
|
PopGlobalSQLState();
|
|
if (!defined $r || $r eq "") {
|
|
$r = "__UNKNOWN__";
|
|
}
|
|
$::cachedNameArray{$id} = $r;
|
|
}
|
|
return $::cachedNameArray{$id};
|
|
}
|
|
|
|
sub DBname_to_id {
|
|
my ($name) = (@_);
|
|
PushGlobalSQLState();
|
|
SendSQL("select userid from profiles where login_name = @{[SqlQuote($name)]}");
|
|
my $r = FetchOneColumn();
|
|
PopGlobalSQLState();
|
|
# $r should be a positive integer, this makes Taint mode happy
|
|
if (defined $r && $r =~ m/^([1-9][0-9]*)$/) {
|
|
return $1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
|
|
sub DBNameToIdAndCheck {
|
|
my ($name, $forceok) = (@_);
|
|
$name = html_quote($name);
|
|
my $result = DBname_to_id($name);
|
|
if ($result > 0) {
|
|
return $result;
|
|
}
|
|
if ($forceok) {
|
|
InsertNewUser($name, "");
|
|
$result = DBname_to_id($name);
|
|
if ($result > 0) {
|
|
return $result;
|
|
}
|
|
print "Yikes; couldn't create user $name. Please report problem to " .
|
|
Param("maintainer") ."\n";
|
|
} else {
|
|
print "\n"; # http://bugzilla.mozilla.org/show_bug.cgi?id=80045
|
|
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);
|
|
}
|
|
|
|
# Use trick_taint() when you know that there is no way that the data
|
|
# in a scalar can be tainted, but taint mode still bails on it.
|
|
# WARNING!! Using this routine on data that really could be tainted
|
|
# defeats the purpose of taint mode. It should only be
|
|
# used on variables that cannot be touched by users.
|
|
|
|
sub trick_taint {
|
|
$_[0] =~ /^(.*)$/s;
|
|
$_[0] = $1;
|
|
return (defined($_[0]));
|
|
}
|
|
|
|
sub detaint_natural {
|
|
$_[0] =~ /^(\d+)$/;
|
|
$_[0] = $1;
|
|
return (defined($_[0]));
|
|
}
|
|
|
|
# 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;
|
|
}
|
|
# Either a comment string or no comma and a compulsory #.
|
|
while ($text =~ s/\bbug(\s|%\#)*(\d+),?\s*comment\s*(\s|%\#)(\d+)/"##$count##"/ei) {
|
|
my $item = $&;
|
|
my $bugnum = $2;
|
|
my $comnum = $4;
|
|
$item = GetBugLink($bugnum, $item);
|
|
$item =~ s/(id=\d+)/$1#c$comnum/;
|
|
$things[$count++] = $item;
|
|
}
|
|
while ($text =~ s/\bcomment(\s|%\#)*(\d+)/"##$count##"/ei) {
|
|
my $item = $&;
|
|
my $num = $2;
|
|
$item = value_quote($item);
|
|
$item = qq{<A HREF="#c$num">$item</A>};
|
|
$things[$count++] = $item;
|
|
}
|
|
while ($text =~ s/\bbug(\s|%\#)*(\d+)/"##$count##"/ei) {
|
|
my $item = $&;
|
|
my $num = $2;
|
|
$item = GetBugLink($num, $item);
|
|
$things[$count++] = $item;
|
|
}
|
|
while ($text =~ s/\battachment(\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="attachment.cgi?id=$num&action=view">$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;
|
|
my $bug_link;
|
|
$bug_link = GetBugLink($num, $num);
|
|
$item =~ s@\d+@$bug_link@;
|
|
$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="attachment.cgi?id=$num&action=view">$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;
|
|
}
|
|
|
|
# This is a new subroutine written 12/20/00 for the purpose of processing a
|
|
# link to a bug. It can be called using "GetBugLink (<BugNumber>, <LinkText>);"
|
|
# Where <BugNumber> is the number of the bug and <LinkText> is what apprears
|
|
# between '<a>' and '</a>'.
|
|
|
|
sub GetBugLink {
|
|
my ($bug_num, $link_text) = (@_);
|
|
detaint_natural($bug_num) || die "GetBugLink() called with non-integer bug number";
|
|
|
|
# If we've run GetBugLink() for this bug number before, %::buglink
|
|
# will contain an anonymous array ref of relevent values, if not
|
|
# we need to get the information from the database.
|
|
if (! defined $::buglink{$bug_num}) {
|
|
# Make sure any unfetched data from a currently running query
|
|
# is saved off rather than overwritten
|
|
PushGlobalSQLState();
|
|
|
|
SendSQL("SELECT bugs.bug_status, resolution, short_desc, groupset " .
|
|
"FROM bugs WHERE bugs.bug_id = $bug_num");
|
|
|
|
# If the bug exists, save its data off for use later in the sub
|
|
if (MoreSQLData()) {
|
|
my ($bug_state, $bug_res, $bug_desc, $bug_grp) = FetchSQLData();
|
|
# Initialize these variables to be "" so that we don't get warnings
|
|
# if we don't change them below (which is highly likely).
|
|
my ($pre, $title, $post) = ("", "", "");
|
|
|
|
$title = $bug_state;
|
|
if ($bug_state eq $::unconfirmedstate) {
|
|
$pre = "<i>";
|
|
$post = "</i>";
|
|
}
|
|
elsif (! IsOpenedState($bug_state)) {
|
|
$pre = "<strike>";
|
|
$title .= " $bug_res";
|
|
$post = "</strike>";
|
|
}
|
|
if ($bug_grp == 0 || CanSeeBug($bug_num, $::userid, $::usergroupset)) {
|
|
$title .= " - $bug_desc";
|
|
}
|
|
$::buglink{$bug_num} = [$pre, value_quote($title), $post];
|
|
}
|
|
else {
|
|
# Even if there's nothing in the database, we want to save a blank
|
|
# anonymous array in the %::buglink hash so the query doesn't get
|
|
# run again next time we're called for this bug number.
|
|
$::buglink{$bug_num} = [];
|
|
}
|
|
# All done with this sidetrip
|
|
PopGlobalSQLState();
|
|
}
|
|
|
|
# Now that we know we've got all the information we're gonna get, let's
|
|
# return the link (which is the whole reason we were called :)
|
|
my ($pre, $title, $post) = @{$::buglink{$bug_num}};
|
|
# $title will be undefined if the bug didn't exist in the database.
|
|
if (defined $title) {
|
|
return qq{$pre<a href="show_bug.cgi?id=$bug_num" title="$title">$link_text</a>$post};
|
|
}
|
|
else {
|
|
return qq{$link_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 ");
|
|
|
|
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, $when, $text) = (FetchSQLData());
|
|
if ($count) {
|
|
$result .= "\n\n------- Additional Comments From $who".Param('emailsuffix')." ".
|
|
time2str("%Y-%m-%d %H:%M", str2time($when)) . " -------\n";
|
|
}
|
|
$result .= $text;
|
|
$count++;
|
|
}
|
|
|
|
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());
|
|
$email .= Param('emailsuffix');
|
|
if ($count) {
|
|
$result .= qq|<BR><BR><I>------- Additional Comment <a name="c$count" href="#c$count">#$count</a> From |;
|
|
if ($who) {
|
|
$result .= qq{<A HREF="mailto:$email">$who</A> };
|
|
} else {
|
|
$result .= qq{<A HREF="mailto:$email">$email</A> };
|
|
}
|
|
|
|
$result .= 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:
|
|
# -list- the list of column names
|
|
# <name>,type the type for the given name
|
|
|
|
sub LearnAboutColumns {
|
|
my ($table) = (@_);
|
|
my %a;
|
|
SendSQL("show columns from $table");
|
|
my @list = ();
|
|
my @row;
|
|
while (@row = FetchSQLData()) {
|
|
my ($name,$type) = (@row);
|
|
$a{"$name,type"} = $type;
|
|
push @list, $name;
|
|
}
|
|
$a{"-list-"} = \@list;
|
|
return \%a;
|
|
}
|
|
|
|
|
|
|
|
# If the above returned a enum type, take that type and parse it into the
|
|
# list of values. Assumes that enums don't ever contain an apostrophe!
|
|
|
|
sub SplitEnumType {
|
|
my ($str) = (@_);
|
|
my @result = ();
|
|
if ($str =~ /^enum\((.*)\)$/) {
|
|
my $guts = $1 . ",";
|
|
while ($guts =~ /^\'([^\']*)\',(.*)$/) {
|
|
push @result, $1;
|
|
$guts = $2;
|
|
}
|
|
}
|
|
return @result;
|
|
}
|
|
|
|
|
|
# This routine is largely copied from Mysql.pm.
|
|
|
|
sub SqlQuote {
|
|
my ($str) = (@_);
|
|
# if (!defined $str) {
|
|
# confess("Undefined passed to SqlQuote");
|
|
# }
|
|
$str =~ s/([\\\'])/\\$1/g;
|
|
$str =~ s/\0/\\0/g;
|
|
# If it's been SqlQuote()ed, then it's safe, so we tell -T that.
|
|
trick_taint($str);
|
|
return "'$str'";
|
|
}
|
|
|
|
|
|
|
|
sub UserInGroup {
|
|
my ($groupname) = (@_);
|
|
if ($::usergroupset eq "0") {
|
|
return 0;
|
|
}
|
|
ConnectToDatabase();
|
|
PushGlobalSQLState();
|
|
SendSQL("select (bit & $::usergroupset) != 0 from groups where name = " . SqlQuote($groupname));
|
|
my $bit = FetchOneColumn();
|
|
PopGlobalSQLState();
|
|
if ($bit) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub BugInGroup {
|
|
my ($bugid, $groupname) = (@_);
|
|
my $groupbit = GroupNameToBit($groupname);
|
|
PushGlobalSQLState();
|
|
SendSQL("SELECT (bugs.groupset & $groupbit) != 0 FROM bugs WHERE bugs.bug_id = $bugid");
|
|
my $bugingroup = FetchOneColumn();
|
|
PopGlobalSQLState();
|
|
return $bugingroup;
|
|
}
|
|
|
|
sub GroupExists {
|
|
my ($groupname) = (@_);
|
|
ConnectToDatabase();
|
|
SendSQL("select count(*) from groups where name=" . SqlQuote($groupname));
|
|
my $count = FetchOneColumn();
|
|
return $count;
|
|
}
|
|
|
|
# Given the name of an existing group, returns the bit associated with it.
|
|
# If the group does not exist, returns 0.
|
|
# !!! Remove this function when the new group system is implemented!
|
|
sub GroupNameToBit {
|
|
my ($groupname) = (@_);
|
|
ConnectToDatabase();
|
|
PushGlobalSQLState();
|
|
SendSQL("SELECT bit FROM groups WHERE name = " . SqlQuote($groupname));
|
|
my $bit = FetchOneColumn() || 0;
|
|
PopGlobalSQLState();
|
|
return $bit;
|
|
}
|
|
|
|
# Determines whether or not a group is active by checking
|
|
# the "isactive" column for the group in the "groups" table.
|
|
# Note: This function selects groups by bit rather than by name.
|
|
sub GroupIsActive {
|
|
my ($groupbit) = (@_);
|
|
$groupbit ||= 0;
|
|
ConnectToDatabase();
|
|
SendSQL("select isactive from groups where bit=$groupbit");
|
|
my $isactive = FetchOneColumn();
|
|
return $isactive;
|
|
}
|
|
|
|
# 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 (grep($_ eq $state, OpenStates())) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
# This sub will return an array containing any status that
|
|
# is considered an open bug.
|
|
|
|
sub OpenStates {
|
|
return ('NEW', 'REOPENED', 'ASSIGNED', $::unconfirmedstate);
|
|
}
|
|
|
|
|
|
sub RemoveVotes {
|
|
my ($id, $who, $reason) = (@_);
|
|
ConnectToDatabase();
|
|
my $whopart = "";
|
|
if ($who) {
|
|
$whopart = " AND votes.who = $who";
|
|
}
|
|
SendSQL("SELECT profiles.login_name, profiles.userid, votes.count, " .
|
|
"products.votesperuser, products.maxvotesperbug " .
|
|
"FROM profiles " .
|
|
"LEFT JOIN votes ON profiles.userid = votes.who " .
|
|
"LEFT JOIN bugs USING(bug_id) " .
|
|
"LEFT JOIN products USING(product)" .
|
|
"WHERE votes.bug_id = $id " .
|
|
$whopart);
|
|
my @list;
|
|
while (MoreSQLData()) {
|
|
my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = (FetchSQLData());
|
|
push(@list, [$name, $userid, $oldvotes, $votesperuser, $maxvotesperbug]);
|
|
}
|
|
if (0 < @list) {
|
|
foreach my $ref (@list) {
|
|
my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = (@$ref);
|
|
my $s;
|
|
|
|
$maxvotesperbug = $votesperuser if ($votesperuser < $maxvotesperbug);
|
|
|
|
# If this product allows voting and the user's votes are in
|
|
# the acceptable range, then don't do anything.
|
|
next if $votesperuser && $oldvotes <= $maxvotesperbug;
|
|
|
|
# If the user has more votes on this bug than this product
|
|
# allows, then reduce the number of votes so it fits
|
|
my $newvotes = $votesperuser ? $maxvotesperbug : 0;
|
|
|
|
my $removedvotes = $oldvotes - $newvotes;
|
|
|
|
$s = $oldvotes == 1 ? "" : "s";
|
|
my $oldvotestext = "You had $oldvotes vote$s on this bug.";
|
|
|
|
$s = $removedvotes == 1 ? "" : "s";
|
|
my $removedvotestext = "You had $removedvotes vote$s removed from this bug.";
|
|
|
|
my $newvotestext;
|
|
if ($newvotes) {
|
|
SendSQL("UPDATE votes SET count = $newvotes " .
|
|
"WHERE bug_id = $id AND who = $userid");
|
|
$s = $newvotes == 1 ? "" : "s";
|
|
$newvotestext = "You still have $newvotes vote$s on this bug."
|
|
} else {
|
|
SendSQL("DELETE FROM votes WHERE bug_id = $id AND who = $userid");
|
|
$newvotestext = "You have no more votes remaining on this bug.";
|
|
}
|
|
|
|
# Notice that we did not make sure that the user fit within the $votesperuser
|
|
# range. This is considered to be an acceptable alternative to losing votes
|
|
# during product moves. Then next time the user attempts to change their votes,
|
|
# they will be forced to fit within the $votesperuser limit.
|
|
|
|
# Now lets send the e-mail to alert the user to the fact that their votes have
|
|
# been reduced or removed.
|
|
my $sendmailparm = '-ODeliveryMode=deferred';
|
|
if (Param('sendmailnow')) {
|
|
$sendmailparm = '';
|
|
}
|
|
if (open(SENDMAIL, "|/usr/lib/sendmail $sendmailparm -ti")) {
|
|
my %substs;
|
|
|
|
$substs{"to"} = $name;
|
|
$substs{"bugid"} = $id;
|
|
$substs{"reason"} = $reason;
|
|
|
|
$substs{"votesremoved"} = $removedvotes;
|
|
$substs{"votesold"} = $oldvotes;
|
|
$substs{"votesnew"} = $newvotes;
|
|
|
|
$substs{"votesremovedtext"} = $removedvotestext;
|
|
$substs{"votesoldtext"} = $oldvotestext;
|
|
$substs{"votesnewtext"} = $newvotestext;
|
|
|
|
$substs{"count"} = $removedvotes . "\n " . $newvotestext;
|
|
|
|
my $msg = PerformSubsts(Param("voteremovedmail"),
|
|
\%substs);
|
|
print SENDMAIL $msg;
|
|
close SENDMAIL;
|
|
}
|
|
}
|
|
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 ($) {
|
|
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
|
|
# anyone who maliciously tries to tweak the version number by editing
|
|
# the params file. Not to mention that in 2.0, there was a bug that
|
|
# wrote the version number out to the params file...
|
|
my $v = $::param{'version'};
|
|
require "data/params";
|
|
$::param{'version'} = $v;
|
|
}
|
|
if (defined $::param{$value}) {
|
|
return $::param{$value};
|
|
}
|
|
# Well, that didn't help. Maybe it's a new param, and the user
|
|
# hasn't defined anything for it. Try and load a default value
|
|
# for it.
|
|
require "defparams.pl";
|
|
WriteParams();
|
|
if (defined $::param{$value}) {
|
|
return $::param{$value};
|
|
}
|
|
# We're pimped.
|
|
die "Can't find param named $value";
|
|
}
|
|
|
|
# Take two comma or space separated strings and return what
|
|
# values were removed from or added to the new one.
|
|
sub DiffStrings {
|
|
my ($oldstr, $newstr) = @_;
|
|
|
|
# Split the old and new strings into arrays containing their values.
|
|
$oldstr =~ s/[\s,]+/ /g;
|
|
$newstr =~ s/[\s,]+/ /g;
|
|
my @old = split(" ", $oldstr);
|
|
my @new = split(" ", $newstr);
|
|
|
|
my (@remove, @add) = ();
|
|
|
|
# Find values that were removed
|
|
foreach my $value (@old) {
|
|
push (@remove, $value) if !grep($_ eq $value, @new);
|
|
}
|
|
|
|
# Find values that were added
|
|
foreach my $value (@new) {
|
|
push (@add, $value) if !grep($_ eq $value, @old);
|
|
}
|
|
|
|
my $removed = join (", ", @remove);
|
|
my $added = join (", ", @add);
|
|
|
|
return ($removed, $added);
|
|
}
|
|
|
|
sub PerformSubsts {
|
|
my ($str, $substs) = (@_);
|
|
$str =~ s/%([a-z]*)%/(defined $substs->{$1} ? $substs->{$1} : Param($1))/eg;
|
|
return $str;
|
|
}
|
|
|
|
# Min and max routines.
|
|
sub min {
|
|
my $min = shift(@_);
|
|
foreach my $val (@_) {
|
|
$min = $val if $val < $min;
|
|
}
|
|
return $min;
|
|
}
|
|
|
|
sub max {
|
|
my $max = shift(@_);
|
|
foreach my $val (@_) {
|
|
$max = $val if $val > $max;
|
|
}
|
|
return $max;
|
|
}
|
|
|
|
# Trim whitespace from front and back.
|
|
|
|
sub trim {
|
|
my ($str) = @_;
|
|
$str =~ s/^\s+//g;
|
|
$str =~ s/\s+$//g;
|
|
return $str;
|
|
}
|
|
|
|
###############################################################################
|
|
# Global Templatization Code
|
|
|
|
# Use the template toolkit (http://www.template-toolkit.org/) to generate
|
|
# the user interface using templates in the "template/" subdirectory.
|
|
use Template;
|
|
|
|
# Create the global template object that processes templates and specify
|
|
# configuration parameters that apply to all templates processed in this script.
|
|
$::template = Template->new(
|
|
{
|
|
# Colon-separated list of directories containing templates.
|
|
INCLUDE_PATH => "template/custom:template/default" ,
|
|
|
|
# Remove white-space before template directives (PRE_CHOMP) and at the
|
|
# beginning and end of templates and template blocks (TRIM) for better
|
|
# looking, more compact content. Use the plus sign at the beginning
|
|
# of directives to maintain white space (i.e. [%+ DIRECTIVE %]).
|
|
PRE_CHOMP => 1 ,
|
|
TRIM => 1 ,
|
|
|
|
# Functions for processing text within templates in various ways.
|
|
FILTERS =>
|
|
{
|
|
# Render text in strike-through style.
|
|
strike => sub { return "<strike>" . $_[0] . "</strike>" } ,
|
|
|
|
# Returns the text with backslashes, single/double quotes,
|
|
# and newlines/carriage returns escaped for use in JS strings.
|
|
js => sub
|
|
{
|
|
my ($var) = @_;
|
|
$var =~ s/([\\\'\"])/\\$1/g;
|
|
$var =~ s/\n/\\n/g;
|
|
$var =~ s/\r/\\r/g;
|
|
return $var;
|
|
}
|
|
} ,
|
|
}
|
|
);
|
|
|
|
# Use the Toolkit Template's Stash module to add utility pseudo-methods
|
|
# to template variables.
|
|
use Template::Stash;
|
|
|
|
# Add "contains***" methods to list variables that search for one or more
|
|
# items in a list and return boolean values representing whether or not
|
|
# one/all/any item(s) were found.
|
|
$Template::Stash::LIST_OPS->{ contains } =
|
|
sub {
|
|
my ($list, $item) = @_;
|
|
return grep($_ eq $item, @$list);
|
|
};
|
|
|
|
$Template::Stash::LIST_OPS->{ containsany } =
|
|
sub {
|
|
my ($list, $items) = @_;
|
|
foreach my $item (@$items) {
|
|
return 1 if grep($_ eq $item, @$list);
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
# Add a "substr" method to the Template Toolkit's "scalar" object
|
|
# that returns a substring of a string.
|
|
$Template::Stash::SCALAR_OPS->{ substr } =
|
|
sub {
|
|
my ($scalar, $offset, $length) = @_;
|
|
return substr($scalar, $offset, $length);
|
|
};
|
|
|
|
# Add a "truncate" method to the Template Toolkit's "scalar" object
|
|
# that truncates a string to a certain length.
|
|
$Template::Stash::SCALAR_OPS->{ truncate } =
|
|
sub {
|
|
my ($string, $length, $ellipsis) = @_;
|
|
$ellipsis ||= "";
|
|
|
|
return $string if !$length || length($string) <= $length;
|
|
|
|
my $strlen = $length - length($ellipsis);
|
|
my $newstr = substr($string, 0, $strlen) . $ellipsis;
|
|
return $newstr;
|
|
};
|
|
|
|
# Define the global variables and functions that will be passed to the UI
|
|
# template. Additional values may be added to this hash before templates
|
|
# are processed.
|
|
$::vars =
|
|
{
|
|
# Function for retrieving global parameters.
|
|
'Param' => \&Param ,
|
|
|
|
# Function for processing global parameters that contain references
|
|
# to other global parameters.
|
|
'PerformSubsts' => \&PerformSubsts ,
|
|
|
|
# Generic linear search function
|
|
'lsearch' => \&lsearch ,
|
|
};
|
|
|
|
1;
|