CGI.pl 33 KB
Newer Older
1 2
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
3 4 5 6 7 8 9 10 11 12
# 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.
#
13
# The Original Code is the Bugzilla Bug Tracking System.
14
#
15
# The Initial Developer of the Original Code is Netscape Communications
16 17 18 19
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
20
# Contributor(s): Terry Weissman <terry@mozilla.org>
21
#                 Dan Mosedale <dmose@mozilla.org>
22
#                 Joe Robins <jmrobins@tgix.com>
23
#                 Dave Miller <justdave@syndicomm.com>
24
#                 Christopher Aillon <christopher@aillon.com>
25
#                 Gervase Markham <gerv@gerv.net>
26
#                 Christian Reis <kiko@async.com.br>
27 28 29 30

# Contains some global routines used throughout the CGI scripts of Bugzilla.

use strict;
31 32
use lib ".";

terry%mozilla.org's avatar
terry%mozilla.org committed
33
# use Carp;                       # for confess
34

35
use Bugzilla::Util;
36
use Bugzilla::Config;
37

38 39
# commented out the following snippet of code. this tosses errors into the
# CGI if you are perl 5.6, and doesn't if you have perl 5.003. 
40
# We want to check for the existence of the LDAP modules here.
41 42 43
# eval "use Mozilla::LDAP::Conn";
# my $have_ldap = $@ ? 0 : 1;

44 45
# Shut up misguided -w warnings about "used only once".  For some reason,
# "use vars" chokes on me when I try it here.
46

47
sub CGI_pl_sillyness {
48
    my $zz;
49
    $zz = %::dontchange;
50 51
}

52 53 54 55
use CGI::Carp qw(fatalsToBrowser);

require 'globals.pl';

56 57
use vars qw($template $vars);

58 59 60 61 62
# If Bugzilla is shut down, do not go any further, just display a message
# to the user about the downtime.  (do)editparams.cgi is exempted from
# this message, of course, since it needs to be available in order for
# the administrator to open Bugzilla back up.
if (Param("shutdownhtml") && $0 !~ m:[\\/](do)?editparams.cgi$:) {
63
    $::vars->{'message'} = "shutdown";
64 65 66 67 68 69
    
    # Return the appropriate HTTP response headers.
    print "Content-Type: text/html\n\n";
    
    # Generate and return an HTML message about the downtime.
    $::template->process("global/message.html.tmpl", $::vars)
70
      || ThrowTemplateError($::template->error());
71 72 73
    exit;
}

74 75 76 77 78 79 80 81 82 83 84
# Implementations of several of the below were blatently stolen from CGI.pm,
# by Lincoln D. Stein.

# Get rid of all the %xx encoding and the like from the given URL.
sub url_decode {
    my ($todecode) = (@_);
    $todecode =~ tr/+/ /;       # pluses become spaces
    $todecode =~ s/%([0-9a-fA-F]{2})/pack("c",hex($1))/ge;
    return $todecode;
}

85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
# 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) ){

100 101 102
        SendSQL("SELECT description FROM fielddefs WHERE name=" . SqlQuote($fieldname));
        my $result = FetchOneColumn();
        if ($result) {
103
            $vars->{'field'} = $result;
104 105
        }
        else {
106
            $vars->{'field'} = $fieldname;
107
        }
108
        
109
        ThrowCodeError("illegal_field", undef, "abort");
110 111 112 113 114 115 116 117 118
      }
}

# 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
       ) = @_;

119
    if (!defined $formRef->{$fieldname}) {
120 121 122
        $vars->{'field'} = $fieldname;  
        ThrowCodeError("undefined_field");
    }
123
}
124

125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
sub BugAliasToID {
    # Queries the database for the bug with a given alias, and returns
    # the ID of the bug if it exists or the undefined value if it doesn't.
    
    my ($alias) = @_;
    
    return undef unless Param("usebugaliases");
    
    PushGlobalSQLState();
    SendSQL("SELECT bug_id FROM bugs WHERE alias = " . SqlQuote($alias));
    my $id = FetchOneColumn();
    PopGlobalSQLState();
    
    return $id;
}

