# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Error;
use 5.10.1;
use strict;
use warnings;
use parent qw(Exporter);
@Bugzilla::Error::EXPORT = qw(ThrowCodeError ThrowTemplateError ThrowUserError);
use Bugzilla::Constants;
use Bugzilla::WebService::Constants;
use Bugzilla::Hook;
use Carp;
use Data::Dumper;
use Date::Format;
# We cannot use $^S to detect if we are in an eval(), because mod_perl
# already eval'uates everything, so $^S = 1 in all cases under mod_perl!
sub _in_eval {
my $in_eval = 0;
for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) {
last if $sub =~ /^(?:ModPerl|Plack|CGI::Compile)/;
# An eval followed by CGI::Compile is not a "real" eval.
$in_eval = 1 if $sub =~ /^\(eval\)/ && (caller($stack + 1))[3] !~ /^CGI::Compile/;
}
return $in_eval;
}
sub _throw_error {
my ($name, $error, $vars) = @_;
my $dbh = Bugzilla->dbh;
$vars ||= {};
$vars->{error} = $error;
# Make sure any transaction is rolled back (if supported).
# If we are within an eval(), do not roll back transactions as we are
# eval'uating some test on purpose.
$dbh->bz_rollback_transaction() if ($dbh->bz_in_transaction() && !_in_eval());
my $datadir = bz_locations()->{'datadir'};
# If a writable $datadir/errorlog exists, log error details there.
if (-w "$datadir/errorlog") {
require Bugzilla::Util;
require Data::Dumper;
my $mesg = "";
for (1..75) { $mesg .= "-"; };
$mesg .= "\n[$$] " . time2str("%D %H:%M:%S ", time());
$mesg .= "$name $error ";
$mesg .= Bugzilla::Util::remote_ip();
$mesg .= Bugzilla->user->login;
$mesg .= (' actually ' . Bugzilla->sudoer->login) if Bugzilla->sudoer;
$mesg .= "\n";
my %params = Bugzilla->cgi->Vars;
$Data::Dumper::Useqq = 1;
for my $param (sort keys %params) {
my $val = $params{$param};
# obscure passwords
$val = "*****" if $param =~ /password/i;
# limit line length
$val =~ s/^(.{512}).*$/$1\[CHOP\]/;
$mesg .= "[$$] " . Data::Dumper->Dump([$val],["param($param)"]);
}
for my $var (sort keys %ENV) {
my $val = $ENV{$var};
$val = "*****" if $val =~ /password|http_pass/i;
$mesg .= "[$$] " . Data::Dumper->Dump([$val],["env($var)"]);
}
open(ERRORLOGFID, ">>", "$datadir/errorlog");
print ERRORLOGFID "$mesg\n";
close ERRORLOGFID;
}
my $template = Bugzilla->template;
my $message;
# There are some tests that throw and catch a lot of errors,
# and calling $template->process over and over for those errors
# is too slow. So instead, we just "die" with a dump of the arguments.
if (Bugzilla->error_mode != ERROR_MODE_TEST) {
$template->process($name, $vars, \$message)
|| ThrowTemplateError($template->error());
}
# Let's call the hook first, so that extensions can override
# or extend the default behavior, or add their own error codes.
Bugzilla::Hook::process('error_catch', { error => $error, vars => $vars,
message => \$message });
if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) {
my $cgi = Bugzilla->cgi;
$cgi->close_standby_message('text/html', 'inline', 'error', 'html');
print $message;
print $cgi->multipart_final() if $cgi->{_multipart_in_progress};
}
elsif (Bugzilla->error_mode == ERROR_MODE_TEST) {
die Dumper($vars);
}
elsif (Bugzilla->error_mode == ERROR_MODE_DIE) {
die("$message\n");
}
elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT
|| Bugzilla->error_mode == ERROR_MODE_JSON_RPC
|| Bugzilla->error_mode == ERROR_MODE_REST)
{
# Clone the hash so we aren't modifying the constant.
my %error_map = %{ WS_ERROR_CODE() };
Bugzilla::Hook::process('webservice_error_codes',
{ error_map => \%error_map });
my $code = $error_map{$error};
if (!$code) {
$code = ERROR_UNKNOWN_FATAL if $name =~ /code/i;
$code = ERROR_UNKNOWN_TRANSIENT if $name =~ /user/i;
}
if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
die SOAP::Fault->faultcode($code)->faultstring($message);
}
elsif (Bugzilla->error_mode == ERROR_MODE_JSON_RPC) {
my $server = Bugzilla->_json_server;
# Technically JSON-RPC isn't allowed to have error numbers
# higher than 999, but we do this to avoid conflicts with
# the internal JSON::RPC error codes.
$server->raise_error(code => 100000 + $code,
message => $message,
id => $server->{_bz_request_id},
version => $server->version);
# Most JSON-RPC Throw*Error calls happen within an eval inside
# of JSON::RPC. So, in that circumstance, instead of exiting,
# we die with no message. JSON::RPC checks raise_error before
# it checks $@, so it returns the proper error.
die if _in_eval();
$server->response($server->error_response_header);
}
else {
my $server = Bugzilla->api_server;
my %status_code_map = %{ $server->constants->{REST_STATUS_CODE_MAP} };
my $status_code = $status_code_map{$code} || $status_code_map{'_default'};
$server->return_error($status_code, $message, $code);
$server->response;
}
}
exit;
}
sub ThrowUserError {
_throw_error("global/user-error.html.tmpl", @_);
}
sub ThrowCodeError {
my (undef, $vars) = @_;
# Don't show function arguments, in case they contain
# confidential data.
local $Carp::MaxArgNums = -1;
# Don't show the error as coming from Bugzilla::Error, show it
# as coming from the caller.
local $Carp::CarpInternal{'Bugzilla::Error'} = 1;
$vars->{traceback} = Carp::longmess();
_throw_error("global/code-error.html.tmpl", @_);
}
sub ThrowTemplateError {
my ($template_err) = @_;
my $dbh = Bugzilla->dbh;
# Make sure the transaction is rolled back (if supported).
$dbh->bz_rollback_transaction() if $dbh->bz_in_transaction();
my $vars = {};
if (Bugzilla->error_mode == ERROR_MODE_DIE) {
die("error: template error: $template_err");
}
$vars->{'template_error_msg'} = $template_err;
$vars->{'error'} = "template_error";
my $template = Bugzilla->template;
# 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)) {
require Bugzilla::Util;
import Bugzilla::Util qw(html_quote);
my $maintainer = Bugzilla->params->{'maintainer'};
my $error = html_quote($vars->{'template_error_msg'});
my $error2 = html_quote($template->error());
my $url = html_quote(Bugzilla->cgi->self_url);
print <
URL: $url
Template->process() failed twice.
First error: $error
Second error: $error2