diff options
Diffstat (limited to 'Bugzilla.pm')
-rw-r--r-- | Bugzilla.pm | 1054 |
1 files changed, 545 insertions, 509 deletions
diff --git a/Bugzilla.pm b/Bugzilla.pm index 27ed0876c..85a2e4b48 100644 --- a/Bugzilla.pm +++ b/Bugzilla.pm @@ -64,33 +64,34 @@ use constant request_cache => Bugzilla::Install::Util::_cache(); # 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(); - } + # 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'; + } - # 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; + if (i_am_cgi()) { + Bugzilla::Logging->fields->{remote_ip} = remote_ip(); + } - my $script = basename($0); + # 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; - # Because of attachment_base, attachment.cgi handles this itself. - if ($script ne 'attachment.cgi') { - do_ssl_redirect_if_required(); - } + my $script = basename($0); + + # Because of attachment_base, attachment.cgi handles this itself. + if ($script ne 'attachment.cgi') { + do_ssl_redirect_if_required(); + } } ##################################################################### @@ -98,618 +99,647 @@ sub init_page { ##################################################################### my $preload_templates = 0; + sub preload_templates { - $preload_templates = 1; + $preload_templates = 1; - delete request_cache->{template}; - template(); - return 1; + delete request_cache->{template}; + template(); + return 1; } sub template { - request_cache->{template} //= Bugzilla::Template->create(preload => $preload_templates); - request_cache->{template}->{_is_main} = 1; + request_cache->{template} + //= Bugzilla::Template->create(preload => $preload_templates); + request_cache->{template}->{_is_main} = 1; - return request_cache->{template}; + 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 || ''; - my %options = (language => $lang, preload => $preload_templates); - return $cache->{"template_inner_$lang"} ||= Bugzilla::Template->create(%options); + my (undef, $lang) = @_; + my $cache = request_cache; + my $current_lang = $cache->{template_current_lang}->[0]; + $lang ||= $current_lang || ''; + my %options = (language => $lang, preload => $preload_templates); + return $cache->{"template_inner_$lang"} + ||= Bugzilla::Template->create(%options); } 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; + + # 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); + } } - $recursive = 0; - return $cache->{extensions}; + $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}; + 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} ||= Bugzilla::CGI->new; + return request_cache->{cgi} ||= Bugzilla::CGI->new; } 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}; + 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}; + # 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(); + return $_[0]->process_cache->{localconfig} ||= read_localconfig(); } sub urlbase { - my ($class) = @_; + my ($class) = @_; - # Since this could be modified, we have to return a new one every time. - return URI->new($class->localconfig->{urlbase}); + # 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(); + 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}; + my ($class, $name) = @_; + return $class->localconfig->{param_override}{$name} // $class->params->{$name}; } sub user { - return request_cache->{user} ||= new Bugzilla::User; + return request_cache->{user} ||= new Bugzilla::User; } sub set_user { - my (undef, $new_user, %option) = @_; - - if ($option{scope_guard}) { - my $old_user = request_cache->{user}; - request_cache->{user} = $new_user; - return Scope::Guard->new( - sub { - request_cache->{user} = $old_user; - } - ) - } - else { - request_cache->{user} = $new_user; - } + my (undef, $new_user, %option) = @_; + + if ($option{scope_guard}) { + my $old_user = request_cache->{user}; + request_cache->{user} = $new_user; + return Scope::Guard->new(sub { + request_cache->{user} = $old_user; + }); + } + else { + request_cache->{user} = $new_user; + } } sub sudoer { - return request_cache->{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. + 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}; + return request_cache->{page_requires_login}; } sub github_secret { - my ($class) = @_; - my $cache = request_cache; - my $cgi = $class->cgi; + my ($class) = @_; + my $cache = request_cache; + my $cgi = $class->cgi; - $cache->{github_secret} //= $cgi->cookie('github_secret') // generate_random_password(256); + $cache->{github_secret} //= $cgi->cookie('github_secret') + // generate_random_password(256); - return $cache->{github_secret}; + return $cache->{github_secret}; } sub passwdqc { - my ($class) = @_; - require Data::Password::passwdqc; + my ($class) = @_; + require Data::Password::passwdqc; - my $cache = request_cache; - my $params = $class->params; + my $cache = request_cache; + my $params = $class->params; - return $cache->{passwdqc} if $cache->{passwdqc}; + return $cache->{passwdqc} if $cache->{passwdqc}; - my @min = map { $_ eq 'undef' ? undef : $_ } - split( /\s*,\s*/, $params->{passwdqc_min} ); + 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}, - ); + 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 ($class, $password1) = @_; - my $pwqc = $class->passwdqc; - ThrowUserError( 'password_insecure', { reason => $pwqc->reason } ) - unless $pwqc->validate_password($password1); + my $pwqc = $class->passwdqc; + ThrowUserError('password_insecure', {reason => $pwqc->reason}) + unless $pwqc->validate_password($password1); } sub assert_passwords_match { - my ( $class, $password1, $password2 ) = @_; + my ($class, $password1, $password2) = @_; - ThrowUserError('password_mismatch') if $password1 ne $password2; + ThrowUserError('password_mismatch') if $password1 ne $password2; } sub login { - my ($class, $type) = @_; + my ($class, $type) = @_; - return $class->user if $class->user->id; + return $class->user if $class->user->id; - my $authorizer = new Bugzilla::Auth(); - $type = LOGIN_REQUIRED if $class->cgi->param('GoAheadAndLogIn'); + 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; - } + 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; - } + # 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); + my $authenticated_user = $authorizer->login($type); - if (i_am_cgi() && $authenticated_user->id) { - Bugzilla::Logging->fields->{user_id} = $authenticated_user->id; - } + 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(); - } + # 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}); } - # 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 }); - } + # 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 { - $class->set_user($authenticated_user); + 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(); } + } - if (Bugzilla->sudoer) { - Bugzilla->sudoer->update_last_seen_date(); - } else { - $class->user->update_last_seen_date(); + # 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'); } - if ($type == LOGIN_REQUIRED && ! $class->user->id) { - FATAL("Detected failure to throw login_required when login was required and user is not logged in."); - ThrowUserError('login_required'); + 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(); + } + + if ($type == LOGIN_REQUIRED && !$class->user->id) { + FATAL( + "Detected failure to throw login_required when login was required and user is not logged in." + ); + ThrowUserError('login_required'); + } - return $class->user; + return $class->user; } sub logout { - my ($class, $option) = @_; + my ($class, $option) = @_; - # If we're not logged in, go away - return unless $class->user->id; + # 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; + $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}); + 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); + 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! + 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(); + 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; + 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(); + return request_cache->{dbh_main} ||= Bugzilla::DB::connect_main(); } sub languages { - return Bugzilla::Install::Util::supported_languages(); + return Bugzilla::Install::Util::supported_languages(); } sub current_language { - return request_cache->{current_language} ||= (include_languages())[0]; + 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); + 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}; + 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); - } - elsif ($newval == USAGE_MODE_MOJO) { - $class->error_mode(ERROR_MODE_MOJO); - } - else { - ThrowCodeError('usage_mode_invalid', - {'invalid_usage_mode', $newval}); - } - request_cache->{usage_mode} = $newval; + 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); } - return request_cache->{usage_mode} - || (i_am_cgi()? USAGE_MODE_BROWSER : USAGE_MODE_CMDLINE); + elsif ($newval == USAGE_MODE_MOJO) { + $class->error_mode(ERROR_MODE_MOJO); + } + 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; + 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); + my (undef, $filename) = @_; + if ($filename) { + my $s = new Safe; + $s->rdo($filename); - die "Error reading $filename: $!" if $!; - die "Error evaluating $filename: $@" if $@; + 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} || {}; + # 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; - } + 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}; - 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; + # 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; + my $class = shift; - request_cache->{dbh} = $class->dbh_main; - return $class->dbh_main; + 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 ($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; - } + 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 + 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 $@; + VALUES (?, ?, ?, ?, ?, NOW(), ?, ?, ?, ?)", undef, @params + ); + }; + warn $@ if $@; } sub is_shadow_db { - my $class = shift; - return request_cache->{dbh} != $class->dbh_main; + 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 (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 $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}; + 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}; - } - } + # 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]; + 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}}; + 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}; + 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'); + 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)); + 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} } : (); + my (undef, %option) = @_; + my $request_cache = request_cache(); + my @except = $option{except} ? @{$option{except}} : (); - %{ $request_cache } = map { $_ => $request_cache->{$_} } @except; + %{$request_cache} = map { $_ => $request_cache->{$_} } @except; } # This is a per-process cache. Under mod_cgi it's identical to the @@ -718,72 +748,74 @@ sub clear_request_cache { our $_process_cache = {}; sub process_cache { - return $_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(); + return request_cache->{memcached} ||= Bugzilla::Memcached->_new(); } # Connector to the Datadog metrics collection daemon. sub datadog { - my ($class, $namespace) = @_; - my $host = $class->localconfig->{datadog_host}; - my $port = $class->localconfig->{datadog_port}; - - $namespace //= ''; - - if ($class->has_feature('datadog') && $host) { - require DataDog::DogStatsd; - return request_cache->{datadog}{$namespace} //= DataDog::DogStatsd->new( - host => $host, - port => $port, - namespace => $namespace ? "$namespace." : '', - ); - } - else { - return undef; - } + my ($class, $namespace) = @_; + my $host = $class->localconfig->{datadog_host}; + my $port = $class->localconfig->{datadog_port}; + + $namespace //= ''; + + if ($class->has_feature('datadog') && $host) { + require DataDog::DogStatsd; + return request_cache->{datadog}{$namespace} //= DataDog::DogStatsd->new( + host => $host, + port => $port, + namespace => $namespace ? "$namespace." : '', + ); + } + else { + return undef; + } } sub elastic { - my ($class) = @_; - $class->process_cache->{elastic} //= Bugzilla::Elastic->new(); + 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::Quantum::CGI::C->block_ip($ip); - ThrowUserError("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::Quantum::CGI::C->block_ip($ip); + ThrowUserError("rate_limit"); + } } + } } sub markdown_parser { - require Bugzilla::Markdown::GFM; - require Bugzilla::Markdown::GFM::Parser; - return request_cache->{markdown_parser} - ||= Bugzilla::Markdown::GFM::Parser->new( {extensions => [qw( autolink tagfilter table strikethrough)] } ); + require Bugzilla::Markdown::GFM; + require Bugzilla::Markdown::GFM::Parser; + return request_cache->{markdown_parser} + ||= Bugzilla::Markdown::GFM::Parser->new( + {extensions => [qw( autolink tagfilter table strikethrough)]}); } # Private methods @@ -791,29 +823,33 @@ sub markdown_parser { # Per-process cleanup. Note that this is a plain subroutine, not a method, # so we don't have $class available. *cleanup = \&_cleanup; + sub _cleanup { - return if $^C; + return if $^C; - # BMO - allow "end of request" processing - Bugzilla::Hook::process('request_cleanup'); - Bugzilla::Bug->CLEANUP; + # 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(); + 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(); - Log::Log4perl::MDC->remove(); + Log::Log4perl::MDC->remove(); } our ($caller_package, $caller_file) = caller; -init_page() if $caller_package eq 'main' && $caller_package !~ /^Test/ && $caller_file =~ /\.t$/; +init_page() + if $caller_package eq 'main' + && $caller_package !~ /^Test/ + && $caller_file =~ /\.t$/; END { - cleanup() if $caller_package eq 'main'; + cleanup() if $caller_package eq 'main'; } 1; |