141
sub ValidateBugID {
142 143 144
    # Validates and verifies a bug ID, making sure the number is a 
    # positive integer, that it represents an existing bug in the
    # database, and that the user is authorized to access that bug.
145
    # We detaint the number here, too
146

147 148 149 150 151 152
    my ($id, $skip_authorization) = @_;
    
    # Get rid of white-space around the ID.
    $id = trim($id);
    
    # If the ID isn't a number, it might be an alias, so try to convert it.
153 154 155
    my $alias = $id;
    if (!detaint_natural($id)) {
        $id = BugAliasToID($alias);
156
        $id || ThrowUserError("invalid_bug_id_or_alias", {'bug_id' => $id});
157 158 159 160 161 162
    }
    
    # Modify the calling code's original variable to contain the trimmed,
    # converted-from-alias ID.
    $_[0] = $id;
    
163 164
    # First check that the bug exists
    SendSQL("SELECT bug_id FROM bugs WHERE bug_id = $id");
165

166
    FetchOneColumn()
167
      || ThrowUserError("invalid_bug_id_non_existent", {'bug_id' => $id});
168

169 170
    return if $skip_authorization;
    
171
    return if CanSeeBug($id, $::userid);
172 173 174 175

    # The user did not pass any of the authorization tests, which means they
    # are not authorized to see the bug.  Display an error and stop execution.
    # The error the user sees depends on whether or not they are logged in
176 177
    # (i.e. $::userid contains the user's positive integer ID).
    if ($::userid) {
178
        ThrowUserError("bug_access_denied", {'bug_id' => $id});
179
    } else {
180
        ThrowUserError("bug_access_query", {'bug_id' => $id});
181
    }
182 183
}

184 185 186 187 188 189
sub ValidateComment {
    # Make sure a comment is not too large (greater than 64K).
    
    my ($comment) = @_;
    
    if (defined($comment) && length($comment) > 65535) {
190
        ThrowUserError("comment_too_long");
191 192 193
    }
}

194 195 196
sub PasswordForLogin {
    my ($login) = (@_);
    SendSQL("select cryptpassword from profiles where login_name = " .
197
            SqlQuote($login));
198 199 200 201 202
    my $result = FetchOneColumn();
    if (!defined $result) {
        $result = "";
    }
    return $result;
203 204
}

205 206 207 208
sub get_netaddr {
    my ($ipaddr) = @_;

    # Check for a valid IPv4 addr which we know how to parse
209
    if (!$ipaddr || $ipaddr !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
210 211 212 213 214 215 216 217 218 219 220 221
        return undef;
    }

    my $addr = unpack("N", pack("CCCC", split(/\./, $ipaddr)));

    my $maskbits = Param('loginnetmask');

    $addr >>= (32-$maskbits);
    $addr <<= (32-$maskbits);
    return join(".", unpack("CCCC", pack("N", $addr)));
}

222 223 224 225 226 227 228 229
my $login_cookie_set = 0;
# If quietly_check_login is called with no arguments and logins are
# required, it will prompt for a login.
sub quietly_check_login {
    if (Param('requirelogin') && !(@_)) {
        confirm_login();
        return;
    }
230
    $::disabledreason = '';
231
    my $userid = 0;
232 233
    my $ipaddr = $ENV{'REMOTE_ADDR'};
    my $netaddr = get_netaddr($ipaddr);
234
    if (defined $::COOKIE{"Bugzilla_login"} &&
235
        defined $::COOKIE{"Bugzilla_logincookie"}) {
236
        my $query = "SELECT profiles.userid," .
237 238
                " profiles.login_name, " .
                " profiles.disabledtext " .
239 240
                " FROM profiles, logincookies WHERE logincookies.cookie = " .
                SqlQuote($::COOKIE{"Bugzilla_logincookie"}) .
241 242 243
                " AND profiles.userid = logincookies.userid AND" .
                " profiles.login_name = " .
                SqlQuote($::COOKIE{"Bugzilla_login"}) .
244 245 246 247 248 249 250 251
                " AND (logincookies.ipaddr = " .
                SqlQuote($ipaddr);
        if (defined $netaddr) {
            $query .= " OR logincookies.ipaddr = " . SqlQuote($netaddr);
        }
        $query .= ")";
        SendSQL($query);

252
        my @row;
253 254 255
        if (MoreSQLData()) {
            ($userid, my $loginname, my $disabledtext) = FetchSQLData();
            if ($userid > 0) {
256 257 258 259
                if ($disabledtext eq '') {
                    $::COOKIE{"Bugzilla_login"} = $loginname; # Makes sure case
                                                              # is in
                                                              # canonical form.
260 261
                    # We've just verified that this is ok
                    detaint_natural($::COOKIE{"Bugzilla_logincookie"});
262 263
                } else {
                    $::disabledreason = $disabledtext;
264
                    $userid = 0;
265
                }
266 267 268
            }
        }
    }
269 270 271 272 273
    # 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;
    }
