# 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; use 5.10.1; use strict; use warnings; # We want any compile errors to get to the browser, if possible. BEGIN { # This makes sure we're in a CGI. mod_perl doesn't support Carp # and Plack reports errors elsewhere. # We cannot call i_am_persistent() from here as its module is # not loaded yet. if ($ENV{SERVER_SOFTWARE} && !($ENV{MOD_PERL} || $ENV{BZ_PLACK})) { require CGI::Carp; CGI::Carp->import('fatalsToBrowser'); } } use Bugzilla::Auth; use Bugzilla::Auth::Persist::Cookie; use Bugzilla::CGI; use Bugzilla::Config; use Bugzilla::Constants; use Bugzilla::DB; use Bugzilla::Error; use Bugzilla::Extension; use Bugzilla::Field; use Bugzilla::Flag; use Bugzilla::Install::Localconfig qw(read_localconfig); use Bugzilla::Install::Requirements qw(OPTIONAL_MODULES have_vers); use Bugzilla::Install::Util qw(init_console include_languages i_am_persistent); use Bugzilla::Memcached; use Bugzilla::Template; use Bugzilla::Token; use Bugzilla::User; use Bugzilla::Util; use File::Basename; use File::Spec::Functions; use DateTime::TimeZone; use Date::Parse; use Safe; ##################################################################### # Constants ##################################################################### # Scripts that are not stopped by shutdownhtml being in effect. use constant SHUTDOWNHTML_EXEMPT => qw( editparams.cgi checksetup.pl migrate.pl recode.pl ); # Non-cgi scripts that should silently exit. use constant SHUTDOWNHTML_EXIT_SILENTLY => qw( whine.pl ); # shutdownhtml pages are sent as an HTTP 503. After how many seconds # should search engines attempt to index the page again? use constant SHUTDOWNHTML_RETRY_AFTER => 3600; ##################################################################### # Global Code ##################################################################### #$::SIG{__DIE__} = i_am_cgi() ? \&CGI::Carp::confess : \&Carp::confess; # Note that this is a raw subroutine, not a method, so $class isn't available. sub init_page { if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { init_console(); } else { binmode STDOUT, ':utf8'; } if (${^TAINT}) { my $path = ''; if (ON_WINDOWS) { # On Windows, these paths are tainted, preventing # File::Spec::Win32->tmpdir from using them. But we need # a place to temporary store attachments which are uploaded. foreach my $temp (qw(TMPDIR TMP TEMP)) { trick_taint($ENV{$temp}) if $ENV{$temp}; } # Some DLLs used by Strawberry Perl are also in c\bin, # see https://rt.cpan.org/Public/Bug/Display.html?id=99104 if (!ON_ACTIVESTATE) { my $c_path = $path = dirname($^X); $c_path =~ s/\bperl\b(?=\\bin)/c/; $path .= ";$c_path"; } } # Some environment variables are not taint safe delete @::ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; # Some modules throw undefined errors (notably File::Spec::Win32) if # PATH is undefined. $ENV{'PATH'} = $path; } # Because this function is run live from perl "use" commands of # other scripts, we're skipping the rest of this function if we get here # during a perl syntax check (perl -c, like we do during the # 001compile.t test). return if $^C; # IIS prints out warnings to the webpage, so ignore them, or log them # to a file if the file exists. if ($ENV{SERVER_SOFTWARE} && $ENV{SERVER_SOFTWARE} =~ /microsoft-iis/i) { $SIG{__WARN__} = sub { my ($msg) = @_; my $datadir = bz_locations()->{'datadir'}; if (-w "$datadir/errorlog") { my $warning_log = new IO::File(">>$datadir/errorlog"); print $warning_log $msg; $warning_log->close(); } }; } my $script = basename($0); # Because of attachment_base, attachment.cgi handles this itself. if ($script ne 'attachment.cgi') { do_ssl_redirect_if_required(); } # If Bugzilla is shut down, do not allow anything to run, just display a # message to the user about the downtime and log out. Scripts listed in # SHUTDOWNHTML_EXEMPT are exempt from this message. # # This code must go here. It cannot go anywhere in Bugzilla::CGI, because # it uses Template, and that causes various dependency loops. if (!grep { $_ eq $script } SHUTDOWNHTML_EXEMPT and Bugzilla->params->{'shutdownhtml'}) { # Allow non-cgi scripts to exit silently (without displaying any # message), if desired. At this point, no DBI call has been made # yet, and no error will be returned if the DB is inaccessible. if (!i_am_cgi() && grep { $_ eq $script } SHUTDOWNHTML_EXIT_SILENTLY) { exit; } # Plack requires to exit differently. return -1 if $ENV{BZ_PLACK}; _shutdown(); } } sub _shutdown { # For security reasons, log out users when Bugzilla is down. # Bugzilla->login() is required to catch the logincookie, if any. my $user = eval { Bugzilla->login(LOGIN_OPTIONAL); }; if ($@) { # The DB is not accessible. Use the default user object. $user = Bugzilla->user; $user->{settings} = {}; } my $userid = $user->id; Bugzilla->logout(); # Generate and return a message about the downtime, appropriately # for if we're a command-line script or a CGI script. my $cgi = Bugzilla->cgi; my $extension = 'txt'; if (i_am_cgi()) { # Set the HTTP status to 503 when Bugzilla is down to avoid pages # being indexed by search engines. print $cgi->header(-status => 503, -retry_after => SHUTDOWNHTML_RETRY_AFTER); if (!$cgi->param('ctype') || $cgi->param('ctype') eq 'html') { $extension = 'html'; } } my $template = Bugzilla->template; my $vars = { message => 'shutdown', userid => $userid }; $template->process("global/message.$extension.tmpl", $vars) or ThrowTemplateError($template->error); exit; } ##################################################################### # Subroutines and Methods ##################################################################### sub template { return $_[0]->request_cache->{template} ||= Bugzilla::Template->create(); } sub template_inner { my ($class, $lang) = @_; my $cache = $class->request_cache; my $current_lang = $cache->{template_current_lang}->[0]; $lang ||= $current_lang || ''; return $cache->{"template_inner_$lang"} ||= Bugzilla::Template->create(language => $lang); } our $extension_packages; sub extensions { my ($class) = @_; my $cache = $class->request_cache; if (!$cache->{extensions}) { # Under mod_perl, mod_perl.pl populates $extension_packages for us. if (!$extension_packages) { $extension_packages = Bugzilla::Extension->load_all(); } my @extensions; foreach my $package (@$extension_packages) { my $extension = $package->new(); if ($extension->enabled) { push(@extensions, $extension); } } $cache->{extensions} = \@extensions; } return $cache->{extensions}; } sub api_server { my $class = shift; my $cache = $class->request_cache; return $cache->{api_server} if defined $cache->{api_server}; require Bugzilla::API::Server; $cache->{api_server} = Bugzilla::API::Server->server; if (my $load_error = $cache->{api_server}->load_error) { my @error_params = ($load_error->{error}, $load_error->{vars}); ThrowCodeError(@error_params) if $load_error->{type} eq 'code'; ThrowUserError(@error_params) if $load_error->{type} eq 'user'; } return $cache->{api_server}; } sub feature { my ($class, $feature) = @_; my $cache = $class->request_cache; return $cache->{feature}->{$feature} if exists $cache->{feature}->{$feature}; my $feature_map = $cache->{feature_map}; if (!$feature_map) { foreach my $package (@{ OPTIONAL_MODULES() }) { foreach my $f (@{ $package->{feature} }) { $feature_map->{$f} ||= []; push(@{ $feature_map->{$f} }, $package); } } $cache->{feature_map} = $feature_map; } if (!$feature_map->{$feature}) { ThrowCodeError('invalid_feature', { feature => $feature }); } my $success = 1; foreach my $package (@{ $feature_map->{$feature} }) { have_vers($package) or $success = 0; } $cache->{feature}->{$feature} = $success; return $success; } sub cgi { return $_[0]->request_cache->{cgi} ||= new Bugzilla::CGI(); } sub input_params { my ($class, $params) = @_; my $cache = $class->request_cache; # This is how the WebService and other places set input_params. if (defined $params) { $cache->{input_params} = $params; } return $cache->{input_params} if defined $cache->{input_params}; # Making this scalar makes it a tied hash to the internals of $cgi, # so if a variable is changed, then it actually changes the $cgi object # as well. $cache->{input_params} = $class->cgi->Vars; return $cache->{input_params}; } sub localconfig { return $_[0]->process_cache->{localconfig} ||= read_localconfig(); } sub params { return $_[0]->request_cache->{params} ||= Bugzilla::Config::read_param_file(); } sub user { return $_[0]->request_cache->{user} ||= new Bugzilla::User; } sub set_user { my ($class, $user) = @_; $class->request_cache->{user} = $user; } sub sudoer { return $_[0]->request_cache->{sudoer}; } sub sudo_request { my ($class, $new_user, $new_sudoer) = @_; $class->request_cache->{user} = $new_user; $class->request_cache->{sudoer} = $new_sudoer; # NOTE: If you want to log the start of an sudo session, do it here. } sub page_requires_login { return $_[0]->request_cache->{page_requires_login}; } sub login { my ($class, $type) = @_; return $class->user if $class->user->id; my $authorizer = new Bugzilla::Auth(); $type = LOGIN_REQUIRED if $class->cgi->param('GoAheadAndLogIn'); if (!defined $type || $type == LOGIN_NORMAL) { $type = $class->params->{'requirelogin'} ? LOGIN_REQUIRED : LOGIN_NORMAL; } # Allow templates to know that we're in a page that always requires # login. if ($type == LOGIN_REQUIRED) { $class->request_cache->{page_requires_login} = 1; } my $authenticated_user = $authorizer->login($type); # At this point, we now know if a real person is logged in. # We must now check to see if an sudo session is in progress. # For a session to be in progress, the following must be true: # 1: There must be a logged in user # 2: That user must be in the 'bz_sudoer' group # 3: There must be a valid value in the 'sudo' cookie # 4: A Bugzilla::User object must exist for the given cookie value # 5: That user must NOT be in the 'bz_sudo_protect' group my $token = $class->cgi->cookie('sudo'); if (defined $authenticated_user && $token) { my ($user_id, $date, $sudo_target_id) = Bugzilla::Token::GetTokenData($token); if (!$user_id || $user_id != $authenticated_user->id || !detaint_natural($sudo_target_id) || (time() - str2time($date) > MAX_SUDO_TOKEN_AGE)) { $class->cgi->remove_cookie('sudo'); ThrowUserError('sudo_invalid_cookie'); } my $sudo_target = new Bugzilla::User($sudo_target_id); if ($authenticated_user->in_group('bz_sudoers') && defined $sudo_target && !$sudo_target->in_group('bz_sudo_protect')) { $class->set_user($sudo_target); $class->request_cache->{sudoer} = $authenticated_user; # And make sure that both users have the same Auth object, # since we never call Auth::login for the sudo target. $sudo_target->set_authorizer($authenticated_user->authorizer); # NOTE: If you want to do any special logging, do it here. } else { delete_token($token); $class->cgi->remove_cookie('sudo'); ThrowUserError('sudo_illegal_action', { sudoer => $authenticated_user, target_user => $sudo_target }); } } else { $class->set_user($authenticated_user); } if ($class->sudoer) { $class->sudoer->update_last_seen_date(); } else { $class->user->update_last_seen_date(); } return $class->user; } sub logout { my ($class, $option) = @_; # If we're not logged in, go away return unless $class->user->id; $option = LOGOUT_CURRENT unless defined $option; Bugzilla::Auth::Persist::Cookie->logout({type => $option}); $class->logout_request() unless $option eq LOGOUT_KEEP_CURRENT; } sub logout_user { my ($class, $user) = @_; # When we're logging out another user we leave cookies alone, and # therefore avoid calling Bugzilla->logout() directly. Bugzilla::Auth::Persist::Cookie->logout({user => $user}); } # just a compatibility front-end to logout_user that gets a user by id sub logout_user_by_id { my ($class, $id) = @_; my $user = new Bugzilla::User($id); $class->logout_user($user); } # hack that invalidates credentials for a single request sub logout_request { my $class = shift; delete $class->request_cache->{user}; delete $class->request_cache->{sudoer}; # We can't delete from $cgi->cookie, so logincookie data will remain # there. Don't rely on it: use Bugzilla->user->login instead! } sub markdown { return if !Bugzilla->feature('markdown'); require Bugzilla::Markdown; return $_[0]->request_cache->{markdown} ||= Bugzilla::Markdown->new(); } sub job_queue { require Bugzilla::JobQueue; return $_[0]->request_cache->{job_queue} ||= Bugzilla::JobQueue->new(); } sub dbh { # If we're not connected, then we must want the main db return $_[0]->request_cache->{dbh} ||= $_[0]->dbh_main; } sub dbh_main { return $_[0]->request_cache->{dbh_main} ||= Bugzilla::DB::connect_main(); } sub languages { return Bugzilla::Install::Util::supported_languages(); } sub current_language { return $_[0]->request_cache->{current_language} ||= (include_languages())[0]; } sub error_mode { my ($class, $newval) = @_; if (defined $newval) { $class->request_cache->{error_mode} = $newval; } # XXX - Once we require Perl 5.10.1, this test can be replaced by //. if (exists $class->request_cache->{error_mode}) { return $class->request_cache->{error_mode}; } else { return (i_am_cgi() ? ERROR_MODE_WEBPAGE : ERROR_MODE_DIE); } } # This is used only by Bugzilla::Error to throw errors. sub _json_server { my ($class, $newval) = @_; if (defined $newval) { $class->request_cache->{_json_server} = $newval; } return $class->request_cache->{_json_server}; } sub usage_mode { my ($class, $newval) = @_; if (defined $newval) { if ($newval == USAGE_MODE_BROWSER) { $class->error_mode(ERROR_MODE_WEBPAGE); } elsif ($newval == USAGE_MODE_CMDLINE) { $class->error_mode(ERROR_MODE_DIE); } elsif ($newval == USAGE_MODE_XMLRPC) { $class->error_mode(ERROR_MODE_DIE_SOAP_FAULT); } elsif ($newval == USAGE_MODE_JSON) { $class->error_mode(ERROR_MODE_JSON_RPC); } elsif ($newval == USAGE_MODE_EMAIL) { $class->error_mode(ERROR_MODE_DIE); } elsif ($newval == USAGE_MODE_TEST) { $class->error_mode(ERROR_MODE_TEST); } elsif ($newval == USAGE_MODE_REST) { $class->error_mode(ERROR_MODE_REST); } else { ThrowCodeError('usage_mode_invalid', {'invalid_usage_mode', $newval}); } $class->request_cache->{usage_mode} = $newval; } # XXX - Once we require Perl 5.10.1, this test can be replaced by //. if (exists $class->request_cache->{usage_mode}) { return $class->request_cache->{usage_mode}; } else { return (i_am_cgi()? USAGE_MODE_BROWSER : USAGE_MODE_CMDLINE); } } sub installation_mode { my ($class, $newval) = @_; ($class->request_cache->{installation_mode} = $newval) if defined $newval; return $class->request_cache->{installation_mode} || INSTALLATION_MODE_INTERACTIVE; } sub installation_answers { my ($class, $filename) = @_; if ($filename) { my $s = new Safe; $s->rdo($filename); die "Error reading $filename: $!" if $!; die "Error evaluating $filename: $@" if $@; # Now read the param back out from the sandbox $class->request_cache->{installation_answers} = $s->varglob('answer'); } return $class->request_cache->{installation_answers} || {}; } sub switch_to_shadow_db { my $class = shift; if (!$class->request_cache->{dbh_shadow}) { if ($class->params->{'shadowdb'}) { $class->request_cache->{dbh_shadow} = Bugzilla::DB::connect_shadow(); } else { $class->request_cache->{dbh_shadow} = $class->dbh_main; } } $class->request_cache->{dbh} = $class->request_cache->{dbh_shadow}; # we have to return $class->dbh instead of {dbh} as # {dbh_shadow} may be undefined if no shadow DB is used # and no connection to the main DB has been established yet. return $class->dbh; } sub switch_to_main_db { my $class = shift; $class->request_cache->{dbh} = $class->dbh_main; return $class->dbh_main; } sub is_shadow_db { my $class = shift; return $class->request_cache->{dbh} != $class->dbh_main; } sub fields { my ($class, $criteria) = @_; $criteria ||= {}; my $cache = $class->request_cache; # We create an advanced cache for fields by type, so that we # can avoid going back to the database for every fields() call. # (And most of our fields() calls are for getting fields by type.) # # We also cache fields by name, because calling $field->name a few # million times can be slow in calling code, but if we just do it # once here, that makes things a lot faster for callers. if (!defined $cache->{fields}) { my @all_fields = Bugzilla::Field->get_all; my (%by_name, %by_type); foreach my $field (@all_fields) { my $name = $field->name; $by_type{$field->type}->{$name} = $field; $by_name{$name} = $field; } $cache->{fields} = { by_type => \%by_type, by_name => \%by_name }; } my $fields = $cache->{fields}; my %requested; if (my $types = delete $criteria->{type}) { $types = ref($types) ? $types : [$types]; %requested = map { %{ $fields->{by_type}->{$_} || {} } } @$types; } else { %requested = %{ $fields->{by_name} }; } my $do_by_name = delete $criteria->{by_name}; # Filtering before returning the fields based on # the criterias. foreach my $filter (keys %$criteria) { foreach my $field (keys %requested) { if ($requested{$field}->$filter != $criteria->{$filter}) { delete $requested{$field}; } } } return $do_by_name ? \%requested : [sort { $a->sortkey <=> $b->sortkey || $a->name cmp $b->name } values %requested]; } sub active_custom_fields { my $class = shift; if (!exists $class->request_cache->{active_custom_fields}) { $class->request_cache->{active_custom_fields} = Bugzilla::Field->match({ custom => 1, obsolete => 0 }); } return @{$class->request_cache->{active_custom_fields}}; } sub has_flags { my $class = shift; if (!defined $class->request_cache->{has_flags}) { $class->request_cache->{has_flags} = Bugzilla::Flag->any_exist; } return $class->request_cache->{has_flags}; } sub local_timezone { return $_[0]->process_cache->{local_timezone} ||= DateTime::TimeZone->new(name => 'local'); } # This creates the request cache for non-mod_perl installations. # This is identical to Install::Util::_cache so that things loaded # into Install::Util::_cache during installation can be read out # of request_cache later in installation. our $_request_cache = $Bugzilla::Install::Util::_cache; sub request_cache { if ($ENV{MOD_PERL}) { require Apache2::RequestUtil; # Sometimes (for example, during mod_perl.pl), the request # object isn't available, and we should use $_request_cache instead. my $request = eval { Apache2::RequestUtil->request }; return $_request_cache if !$request; return $request->pnotes(); } return $_request_cache; } sub clear_request_cache { $_request_cache = {}; if ($ENV{MOD_PERL}) { require Apache2::RequestUtil; my $request = eval { Apache2::RequestUtil->request }; if ($request) { my $pnotes = $request->pnotes; delete @$pnotes{(keys %$pnotes)}; } } } # This is a per-process cache. Under mod_cgi it's identical to the # request_cache. When using mod_perl, items in this cache live until the # worker process is terminated. our $_process_cache = {}; sub process_cache { return $_process_cache; } # This is a memcached wrapper, which provides cross-process and cross-system # caching. sub memcached { return $_[0]->process_cache->{memcached} ||= Bugzilla::Memcached->_new(); } # Private methods # Per-process cleanup. Note that this is a plain subroutine, not a method, # so we don't have $class available. sub _cleanup { my $cache = Bugzilla->request_cache; my $main = $cache->{dbh_main}; my $shadow = $cache->{dbh_shadow}; foreach my $dbh ($main, $shadow) { next if !$dbh; $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction; $dbh->disconnect; } my $smtp = $cache->{smtp}; $smtp->disconnect if $smtp; clear_request_cache(); # These are both set by CGI.pm but need to be undone so that # Apache can actually shut down its children if it needs to. foreach my $signal (qw(TERM PIPE)) { $SIG{$signal} = 'DEFAULT' if $SIG{$signal} && $SIG{$signal} eq 'IGNORE'; } } sub END { # This is managed in mod_perl.pl and app.psgi when running # in a persistent environment. _cleanup() unless i_am_persistent(); } # Also managed in mod_perl.pl and app.psgi. init_page() unless i_am_persistent(); 1; __END__ =head1 NAME Bugzilla - Semi-persistent collection of various objects used by scripts and modules =head1 SYNOPSIS use Bugzilla; sub someModulesSub { Bugzilla->dbh->prepare(...); Bugzilla->template->process(...); } =head1 DESCRIPTION Several Bugzilla 'things' are used by a variety of modules and scripts. This includes database handles, template objects, and so on. This module is a singleton intended as a central place to store these objects. This approach has several advantages: =over 4 =item * They're not global variables, so we don't have issues with them staying around with mod_perl =item * Everything is in one central place, so it's easy to access, modify, and maintain =item * Code in modules can get access to these objects without having to have them all passed from the caller, and the caller's caller, and.... =item * We can reuse objects across requests using mod_perl where appropriate (eg templates), whilst destroying those which are only valid for a single request (such as the current user) =back Note that items accessible via this object are demand-loaded when requested. For something to be added to this object, it should either be able to benefit from persistence when run under mod_perl (such as the a C