# 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; use Bugzilla::Logging; # We want any compile errors to get to the browser, if possible. BEGIN { # This makes sure we're in a CGI. if ($ENV{SERVER_SOFTWARE} && !$ENV{MOD_PERL}) { require CGI::Carp; CGI::Carp->import('fatalsToBrowser'); } } our $VERSION = '20180703.1'; use Bugzilla::Auth; use Bugzilla::Auth::Persist::Cookie; use Bugzilla::CGI; use Bugzilla::Elastic; use Bugzilla::Config; use Bugzilla::Constants; use Bugzilla::DB; use Bugzilla::Error; use Bugzilla::Extension; use Bugzilla::Field; use Bugzilla::Flag; use Bugzilla::Hook; use Bugzilla::Install::Localconfig qw(read_localconfig); use Bugzilla::Install::Util qw(init_console include_languages); use Bugzilla::Markdown::GFM; use Bugzilla::Markdown::GFM::Parser; use Bugzilla::Memcached; use Bugzilla::Template; use Bugzilla::Token; use Bugzilla::User; use Bugzilla::Util; use Bugzilla::CPAN; use Bugzilla::Bloomfilter; use Date::Parse; use DateTime::TimeZone; use Encode; use File::Basename; use File::Spec::Functions; use Safe; use JSON::XS qw(decode_json); use URI; use parent qw(Bugzilla::CPAN); ##################################################################### # 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; # 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. use constant request_cache => Bugzilla::Install::Util::_cache(); ##################################################################### # Global Code ##################################################################### # Note that this is a raw subroutine, not a method, so $class isn't available. sub init_page { # This is probably not needed, but bugs resulting from a dirty # request cache are very annoying (see bug 1347335) # and this is not an expensive operation. clear_request_cache(); if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { init_console(); } elsif (Bugzilla->params->{'utf8'}) { binmode STDOUT, ':utf8'; } if (i_am_cgi()) { Bugzilla::Logging->fields->{remote_ip} = remote_ip(); } if (${^TAINT}) { # 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'} = ''; } # 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; 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 (Bugzilla->params->{"shutdownhtml"} && !grep { $_ eq $script } SHUTDOWNHTML_EXEMPT) { # 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; } # For security reasons, log out users when Bugzilla is down. # Bugzilla->login() is required to catch the logincookie, if any. my $user; eval { $user = 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(); my $template = Bugzilla->template; my $vars = {}; $vars->{'message'} = 'shutdown'; $vars->{'userid'} = $userid; # Generate and return a message about the downtime, appropriately # for if we're a command-line script or a CGI script. my $extension; if (i_am_cgi() && (!Bugzilla->cgi->param('ctype') || Bugzilla->cgi->param('ctype') eq 'html')) { $extension = 'html'; } else { $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 Bugzilla->cgi->header(-status => 503, -retry_after => SHUTDOWNHTML_RETRY_AFTER); } my $t_output; $template->process("global/message.$extension.tmpl", $vars, \$t_output) || ThrowTemplateError($template->error); print $t_output . "\n"; exit; } } ##################################################################### # Subroutines and Methods ##################################################################### sub template { request_cache->{template} ||= Bugzilla::Template->create(); request_cache->{template}->{_is_main} = 1; return request_cache->{template}; } sub template_inner { my (undef, $lang) = @_; my $cache = request_cache; my $current_lang = $cache->{template_current_lang}->[0]; $lang ||= $current_lang || ''; return $cache->{"template_inner_$lang"} ||= Bugzilla::Template->create(language => $lang); } sub extensions { # Guard against extensions querying the extension list during initialization # (through this method or has_extension). # The extension list is not fully populated at that point, # so the results would not be meaningful. state $recursive = 0; die "Recursive attempt to load/query extensions" if $recursive; $recursive = 1; my $cache = request_cache; if (!$cache->{extensions}) { my $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; } $recursive = 0; return $cache->{extensions}; } sub has_extension { my ($class, $name) = @_; my $cache = $class->request_cache; if (!$cache->{extensions_hash}) { my %extensions = map { $_->NAME => 1 } @{ Bugzilla->extensions }; $cache->{extensions_hash} = \%extensions; } return exists $cache->{extensions_hash}{$name}; } sub cgi { return request_cache->{cgi} ||= new Bugzilla::CGI(); } sub input_params { my ($class, $params) = @_; my $cache = 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 urlbase { my ($class) = @_; # Since this could be modified, we have to return a new one every time. return URI->new($class->localconfig->{urlbase}); } sub params { return request_cache->{params} ||= Bugzilla::Config::read_param_file(); } sub get_param_with_override { my ($class, $name) = @_; return $class->localconfig->{param_override}{$name} // $class->params->{$name}; } sub user { return request_cache->{user} ||= new Bugzilla::User; } sub set_user { my (undef, $user) = @_; request_cache->{user} = $user; } sub sudoer { return request_cache->{sudoer}; } sub sudo_request { my (undef, $new_user, $new_sudoer) = @_; request_cache->{user} = $new_user; 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 request_cache->{page_requires_login}; } sub github_secret { my ($class) = @_; my $cache = request_cache; my $cgi = $class->cgi; $cache->{github_secret} //= $cgi->cookie('github_secret') // generate_random_password(256); return $cache->{github_secret}; } sub passwdqc { my ($class) = @_; require Data::Password::passwdqc; my $cache = request_cache; my $params = $class->params; return $cache->{passwdqc} if $cache->{passwdqc}; my @min = map { $_ eq 'undef' ? undef : $_ } split( /\s*,\s*/, $params->{passwdqc_min} ); return $cache->{passwdqc} = Data::Password::passwdqc->new( min => \@min, max => $params->{passwdqc_max}, passphrase_words => $params->{passwdqc_passphrase_words}, match_length => $params->{passwdqc_match_length}, random_bits => $params->{passwdqc_random_bits}, ); } sub assert_password_is_secure { my ( $class, $password1 ) = @_; my $pwqc = $class->passwdqc; ThrowUserError( 'password_insecure', { reason => $pwqc->reason } ) unless $pwqc->validate_password($password1); } sub assert_passwords_match { my ( $class, $password1, $password2 ) = @_; ThrowUserError('password_mismatch') if $password1 ne $password2; } sub login { my ($class, $type) = @_; return $class->user if $class->user->id; # Load all extensions here if not running under mod_perl $class->extensions unless BZ_PERSISTENT; 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) { request_cache->{page_requires_login} = 1; } my $authenticated_user = $authorizer->login($type); if (i_am_cgi() && $authenticated_user->id) { Bugzilla::Logging->fields->{user_id} = $authenticated_user->id; } # At this point, we now know if a real person is logged in. # Check if a password reset is required my $cgi = Bugzilla->cgi; my $script_name = $cgi->script_name; my $do_logout = $cgi->param('logout'); if ( $authenticated_user->password_change_required ) { # We cannot show the password reset UI for API calls, so treat those as # a disabled account. if ( i_am_webservice() ) { ThrowUserError( "account_disabled", { disabled_reason => $authenticated_user->password_change_reason } ); } # only allow the reset-password and token pages to handle requests # (tokens handles the 'forgot password' process) # otherwise redirect user to the reset-password page. if ( $script_name !~ m#/(?:reset_password|token)\.cgi$# && !$do_logout ) { my $self_url = trim($cgi->self_url); my $sig_type = 'prev_url:' . $authenticated_user->id; my $self_url_sig = issue_hash_sig($sig_type, $self_url); my $redir_url = URI->new( Bugzilla->localconfig->{urlbase} . "reset_password.cgi" ); $redir_url->query_form(prev_url => $self_url, prev_url_sig => $self_url_sig); print $cgi->redirect($redir_url); exit; } } elsif ( !i_am_webservice() && $authenticated_user->in_mfa_group && !$authenticated_user->mfa ) { # decide if the user needs a warning or to be blocked. my $date = $authenticated_user->mfa_required_date('UTC'); my $grace_period = Bugzilla->params->{mfa_group_grace_period}; my $expired = defined $date && $date < DateTime->now; my $on_mfa_page = $script_name eq '/userprefs.cgi' && $cgi->param('tab') eq 'mfa'; my $on_token_page = $script_name eq '/token.cgi'; Bugzilla->request_cache->{mfa_warning} = 1; Bugzilla->request_cache->{mfa_grace_period_expired} = $expired; Bugzilla->request_cache->{on_mfa_page} = $on_mfa_page; if ( $grace_period == 0 || $expired) { if ( !( $on_mfa_page || $on_token_page || $do_logout ) ) { print Bugzilla->cgi->redirect("userprefs.cgi?tab=mfa"); exit; } } else { my $dbh = Bugzilla->dbh_main; my $date = $dbh->sql_date_math( 'NOW()', '+', '?', 'DAY' ); my ($mfa_required_date) = $dbh->selectrow_array( "SELECT $date", undef, $grace_period ); $authenticated_user->set_mfa_required_date($mfa_required_date); $authenticated_user->update(); } } # 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); 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 (Bugzilla->sudoer) { Bugzilla->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 request_cache->{user}; delete 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 job_queue { require Bugzilla::JobQueue; return request_cache->{job_queue} ||= Bugzilla::JobQueue->new(); } sub dbh { my ($class) = @_; # If we're not connected, then we must want the main db return request_cache->{dbh} ||= $class->dbh_main; } sub dbh_main { return request_cache->{dbh_main} ||= Bugzilla::DB::connect_main(); } sub languages { return Bugzilla::Install::Util::supported_languages(); } sub current_language { return request_cache->{current_language} ||= (include_languages())[0]; } sub error_mode { my (undef, $newval) = @_; if (defined $newval) { request_cache->{error_mode} = $newval; } return request_cache->{error_mode} || (i_am_cgi() ? ERROR_MODE_WEBPAGE : ERROR_MODE_DIE); } # This is used only by Bugzilla::Error to throw errors. sub _json_server { my (undef, $newval) = @_; if (defined $newval) { request_cache->{_json_server} = $newval; } return 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}); } request_cache->{usage_mode} = $newval; } return request_cache->{usage_mode} || (i_am_cgi()? USAGE_MODE_BROWSER : USAGE_MODE_CMDLINE); } sub installation_mode { my (undef, $newval) = @_; (request_cache->{installation_mode} = $newval) if defined $newval; return request_cache->{installation_mode} || INSTALLATION_MODE_INTERACTIVE; } sub installation_answers { my (undef, $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 request_cache->{installation_answers} = $s->varglob('answer'); } return request_cache->{installation_answers} || {}; } sub switch_to_shadow_db { my $class = shift; if (!request_cache->{dbh_shadow}) { if ($class->get_param_with_override('shadowdb')) { request_cache->{dbh_shadow} = Bugzilla::DB::connect_shadow(); } else { request_cache->{dbh_shadow} = $class->dbh_main; } } request_cache->{dbh} = 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; request_cache->{dbh} = $class->dbh_main; return $class->dbh_main; } sub log_user_request { my ($class, $bug_id, $attach_id, $action) = @_; return unless (i_am_cgi() || i_am_webservice()) && Bugzilla->params->{log_user_requests}; my $cgi = $class->cgi; my $user_id = $class->user->id; my $request_url = $cgi->request_uri // ''; my $method = $cgi->request_method; my $user_agent = $cgi->user_agent // ''; my $script_name = $cgi->script_name; my $server = "web"; if ($script_name =~ /rest\.cgi/) { $server = $script_name =~ /BzAPI/ ? "bzapi" : "rest"; } elsif ($script_name =~ /xmlrpc\.cgi/) { $server = "xmlrpc"; } elsif ($script_name =~ /jsonrpc\.cgi/) { $server = "jsonrpc"; } my @params = ($user_id, remote_ip(), $user_agent, $request_url, $method, $bug_id, $attach_id, $action, $server); foreach my $param (@params) { trick_taint($param) if defined $param; } eval { local request_cache->{dbh}; $class->switch_to_main_db(); $class->dbh->do("INSERT INTO user_request_log (user_id, ip_address, user_agent, request_url, method, timestamp, bug_id, attach_id, action, server) VALUES (?, ?, ?, ?, ?, NOW(), ?, ?, ?, ?)", undef, @params); }; warn $@ if $@; } sub is_shadow_db { my $class = shift; return request_cache->{dbh} != $class->dbh_main; } sub fields { my (undef, $criteria) = @_; $criteria ||= {}; my $cache = 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 (undef, $params) = @_; my $cache_id = 'active_custom_fields'; if ($params) { $cache_id .= ($params->{product} ? '_p' . $params->{product}->id : '') . ($params->{component} ? '_c' . $params->{component}->id : ''); $cache_id .= ':noext' if $params->{skip_extensions}; } if (!exists request_cache->{$cache_id}) { my $fields = Bugzilla::Field->match({ custom => 1, obsolete => 0, skip_extensions => 1 }); Bugzilla::Hook::process('active_custom_fields', { fields => \$fields, params => $params }); request_cache->{$cache_id} = $fields; } return @{request_cache->{$cache_id}}; } sub has_flags { if (!defined request_cache->{has_flags}) { request_cache->{has_flags} = Bugzilla::Flag->any_exist; } return request_cache->{has_flags}; } sub local_timezone { return $_[0]->process_cache->{local_timezone} ||= DateTime::TimeZone->new(name => 'local'); } # Send messages to syslog for the auditing systems (eg. mozdef) to pick up. sub audit { my (undef, $message) = @_; state $logger = Log::Log4perl->get_logger("audit"); $logger->notice(encode_utf8($message)); } sub clear_request_cache { my (undef, %option) = @_; my $request_cache = request_cache(); my @except = $option{except} ? @{ $option{except} } : (); %{ $request_cache } = map { $_ => $request_cache->{$_} } @except; } # 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 request_cache->{memcached} ||= Bugzilla::Memcached->_new(); } sub elastic { my ($class) = @_; $class->process_cache->{elastic} //= Bugzilla::Elastic->new(); } sub check_rate_limit { my ($class, $name, $ip) = @_; my $params = Bugzilla->params; if ($params->{rate_limit_active}) { my $rules = decode_json($params->{rate_limit_rules}); my $limit = $rules->{$name}; unless ($limit) { warn "no rules for $name!"; return 0; } if (Bugzilla->memcached->should_rate_limit("$name:$ip", @$limit)) { my $action = 'block'; my $filter = Bugzilla::Bloomfilter->lookup("rate_limit_whitelist"); if ($filter && $filter->test($ip)) { $action = 'ignore'; } my $limit = join("/", @$limit); Bugzilla->audit("[rate_limit] action=$action, ip=$ip, limit=$limit, name=$name"); if ($action eq 'block') { Bugzilla::ModPerl::BlockIP->block_ip($ip); ThrowUserError("rate_limit"); } } } } sub markdown_parser { return request_cache->{markdown_parser} ||= Bugzilla::Markdown::GFM::Parser->new( {extensions => [qw( autolink tagfilter table strikethrough)] } ); } # Private methods # Per-process cleanup. Note that this is a plain subroutine, not a method, # so we don't have $class available. sub _cleanup { return if $^C; # BMO - allow "end of request" processing Bugzilla::Hook::process('request_cleanup'); Bugzilla::Bug->CLEANUP; my $main = Bugzilla->request_cache->{dbh_main}; my $shadow = Bugzilla->request_cache->{dbh_shadow}; foreach my $dbh ($main, $shadow) { next if !$dbh; $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction; } 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'; } Log::Log4perl::MDC->remove(); } sub END { # Bugzilla.pm cannot compile in mod_perl.pl if this runs. _cleanup() unless BZ_PERSISTENT; } init_page() unless BZ_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