274
    if (!$userid) {
275 276
        delete $::COOKIE{"Bugzilla_login"};
    }
277
                    
278 279
    $::userid = $userid;
    ConfirmGroup($userid);
280
    $vars->{'user'} = GetUserInfo($::userid);
281
    return $userid;
282 283
}

284 285 286 287 288 289
# Populate a hash with information about this user. 
sub GetUserInfo {
    my ($userid) = (@_);
    my %user;
    my @queries;
    my %groups;
290
    my @groupids;
291 292 293 294 295 296 297
    
    # No info if not logged in
    return \%user if ($userid == 0);
    
    $user{'login'} = $::COOKIE{"Bugzilla_login"};
    $user{'userid'} = $userid;
    
298
    SendSQL("SELECT mybugslink, realname " . 
299
            "FROM profiles WHERE userid = $userid");
300
    ($user{'showmybugslink'}, $user{'realname'}) = FetchSQLData();
301 302 303 304 305 306 307 308 309 310 311 312

    SendSQL("SELECT name, query, linkinfooter FROM namedqueries " .
            "WHERE userid = $userid");
    while (MoreSQLData()) {
        my %query;
        ($query{'name'}, $query{'query'}, $query{'linkinfooter'}) = 
                                                                 FetchSQLData();
        push(@queries, \%query);    
    }

    $user{'queries'} = \@queries;

313 314
    $user{'canblessany'} = UserCanBlessAnything();

315
    SendSQL("SELECT DISTINCT id, name FROM groups, user_group_map " .
316 317 318
            "WHERE groups.id = user_group_map.group_id " .
            "AND user_id = $userid " .
            "AND NOT isbless");
319
    while (MoreSQLData()) {
320 321
        my ($id, $name) = FetchSQLData();    
        push(@groupids,$id);
322
        $groups{$name} = 1;
323 324 325
    }

    $user{'groups'} = \%groups;
326
    $user{'groupids'} = \@groupids;
327 328 329 330

    return \%user;
}

331 332
sub CheckEmailSyntax {
    my ($addr) = (@_);
333
    my $match = Param('emailregexp');
334
    if ($addr !~ /$match/ || $addr =~ /[\\\(\)<>&,;:"\[\] \t\r\n]/) {
335 336
        $vars->{'addr'} = $addr;
        ThrowUserError("illegal_email_address");
337 338 339
    }
}

340 341 342
sub MailPassword {
    my ($login, $password) = (@_);
    my $urlbase = Param("urlbase");
343 344 345 346 347
    my $template = Param("passwordmail");
    my $msg = PerformSubsts($template,
                            {"mailaddress" => $login . Param('emailsuffix'),
                             "login" => $login,
                             "password" => $password});
348

349
    open SENDMAIL, "|/usr/lib/sendmail -t -i";
350 351 352 353
    print SENDMAIL $msg;
    close SENDMAIL;
}

354 355 356 357 358 359
sub confirm_login {
    my ($nexturl) = (@_);

# Uncommenting the next line can help debugging...
#    print "Content-type: text/plain\n\n";

360 361 362 363 364
    # I'm going to reorganize some of this stuff a bit.  Since we're adding
    # a second possible validation method (LDAP), we need to move some of this
    # to a later section.  -Joe Robins, 8/3/00
    my $enteredlogin = "";
    my $realcryptpwd = "";
365
    my $userid;
366

367 368 369 370 371 372 373 374 375 376 377 378
    # If the form contains Bugzilla login and password fields, use Bugzilla's 
    # built-in authentication to authenticate the user (otherwise use LDAP below).
    if (defined $::FORM{"Bugzilla_login"} && defined $::FORM{"Bugzilla_password"}) {
        # Make sure the user's login name is a valid email address.
        $enteredlogin = $::FORM{"Bugzilla_login"};
        CheckEmailSyntax($enteredlogin);

        # Retrieve the user's ID and crypted password from the database.
        SendSQL("SELECT userid, cryptpassword FROM profiles 
                 WHERE login_name = " . SqlQuote($enteredlogin));
        ($userid, $realcryptpwd) = FetchSQLData();

379 380
        # Make sure the user exists or throw an error (but do not admit it was a username
        # error to make it harder for a cracker to find account names by brute force).
381
        $userid || ThrowUserError("invalid_username_or_password");
382

383 384 385
        # If this is a new user, generate a password, insert a record
        # into the database, and email their password to them.
        if ( defined $::FORM{"PleaseMailAPassword"} && !$userid ) {
386 387
            # Ensure the new login is valid
            if(!ValidateNewUser($enteredlogin)) {
388
                ThrowUserError("account_exists");
389 390
            }

391 392
            my $password = InsertNewUser($enteredlogin, "");
            MailPassword($enteredlogin, $password);
393 394 395 396 397 398
            
            $vars->{'login'} = $enteredlogin;
            
            print "Content-Type: text/html\n\n";
            $template->process("account/created.html.tmpl", $vars)
              || ThrowTemplateError($template->error());                 
399 400 401 402 403 404 405 406 407 408 409 410
        }

        # Otherwise, authenticate the user.
        else {
            # Get the salt from the user's crypted password.
            my $salt = $realcryptpwd;

            # Using the salt, crypt the password the user entered.
            my $enteredCryptedPassword = crypt( $::FORM{"Bugzilla_password"} , $salt );

            # Make sure the passwords match or throw an error.
            ($enteredCryptedPassword eq $realcryptpwd)
411
              || ThrowUserError("invalid_username_or_password");
412 413 414 415 416 417 418 419 420 421

            # If the user has successfully logged in, delete any password tokens
            # lying around in the system for them.
            use Token;
            my $token = Token::HasPasswordToken($userid);
            while ( $token ) {
                Token::Cancel($token, "user logged in");
                $token = Token::HasPasswordToken($userid);
            }
        }
422 423 424 425 426 427

     } elsif (Param("useLDAP") &&
              defined $::FORM{"LDAP_login"} &&
              defined $::FORM{"LDAP_password"}) {
       # If we're using LDAP for login, we've got an entirely different
       # set of things to check.
428 429

# see comment at top of file near eval
430 431
       # First, if we don't have the LDAP modules available to us, we can't
       # do this.
432 433 434 435 436 437 438 439 440
#       if(!$have_ldap) {
#         print "Content-type: text/html\n\n";
#         PutHeader("LDAP not enabled");
#         print "The necessary modules for LDAP login are not installed on ";
#         print "this machine.  Please send mail to ".Param("maintainer");
#         print " and notify him of this problem.\n";
#         PutFooter();
#         exit;
#       }
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474

       # Next, we need to bind anonymously to the LDAP server.  This is
       # because we need to get the Distinguished Name of the user trying
       # to log in.  Some servers (such as iPlanet) allow you to have unique
       # uids spread out over a subtree of an area (such as "People"), so
       # just appending the Base DN to the uid isn't sufficient to get the
       # user's DN.  For servers which don't work this way, there will still
       # be no harm done.
       my $LDAPserver = Param("LDAPserver");
       if ($LDAPserver eq "") {
         print "Content-type: text/html\n\n";
         PutHeader("LDAP server not defined");
         print "The LDAP server for authentication has not been defined.  ";
         print "Please contact ".Param("maintainer")." ";
         print "and notify him of this problem.\n";
         PutFooter();
         exit;
       }

       my $LDAPport = "389";  #default LDAP port
       if($LDAPserver =~ /:/) {
         ($LDAPserver, $LDAPport) = split(":",$LDAPserver);
       }
       my $LDAPconn = new Mozilla::LDAP::Conn($LDAPserver,$LDAPport);
       if(!$LDAPconn) {
         print "Content-type: text/html\n\n";
         PutHeader("Unable to connect to LDAP server");
         print "I was unable to connect to the LDAP server for user ";
         print "authentication.  Please contact ".Param("maintainer");
         print " and notify him of this problem.\n";
         PutFooter();
         exit;
       }

475 476 477 478 479 480 481 482 483 484 485 486 487 488 489
       # if no password was provided, then fail the authentication
       # while it may be valid to not have an LDAP password, when you
       # bind without a password (regardless of the binddn value), you
       # will get an anonymous bind.  I do not know of a way to determine
       # whether a bind is anonymous or not without making changes to the
       # LDAP access control settings
       if ( ! $::FORM{"LDAP_password"} ) {
         print "Content-type: text/html\n\n";
         PutHeader("Login Failed");
         print "You did not provide a password.\n";
         print "Please click <b>Back</b> and try again.\n";
         PutFooter();
         exit;
       }

490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555
       # We've got our anonymous bind;  let's look up this user.
       my $dnEntry = $LDAPconn->search(Param("LDAPBaseDN"),"subtree","uid=".$::FORM{"LDAP_login"});
       if(!$dnEntry) {
         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;
       }

       # Now we get the DN from this search.  Once we've got that, we're
       # done with the anonymous bind, so we close it.
       my $userDN = $dnEntry->getDN;
       $LDAPconn->close;

       # Now we attempt to bind as the specified user.
       $LDAPconn = new Mozilla::LDAP::Conn($LDAPserver,$LDAPport,$userDN,$::FORM{"LDAP_password"});
       if(!$LDAPconn) {
         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;
       }

       # And now we're going to repeat the search, so that we can get the
       # mail attribute for this user.
       my $userEntry = $LDAPconn->search(Param("LDAPBaseDN"),"subtree","uid=".$::FORM{"LDAP_login"});
       if(!$userEntry->exists(Param("LDAPmailattribute"))) {
         print "Content-type: text/html\n\n";
         PutHeader("LDAP authentication error");
         print "I was unable to retrieve the ".Param("LDAPmailattribute");
         print " attribute from the LDAP server.  Please contact ";
         print Param("maintainer")." and notify him of this error.\n";
         PutFooter();
         exit;
       }

       # Mozilla::LDAP::Entry->getValues returns an array for the attribute
       # requested, even if there's only one entry.
       $enteredlogin = ($userEntry->getValues(Param("LDAPmailattribute")))[0];

       # We're going to need the cryptpwd for this user from the database
       # so that we can set the cookie below, even though we're not going
       # to use it for authentication.
       $realcryptpwd = PasswordForLogin($enteredlogin);

       # If we don't get a result, then we've got a user who isn't in
       # Bugzilla's database yet, so we've got to add them.
       if($realcryptpwd eq "") {
         # We'll want the user's name for this.
         my $userRealName = ($userEntry->getValues("displayName"))[0];
         if($userRealName eq "") {
           $userRealName = ($userEntry->getValues("cn"))[0];
         }
         InsertNewUser($enteredlogin, $userRealName);
         $realcryptpwd = PasswordForLogin($enteredlogin);
       }
     } # end LDAP authentication

     # And now, if we've logged in via either method, then we need to set
     # the cookies.
     if($enteredlogin ne "") {
       $::COOKIE{"Bugzilla_login"} = $enteredlogin;
556 557 558 559 560 561 562 563 564 565
       my $ipaddr = $ENV{'REMOTE_ADDR'};

       # Unless we're restricting the login, or restricting would have no
       # effect, loosen the IP which we record in the table
       unless ($::FORM{'Bugzilla_restrictlogin'} ||
               Param('loginnetmask') == 32) {
           $ipaddr = get_netaddr($ipaddr);
           $ipaddr = $ENV{'REMOTE_ADDR'} unless defined $ipaddr;
       }
       SendSQL("insert into logincookies (userid,ipaddr) values (@{[DBNameToIdAndCheck($enteredlogin)]}, @{[SqlQuote($ipaddr)]})");
566 567 568 569
       SendSQL("select LAST_INSERT_ID()");
       my $logincookie = FetchOneColumn();

       $::COOKIE{"Bugzilla_logincookie"} = $logincookie;
570
       my $cookiepath = Param("cookiepath");
571 572 573 574 575
       if ($login_cookie_set == 0) {
           $login_cookie_set = 1;
           print "Set-Cookie: Bugzilla_login= " . url_quote($enteredlogin) . " ; path=$cookiepath; expires=Sun, 30-Jun-2029 00:00:00 GMT\n";
           print "Set-Cookie: Bugzilla_logincookie=$logincookie ; path=$cookiepath; expires=Sun, 30-Jun-2029 00:00:00 GMT\n";
       }
576 577
    }

578 579 580 581 582 583
    # If anonymous logins are disabled, quietly_check_login will force
    # the user to log in by calling confirm_login() when called by any 
    # code that does not call it with an argument. When confirm_login
    # calls quietly_check_login, it must not result in confirm_login
    # being called back.
    $userid = quietly_check_login('do_not_recurse_here');
584

585
    if (!$userid) {
586
        if ($::disabledreason) {
587 588 589
            my $cookiepath = Param("cookiepath");
            print "Set-Cookie: Bugzilla_login= ; path=$cookiepath; expires=Sun, 30-Jun-80 00:00:00 GMT
Set-Cookie: Bugzilla_logincookie= ; path=$cookiepath; expires=Sun, 30-Jun-80 00:00:00 GMT
590 591 592
Content-type: text/html

";
593 594
            $vars->{'disabled_reason'} = $::disabledreason;
            ThrowUserError("account_disabled");
595
        }
596
        
597
        if (!defined $nexturl || $nexturl eq "") {
598 599
            # Sets nexturl to be argv0, stripping everything up to and
            # including the last slash (or backslash on Windows).
600 601
            $0 =~ m:([^/\\]*)$:;
            $nexturl = $1;
602
        }
603 604 605
        
        $vars->{'target'} = $nexturl;
        $vars->{'form'} = \%::FORM;
606
        $vars->{'mform'} = \%::MFORM;
607 608 609 610 611
        
        print "Content-type: text/html\n\n";
        $template->process("account/login.html.tmpl", $vars)
          || ThrowTemplateError($template->error());
                
612 613 614
        # 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.
615 616 617 618
        if ($::dbwritesallowed) {
            SendSQL("DELETE FROM logincookies " .
                    "WHERE TO_DAYS(NOW()) - TO_DAYS(lastused) > 30");
        }
619 620 621 622 623

        exit;
    }

    # Update the timestamp on our logincookie, so it'll keep on working.
624 625 626 627
    if ($::dbwritesallowed) {
        SendSQL("UPDATE logincookies SET lastused = null " .
                "WHERE cookie = $::COOKIE{'Bugzilla_logincookie'}");
    }
628 629
    ConfirmGroup($userid);
    return $userid;
630 631 632
}

sub PutHeader {
633
    ($vars->{'title'}, $vars->{'h1'}, $vars->{'h2'}) = (@_);
634 635 636
     
    $::template->process("global/header.html.tmpl", $::vars)
      || ThrowTemplateError($::template->error());
637
    $vars->{'header_done'} = 1;
638 639
}

640
sub PutFooter {
641 642
    $::template->process("global/footer.html.tmpl", $::vars)
      || ThrowTemplateError($::template->error());
643 644
}

645 646 647 648 649
###############################################################################
# Error handling
#
# If you are doing incremental output, set $vars->{'header_done'} once you've
# done the header.
650 651 652 653
#
# You can call Throw*Error with extra template variables in one pass by using
# the $extra_vars hash reference parameter:
# ThrowUserError("some_tag", { bug_id => $bug_id, size => 127 });
654 655 656
###############################################################################

# For "this shouldn't happen"-type places in the code.
657 658
# The contents of $extra_vars get printed out in the template - useful for
# debugging info.
659
sub ThrowCodeError {
660
  ($vars->{'error'}, my $extra_vars, my $unlock_tables) = (@_);
661

662 663
  SendSQL("UNLOCK TABLES") if $unlock_tables;
  
664
  # Copy the extra_vars into the vars hash 
665 666 667 668
  foreach my $var (keys %$extra_vars) {
      $vars->{$var} = $extra_vars->{$var};
  }
  
669 670
  # We may one day log something to file here also.
  $vars->{'variables'} = $extra_vars;
671
  
672 673
  print "Content-type: text/html\n\n" if !$vars->{'header_done'};
  $template->process("global/code-error.html.tmpl", $vars)
674
    || ThrowTemplateError($template->error());
675 676 677
    
  exit;
}
678

679
# For errors made by the user.
680
sub ThrowUserError {
681
  ($vars->{'error'}, my $extra_vars, my $unlock_tables) = (@_);
682

683
  SendSQL("UNLOCK TABLES") if $unlock_tables;
684 685
 
  # Copy the extra_vars into the vars hash 
686 687 688 689
  foreach my $var (keys %$extra_vars) {
      $vars->{$var} = $extra_vars->{$var};
  }
  
690 691
  print "Content-type: text/html\n\n" if !$vars->{'header_done'};
  $template->process("global/user-error.html.tmpl", $vars)
692
    || ThrowTemplateError($template->error());
693 694 695
    
  exit;
}
696

697 698 699 700
# This function should only be called if a template->process() fails.
# It tries another template first, because often one template being
# broken or missing doesn't mean that they all are. But it falls back on
# a print statement.
701
# The Content-Type will already have been printed.
702
sub ThrowTemplateError {
703 704
    ($vars->{'template_error_msg'}) = (@_);
    $vars->{'error'} = "template_error";
705
    
706 707 708 709
    # Try a template first; but if this one fails too, fall back
    # on plain old print statements.
    if (!$template->process("global/code-error.html.tmpl", $vars)) {
        my $maintainer = Param('maintainer');
710
        my $error = html_quote($vars->{'template_error_msg'});
711 712 713 714 715 716 717 718
        my $error2 = html_quote($template->error());
        print <<END;
        <tt>
          <p>
            Bugzilla has suffered an internal error. Please save this page and 
            send it to $maintainer with details of what you were doing at the 
            time this message appeared.
          </p>
719
          <script type="text/javascript"> <!--
720 721 722 723 724 725 726
            document.write("<p>URL: " + document.location + "</p>");
          // -->
          </script>
          <p>Template->process() failed twice.<br>
          First error: $error<br>
          Second error: $error2</p>
        </tt>
727
END
728 729
    }
    
730
    exit;  
731 732
}

733 734 735 736 737
sub CheckIfVotedConfirmed {
    my ($id, $who) = (@_);
    SendSQL("SELECT bugs.votes, bugs.bug_status, products.votestoconfirm, " .
            "       bugs.everconfirmed " .
            "FROM bugs, products " .
738
            "WHERE bugs.bug_id = $id AND products.id = bugs.product_id");
739 740 741 742 743 744
    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 " .
745
                "(bug_id,who,bug_when,fieldid,removed,added) VALUES " .
746 747 748 749
                "($id,$who,now(),$fieldid,'$::unconfirmedstate','NEW')");
        if (!$everconfirmed) {
            $fieldid = GetFieldID("everconfirmed");
            SendSQL("INSERT INTO bugs_activity " .
750
                    "(bug_id,who,bug_when,fieldid,removed,added) VALUES " .
751 752
                    "($id,$who,now(),$fieldid,'0','1')");
        }
753
        
754
        AppendComment($id, DBID_to_name($who),
755
                      "*** This bug has been confirmed by popular vote. ***", 0);
756 757 758 759 760 761 762 763 764 765
                      
        $vars->{'type'} = "votes";
        $vars->{'id'} = $id;
        $vars->{'mail'} = "";
        open(PMAIL, "-|") or exec('./processmail', $id);
        $vars->{'mail'} .= $_ while <PMAIL>;
        close(PMAIL);
        
        $template->process("bug/process/results.html.tmpl", $vars)
          || ThrowTemplateError($template->error());
766 767 768
    }

}
769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799
sub LogActivityEntry {
    my ($i,$col,$removed,$added,$whoid,$timestamp) = @_;
    # in the case of CCs, deps, and keywords, there's a possibility that someone    # might try to add or remove a lot of them at once, which might take more
    # space than the activity table allows.  We'll solve this by splitting it
    # into multiple entries if it's too long.
    while ($removed || $added) {
        my ($removestr, $addstr) = ($removed, $added);
        if (length($removestr) > 254) {
            my $commaposition = FindWrapPoint($removed, 254);
            $removestr = substr($removed,0,$commaposition);
            $removed = substr($removed,$commaposition);
            $removed =~ s/^[,\s]+//; # remove any comma or space
        } else {
            $removed = ""; # no more entries
        }
        if (length($addstr) > 254) {
            my $commaposition = FindWrapPoint($added, 254);
            $addstr = substr($added,0,$commaposition);
            $added = substr($added,$commaposition);
            $added =~ s/^[,\s]+//; # remove any comma or space
        } else {
            $added = ""; # no more entries
        }
        $addstr = SqlQuote($addstr);
        $removestr = SqlQuote($removestr);
        my $fieldid = GetFieldID($col);
        SendSQL("INSERT INTO bugs_activity " .
                "(bug_id,who,bug_when,fieldid,removed,added) VALUES " .
                "($i,$whoid," . SqlQuote($timestamp) . ",$fieldid,$removestr,$addstr)");
    }
}
800

801
sub GetBugActivity {
802 803
    my ($id, $starttime) = (@_);
    my $datepart = "";
804 805 806

    die "Invalid id: $id" unless $id=~/^\s*\d+\s*$/;

807
    if (defined $starttime) {
808
        $datepart = "and bugs_activity.bug_when > " . SqlQuote($starttime);
809
    }
810
    
811
    my $query = "
812
        SELECT IFNULL(fielddefs.description, bugs_activity.fieldid),
813
                fielddefs.name,
814
                bugs_activity.attach_id,
815
                DATE_FORMAT(bugs_activity.bug_when,'%Y.%m.%d %H:%i'),
816
                bugs_activity.removed, bugs_activity.added,
817
                profiles.login_name
818 819 820
        FROM bugs_activity LEFT JOIN fielddefs ON 
                                     bugs_activity.fieldid = fielddefs.fieldid,
             profiles
821 822 823
        WHERE bugs_activity.bug_id = $id $datepart
              AND profiles.userid = bugs_activity.who
        ORDER BY bugs_activity.bug_when";
824 825 826

    SendSQL($query);
    
827 828 829
    my @operations;
    my $operation = {};
    my $changes = [];
830
    my $incomplete_data = 0;
831
    
832
    while (my ($field, $fieldname, $attachid, $when, $removed, $added, $who) 
833 834 835
                                                               = FetchSQLData())
    {
        my %change;
836
        my $activity_visible = 1;
837
        
838 839 840 841
        # check if the user should see this field's activity
        if ($fieldname eq 'remaining_time' ||
            $fieldname eq 'estimated_time' ||
            $fieldname eq 'work_time') {
842

843 844 845 846 847 848 849 850 851 852 853 854 855 856 857
            if (!UserInGroup(Param('timetrackinggroup'))) {
                $activity_visible = 0;
            } else {
                $activity_visible = 1;
            }
        } else {
            $activity_visible = 1;
        }
                
        if ($activity_visible) {
            # This gets replaced with a hyperlink in the template.
            $field =~ s/^Attachment// if $attachid;

            # Check for the results of an old Bugzilla data corruption bug
            $incomplete_data = 1 if ($added =~ /^\?/ || $removed =~ /^\?/);
858
        
859 860 861 862 863 864 865 866 867 868
            # An operation, done by 'who' at time 'when', has a number of
            # 'changes' associated with it.
            # If this is the start of a new operation, store the data from the
            # previous one, and set up the new one.
            if ($operation->{'who'} 
                && ($who ne $operation->{'who'} 
                    || $when ne $operation->{'when'})) 
            {
                $operation->{'changes'} = $changes;
                push (@operations, $operation);
869
            
870 871 872 873
                # Create new empty anonymous data structures.
                $operation = {};
                $changes = [];
            }  
874
        
875 876
            $operation->{'who'} = $who;
            $operation->{'when'} = $when;            
877
        
878 879 880 881 882 883 884
            $change{'field'} = $field;
            $change{'fieldname'} = $fieldname;
            $change{'attachid'} = $attachid;
            $change{'removed'} = $removed;
            $change{'added'} = $added;
            push (@$changes, \%change);
        }   
885
    }
886 887 888 889
    
    if ($operation->{'who'}) {
        $operation->{'changes'} = $changes;
        push (@operations, $operation);
890
    }
891 892
    
    return(\@operations, $incomplete_data);
893 894
}

895 896
############# Live code below here (that is, not subroutine defs) #############

897
use Bugzilla::CGI();
898

899 900 901 902
# XXX - mod_perl, this needs to move into all the scripts individually
# Once we do that, look into setting DISABLE_UPLOADS, and overriding
# on a per-script basis
$::cgi = new Bugzilla::CGI();
903

904 905 906
# Set up stuff for compatibility with the old CGI.pl code
# This code will be removed as soon as possible, in favour of
# using the CGI.pm stuff directly
907

908 909 910 911 912 913
# XXX - mod_perl - reset these between runs

foreach my $name ($::cgi->param()) {
    my @val = $::cgi->param($name);
    $::FORM{$name} = join('', @val);
    $::MFORM{$name} = \@val;
914 915
}

916 917 918 919
$::buffer = $::cgi->query_string();

foreach my $name ($::cgi->cookie()) {
    $::COOKIE{$name} = $::cgi->cookie($name);
920 921
}

922 923 924
# This could be needed in any CGI, so we set it here.
$vars->{'help'} = $::cgi->param('help') ? 1 : 0;

925
1;