Commit 53fccc68 authored by System Administrator's avatar System Administrator

Bugzilla 5.9.1 initial import

parents

Too many changes to show.

To preserve performance only 1000 of 1000+ files are displayed.

This diff is collapsed. Click to expand it.
# 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::App;
use Mojo::Base 'Mojolicious';
# Needed for its exit() overload, must happen early in execution.
use CGI::Compile;
use utf8;
use Encode;
use FileHandle; # this is for compat back to 5.10
use Bugzilla ();
use Bugzilla::BugMail ();
use Bugzilla::CGI ();
use Bugzilla::Constants qw(bz_locations MAX_STS_AGE);
use Bugzilla::Extension ();
use Bugzilla::Install::Requirements ();
use Bugzilla::Logging;
use Bugzilla::App::API;
use Bugzilla::App::BouncedEmails;
use Bugzilla::App::CGI;
use Bugzilla::App::Main;
use Bugzilla::App::Users;
use if Bugzilla->has_feature('oauth2_server'), 'Bugzilla::App::OAuth2::Clients';
use Bugzilla::App::SES;
use Bugzilla::App::Static;
use Mojo::Loader qw( find_modules );
use Module::Runtime qw( require_module );
use Bugzilla::Util ();
use Cwd qw(realpath);
use MojoX::Log::Log4perl;
use Bugzilla::WebService::Server::REST;
use Try::Tiny;
has 'static' => sub { Bugzilla::App::Static->new };
sub startup {
my ($self) = @_;
$self->log(MojoX::Log::Log4perl->new);
TRACE('Starting up');
$self->plugin('Bugzilla::App::Plugin::BlockIP');
$self->plugin('Bugzilla::App::Plugin::Glue');
$self->plugin('Bugzilla::App::Plugin::Hostage')
unless $ENV{BUGZILLA_DISABLE_HOSTAGE};
$self->plugin('Bugzilla::App::Plugin::SizeLimit')
unless $ENV{BUGZILLA_DISABLE_SIZELIMIT};
$self->plugin('ForwardedFor') if Bugzilla->has_feature('better_xff');
$self->plugin('Bugzilla::App::Plugin::Helpers');
if (Bugzilla->has_feature('oauth2_server')) {
$self->plugin('Bugzilla::App::Plugin::OAuth2');
}
push @{$self->commands->namespaces}, 'Bugzilla::App::Command';
push @{$self->renderer->paths}, @{ Bugzilla::Template::_include_path() };
$self->sessions->cookie_name('bugzilla');
$self->sessions->default_expiration(60 * 60 * 24 * 7); # 1 week
$self->hook(
before_routes => sub {
my ($c) = @_;
return if $c->stash->{'mojo.static'};
# It is possible the regexp is bad.
# If that is the case, we just log the error and continue on.
try {
my $regexp = Bugzilla->params->{block_user_agent};
my $user_agent = $c->req->headers->user_agent // '';
if ($regexp && $user_agent =~ /$regexp/) {
my $msg = "Contact " . Bugzilla->params->{maintainer};
$c->respond_to(
json => {json => {error => $msg}, status => 400},
any => {text => "$msg\n", status => 400},
);
}
}
catch {
ERROR($_);
};
}
);
$ENV{MOJO_MAX_LINE_SIZE} ||= 1024 * 10; # Mojo default is 8kb
# hypnotoad is weird and doesn't look for MOJO_LISTEN itself.
$self->config(
hypnotoad => {
proxy => $ENV{MOJO_REVERSE_PROXY} // 1,
heartbeat_interval => $ENV{MOJO_HEARTBEAT_INTERVAL} // 10,
heartbeat_timeout => $ENV{MOJO_HEARTBEAT_TIMEOUT} // 120,
inactivity_timeout => $ENV{MOJO_INACTIVITY_TIMEOUT} // 120,
workers => $ENV{MOJO_WORKERS} // 1,
clients => $ENV{MOJO_CLIENTS} // 200,
spare => $ENV{MOJO_SPARE} // 1,
listen => [$ENV{MOJO_LISTEN} // 'http://*:3000'],
},
);
# Make sure each httpd child receives a different random seed (bug 476622).
# Bugzilla::RNG has one srand that needs to be called for
# every process, and Perl has another. (Various Perl modules still use
# the built-in rand(), even though we never use it in Bugzilla itself,
# so we need to srand() both of them.)
# Also, ping the dbh to force a reconnection.
Mojo::IOLoop->next_tick(sub {
Bugzilla::RNG::srand();
srand();
eval { Bugzilla->dbh->ping };
});
Bugzilla::Extension->load_all();
if ($self->mode ne 'development') {
Bugzilla->preload_features();
DEBUG('preloading templates');
Bugzilla->preload_templates();
DEBUG('done preloading templates');
require_module($_) for find_modules('Bugzilla::User::Setting');
$self->hook(
after_static => sub {
my ($c) = @_;
my $version = $c->stash->{static_file_version};
if ($version && $version > Bugzilla->VERSION) {
$c->res->headers->cache_control('no-cache, no-store');
}
else {
$c->res->headers->cache_control('public, max-age=31536000, immutable');
}
}
);
}
$self->hook(after_dispatch => sub {
my ($c) = @_;
my ($req, $res) = ($c->req, $c->res);
if ( $req->is_secure
&& !$res->headers->strict_transport_security
&& Bugzilla->params->{'strict_transport_security'} ne 'off')
{
my $sts_opts = 'max-age=' . MAX_STS_AGE;
if (Bugzilla->params->{'strict_transport_security'} eq 'include_subdomains') {
$sts_opts .= '; includeSubDomains';
}
$res->headers->strict_transport_security($sts_opts);
}
# Add X-Frame-Options header to prevent framing and subsequent
# possible clickjacking problems.
unless ($c->url_is_attachment_base) {
$res->headers->header('X-frame-options' => 'SAMEORIGIN');
}
# Add X-XSS-Protection header to prevent simple XSS attacks
# and enforce the blocking (rather than the rewriting) mode.
$res->headers->header('X-xss-protection' => '1; mode=block');
# Add X-Content-Type-Options header to prevent browsers sniffing
# the MIME type away from the declared Content-Type.
$res->headers->header('X-content-type-options' => 'nosniff');
if (length($req->url->to_abs->to_string) > 8000) {
$res->headers->header('Referrer-policy' => 'origin');
}
else {
$res->headers->header('Referrer-policy' => 'same-origin');
}
unless ($res->headers->content_security_policy) {
if (my $csp = $c->content_security_policy) {
$res->headers->header($csp->header_name, $csp->value);
}
}
});
Bugzilla::WebService::Server::REST->preload;
$self->setup_routes;
Bugzilla::Hook::process('app_startup', {app => $self});
}
sub setup_routes {
my ($self) = @_;
my $r = $self->routes;
Bugzilla::App::API->setup_routes($r);
Bugzilla::App::BouncedEmails->setup_routes($r);
Bugzilla::App::CGI->setup_routes($r);
Bugzilla::App::Main->setup_routes($r);
Bugzilla::App::Users->setup_routes($r);
Bugzilla::App::OAuth2::Clients->setup_routes($r)
if Bugzilla->has_feature('oauth2_server');
Bugzilla::App::SES->setup_routes($r);
$r->static_file('/__lbheartbeat__');
$r->static_file(
'/__version__' => {file => 'version.json', content_type => 'application/json'});
$r->static_file('/version.json', {content_type => 'application/json'});
$r->page('/review', 'splinter.html');
$r->page('/user_profile', 'user_profile.html');
$r->page('/userprofile', 'user_profile.html');
$r->page('/request_defer', 'request_defer.html');
}
1;
# 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::App::API;
use 5.10.1;
use Mojo::Base qw( Mojolicious::Controller );
use Mojo::JSON qw( true false );
sub setup_routes {
my ($class, $r) = @_;
$r->get('/api/user/profile')->to('API#user_profile');
}
sub user_profile {
my ($self) = @_;
my $user = $self->bugzilla->oauth('user:read');
if ($user && $user->id) {
$self->render(
json => {
id => $user->id,
name => $user->name,
login => $user->login,
nick => $user->nick,
groups => [map { $_->name } @{$user->groups}],
mfa => lc($user->mfa),
mfa_required_by_group => $user->in_mfa_group ? true : false,
}
);
}
else {
$self->render(status => 401, text => 'Unauthorized');
}
}
1;
# 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::App::BouncedEmails;
use 5.10.1;
use Mojo::Base qw( Mojolicious::Controller );
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Token;
sub setup_routes {
my ($class, $r) = @_;
$r->any('/bounced_emails/:userid')->to('BouncedEmails#view');
}
sub view {
my ($self) = @_;
my $user = $self->bugzilla->login(LOGIN_REQUIRED);
my $other_user = Bugzilla::User->check({id => $self->param('userid')});
unless ($user->in_group('editusers')
|| $user->in_group('disableusers')
|| $user->id == $other_user->id)
{
ThrowUserError('auth_failure',
{reason => "not_visible", action => "modify", object => "user"});
}
if ( $self->param('enable_email')
&& $user->id == $other_user->id
&& $other_user->email_disabled)
{
my $token = $self->param('token');
check_token_data($token, 'bounced_emails');
$other_user->set_email_enabled(1);
$other_user->update();
return $self->redirect_to('/home');
}
my $token = issue_session_token('bounced_emails');
$self->stash(
{
bounce_max => BOUNCE_COUNT_MAX,
user => $user,
other_user => $other_user,
token => $token
}
);
return $self->render(
template => 'account/email/bounced-emails',
handler => 'bugzilla'
);
}
1;
# 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::App::CGI;
use Mojo::Base 'Mojolicious::Controller';
use CGI::Compile;
use Bugzilla::Logging;
use Try::Tiny;
use Sys::Hostname;
use Sub::Quote 2.005000;
use Sub::Name;
use Socket qw(AF_INET inet_aton);
use Mojo::File qw(path);
use English qw(-no_match_vars);
use Bugzilla::App::Stdout;
use Bugzilla::Constants qw(bz_locations USAGE_MODE_BROWSER);
our $C;
my %SEEN;
sub setup_routes {
my ($class, $r) = @_;
foreach my $file (glob '*.cgi') {
my $name = _file_to_method($file);
$class->load_one($name, $file);
$r->any("/$file")->to("CGI#$name");
}
$r->get('/home')->to('CGI#index_cgi');
$r->any('/bug/<id:num>')->to('CGI#show_bug_cgi');
$r->any('/<id:num>')->to('CGI#show_bug_cgi');
$r->any('/rest')->to('CGI#rest_cgi');
$r->any('/rest.cgi/*PATH_INFO')->to('CGI#rest_cgi' => {PATH_INFO => ''});
$r->any('/rest/*PATH_INFO')->to('CGI#rest_cgi' => {PATH_INFO => ''});
$r->get('/__heartbeat__')->to('CGI#heartbeat_cgi');
$r->get('/robots.txt')->to('CGI#robots_cgi');
$r->any('/login')->to('CGI#index_cgi' => {'GoAheadAndLogIn' => '1'});
$r->any('/logout')->to('CGI#index_cgi' => {'logout' => '1'});
$r->any('/:new_bug' => [new_bug => qr{new[-_]bug}] => sub {
my $c = shift;
$c->res->code(301);
$c->redirect_to(Bugzilla->localconfig->basepath . 'enter_bug.cgi');
});
}
sub load_one {
my ($class, $name, $file) = @_;
my $package = __PACKAGE__ . "::$name", my $inner_name = "_$name";
my $content = path(bz_locations->{cgi_path}, $file)->slurp;
$content = "package $package; local our \$C = \$Bugzilla::App::CGI::C; $content";
my %options = (package => $package, file => $file, line => 1, no_defer => 1,);
die "Tried to load $file more than once" if $SEEN{$file}++;
my $inner = quote_sub $inner_name, $content, {}, \%options;
my $wrapper = sub {
my ($c) = @_;
my $stdin = $c->_STDIN;
local $C = $c;
local %ENV = $c->_ENV($file);
local $CGI::Compile::USE_REAL_EXIT = 0;
local $PROGRAM_NAME = $file;
local *STDIN; ## no critic (local)
open STDIN, '<', $stdin->path
or die "STDIN @{[$stdin->path]}: $!"
if -s $stdin->path;
tie *STDOUT, 'Bugzilla::App::Stdout', controller => $c; ## no critic (tie)
# the finally block calls cleanup.
$Bugzilla::App::Plugin::Glue::cleanup_guard->dismiss if $Bugzilla::App::Plugin::Glue::cleanup_guard;
Bugzilla->usage_mode(USAGE_MODE_BROWSER);
try {
Bugzilla->init_page();
$c->res->headers->cache_control('private, no-cache');
$inner->();
}
catch {
die $_ unless _is_exit($_);
}
finally {
my $error = shift;
untie *STDOUT;
$c->finish if !$error || _is_exit($error);
Bugzilla->cleanup;
};
};
no strict 'refs'; ## no critic (strict)
*{$name} = subname($name, $wrapper);
return 1;
}
sub _ENV {
my ($c, $script_name) = @_;
my $tx = $c->tx;
my $req = $tx->req;
my $headers = $req->headers;
my $content_length
= $req->content->is_multipart ? $req->body_size : $headers->content_length;
my %env_headers = (HTTP_COOKIE => '', HTTP_REFERER => '');
$headers->content_type('application/x-www-form-urlencoded; charset=utf-8')
unless $headers->content_type;
for my $name (@{$headers->names}) {
my $key = uc "http_$name";
$key =~ s/\W/_/g;
$env_headers{$key} = $headers->header($name);
}
my $remote_user;
if (my $userinfo = $req->url->to_abs->userinfo) {
$remote_user = $userinfo =~ /([^:]+)/ ? $1 : '';
}
elsif (my $authenticate = $headers->authorization) {
$remote_user = $authenticate =~ /Basic\s+(.*)/ ? b64_decode $1 : '';
$remote_user = $remote_user =~ /([^:]+)/ ? $1 : '';
}
my $path_info = $c->stash->{'mojo.captures'}{'PATH_INFO'};
my %captures = %{$c->stash->{'mojo.captures'} // {}};
foreach my $key (keys %captures) {
if ( $key eq 'controller'
|| $key eq 'action'
|| $key eq 'PATH_INFO'
|| $key =~ /^REWRITE_/)
{
delete $captures{$key};
}
}
my $cgi_query = Mojo::Parameters->new(%captures);
$cgi_query->append($req->url->query);
return (
%ENV,
CONTENT_LENGTH => $content_length || 0,
CONTENT_TYPE => $headers->content_type || '',
GATEWAY_INTERFACE => 'CGI/1.1',
HTTPS => $req->is_secure ? 'on' : 'off',
%env_headers,
QUERY_STRING => $cgi_query->to_string,
PATH_INFO => $path_info ? "/$path_info" : '',
REMOTE_ADDR => $tx->original_remote_address,
REMOTE_HOST => $tx->original_remote_address,
REMOTE_PORT => $tx->remote_port,
REMOTE_USER => $remote_user || '',
REQUEST_METHOD => $req->method,
SCRIPT_NAME => "/$script_name",
SERVER_NAME => hostname,
SERVER_PORT => $tx->local_port,
SERVER_PROTOCOL => $req->is_secure ? 'HTTPS' : 'HTTP', # TODO: Version is missing
SERVER_SOFTWARE => __PACKAGE__,
);
}
sub _STDIN {
my $c = shift;
my $stdin;
if ($c->req->content->is_multipart) {
$stdin = Mojo::Asset::File->new;
$stdin->add_chunk($c->req->build_body);
}
else {
$stdin = $c->req->content->asset;
}
return $stdin if $stdin->isa('Mojo::Asset::File');
return Mojo::Asset::File->new->add_chunk($stdin->slurp);
}
sub _file_to_method {
my ($name) = @_;
$name =~ s/\./_/s;
$name =~ s/\W+/_/gs;
return $name;
}
sub _is_exit {
my ($error) = @_;
return ref $error eq 'ARRAY' && $error->[0] eq "EXIT\n";
}
1;
# 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::App::Command::move_flag_types; ## no critic (Capitalization)
use Mojo::Base 'Mojolicious::Command';
use Bugzilla::Constants;
use Mojo::File 'path';
use Mojo::Util 'getopt';
use JSON::MaybeXS;
has description => 'Move flag types';
has usage => sub { shift->extract_usage };
sub run {
my ($self, @args) = @_;
my $doit = 0;
my ($oldid, $newid, $product, $component, $debug);
Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
getopt \@args,
'old-id|o=s' => \$oldid,
'new-id|n=s' => \$newid,
'product|p=s' => \$product,
'component|c=s' => \$component,
'doit|d' => \$doit,
'debug|D' => \$debug;
die $self->usage unless $oldid && $newid && $product;
my $model = Bugzilla->dbh->model;
my $old_flagtype = $model->resultset('FlagType')->find({id => $oldid})
or die "No flagtype $oldid";
my $new_flagtype = $model->resultset('FlagType')->find({id => $newid})
or die "No flagtype $newid";
my $bugs = $model->resultset('Bug')->search(_bug_query($product, $component));
my $flags = $bugs->search_related('flags', {'flags.type_id' => $oldid});
my $count = $flags->count;
if ($debug) {
my $query = ${ $flags->as_query };
say STDERR "SQL query:\n", JSON::MaybeXS->new(pretty => 1, canonical => 1)->encode($query);
}
if ($count) {
my $old_name = $old_flagtype->name;
my $new_name = $new_flagtype->name;
say "Moving '$count' flags from $old_name ($oldid) to $new_name ($newid)...";
if (!$doit) {
say
"Pass the argument --doit or -d to permanently make changes to the database.";
}
else {
while (my $flag = $flags->next) {
$model->txn_do(sub {
$flag->type_id($new_flagtype->id);
$flag->update();
});
say "Bug: ", $flag->bug_id, " Flag: ", $flag->id;
}
}
# It's complex to determine which items now need to be flushed from memcached.
# As this is expected to be a rare event, we just flush the entire cache.
Bugzilla->memcached->clear_all();
}
else {
say "No flags to move";
}
}
sub _bug_query {
my ($product, $component) = @_;
# if we have a component name, search on product and component name
if ($component) {
return ({'product.name' => $product, 'component.name' => $component},
{join => {component => 'product'}});
}
else {
return ({'product.name' => $product}, {join => 'product'});
}
}
1;
__END__
=head1 NAME
Bugzilla::App::Command::move_flag_types - Move currently set flags from one type id to another based
on product and optionally component.
=head1 SYNOPSIS
Usage: APPLICATION move_flag_types
./bugzilla.pl move_flag_types --old-id 4 --new-id 720 --product Firefox --component Installer
Options:
-h, --help Print a brief help message and exits.
-o, --oldid type_id Old flag type id. Use editflagtypes.cgi to determine the type id from the URL.
-n, --newid type_id New flag type id. Use editflagtypes.cgi to determine the type id from the URL.
-p, --product name The product that the bugs most be assigned to.
-c, --component name (Optional) The component of the given product that the bugs must be assigned to.
-d, --doit Without this argument, changes are not actually committed to the database.
-D, --debug Show the SQL query
=head1 DESCRIPTION
This command will move bugs matching a specific product (and optionally a component)
from one flag type id to another if the bug has the flag set to either +, -, or ?.
=head1 ATTRIBUTES
L<Bugzilla::App::Command::move_flag_types> inherits all attributes from
L<Mojolicious::Command> and implements the following new ones.
=head2 description
my $description = $move_flag_types->description;
$move_flag_types = $move_flag_types->description('Foo');
Short description of this command, used for the command list.
=head2 usage
my $usage = $move_flag_types->usage;
$move_flag_types = $move_flag_types->usage('Foo');
Usage information for this command, used for the help screen.
=head1 METHODS
L<Bugzilla::App::Command::move_flag_types> inherits all methods from
L<Mojolicious::Command> and implements the following new ones.
=head2 run
$move_flag_types->run(@ARGV);
Run this command.
# 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::App::Command::report_ping; ## no critic (Capitalization)
use Mojo::Base 'Mojolicious::Command';
use Bugzilla::Constants;
use Cwd qw(cwd);
use JSON::MaybeXS;
use Module::Runtime 'require_module';
use Mojo::File 'path';
use Mojo::Util 'getopt';
use PerlX::Maybe 'maybe';
use Try::Tiny;
has description => 'send a report ping to a URL';
has usage => sub { shift->extract_usage };
sub run {
my ($self, @args) = @_;
my $json
= JSON::MaybeXS->new(convert_blessed => 1, canonical => 1, pretty => 1);
my $class = 'Simple';
my $working_dir = cwd();
my $dbh = Bugzilla->dbh;
my (
$namespace, $doctype, $page, $rows,
$base_url, $test, $dump_schema, $dump_documents,
$since, $since_db, $current_ts
);
Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
getopt \@args,
'base-url|u=s' => \$base_url,
'page|p=i' => \$page,
'rows|r=i' => \$rows,
'since=s' => \$since,
'since-db' => \$since_db,
'dump-schema' => \$dump_schema,
'dump-documents' => \$dump_documents,
'class|c=s' => \$class,
'namespace|n=s' => \$namespace,
'doctype|d=s' => \$doctype,
'workdir|C=s' => \$working_dir,
'test' => \$test;
$base_url = 'http://localhost' if $dump_schema || $dump_documents || $test;
die $self->usage unless $base_url;
unless ($class =~ /::/) {
$class = "Bugzilla::Report::Ping::$class";
}
try {
require_module($class);
}
catch {
say "Failed to load $class.";
unless ($_ =~ /^Can't locate \S+ in \@INC/) {
say "Error: $_";
}
exit 1;
};
if ($since_db) {
$current_ts = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
$since
= $dbh->selectrow_array(
'SELECT last_ping_ts FROM report_ping WHERE class = ?',
undef, $class);
}
my $report = $class->new(
model => $dbh->model,
base_url => $base_url,
maybe rows => $rows,
maybe page => $page,
maybe namespace => $namespace,
maybe doctype => $doctype,
maybe since => $since,
);
if ($dump_schema) {
my $schema = $report->validator->schema->data;
$schema->{'$schema'} = "http://json-schema.org/draft-04/schema#";
print $json->encode($schema);
exit;
}
if ($test) {
$self->foreach_page(
$report,
'Testing',
sub {
foreach my $result (@_) {
my @error = $report->test_row($result);
if (@error) {
my $doc = $report->extract_content($result);
die $json->encode({errors => \@error, result => $doc});
}
}
}
);
}
elsif ($dump_documents) {
$self->foreach_page(
$report,
'Dumping',
sub {
foreach my $result (@_) {
my $doc = $report->extract_content($result);
my $id = $result->id;
path($working_dir, "$id.json")->spurt($json->encode($doc));
}
}
);
}
else {
$self->foreach_page(
$report,
'Sending',
sub {
Mojo::Promise->all(map { $report->send_row($_) } @_)->wait if @_;
}
);
# Store last ping timestamp if since-db option as used
if ($since_db) {
if ($since) {
$dbh->do('UPDATE report_ping SET last_ping_ts = ? WHERE class = ?',
undef, $current_ts, $class);
}
else {
$dbh->do('INSERT INTO report_ping (class, last_ping_ts) VALUES (?, ?)',
undef, $class, $current_ts);
}
}
}
}
sub foreach_page {
my ($self, $report, $label, $cb) = @_;
my $rs = $report->resultset;
foreach my $p ($report->page .. $report->pager->last_page) {
$rs = $rs->page($p);
say "$label page $p of ", $report->pager->last_page;
$cb->($rs->all);
}
}
1;
__END__
=head1 NAME
Bugzilla::App::Command::report_ping - Send a report ping to a URL';
=head1 SYNOPSIS
Usage: APPLICATION report_ping
./bugzilla.pl report_ping --base-url=http://example.com/path
Options:
-h, --help Print a brief help message and exits.
-u, --base-url URL to send the JSON documents to.
-r, --rows num (Optional) Number of requests to send at once. Default: 10.
-p, --page num (Optional) Page to start on. Default: 1
--since str (Optional) Typically the date of the last run.
--since-db (Optional) Store and retrieve the last run timestamp from the DB.
--class word (Optional) Report class to use. Default: Simple
--test Validate the JSON documents against the JSON schema.
--dump-schema Print the JSON schema.
=head1 DESCRIPTION
send a report ping to a URL.
=head1 ATTRIBUTES
L<Bugzilla::App::Command::report_ping> inherits all attributes from
L<Mojolicious::Command> and implements the following new ones.
=head2 description
my $description = $report_ping->description;
$rereport r = $re$port_ping->description('Foo');
Short description of this command, used for the command list.
=head2 usage
my $usage = $report_ping->usage;
$report_ping = $report_ping->usage('Foo');
Usage information for this command, used for the help screen.
=head1 METHODS
L<Bugzilla::App::Command::report_ping> inherits all methods from
L<Mojolicious::Command> and implements the following new ones.
=head2 run
$report_ping->run(@ARGV);
Run this command.
# 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::App::Command::revoke_api_keys; ## no critic (Capitalization)
use 5.10.1;
use Mojo::Base 'Mojolicious::Command';
use Bugzilla::Constants;
use Bugzilla::User::APIKey;
use Mojo::File 'path';
use Mojo::Util 'getopt';
use PerlX::Maybe 'maybe';
has description => 'Revoke api keys';
has usage => sub { shift->extract_usage };
sub run {
my ($self, @args) = @_;
my ($app_id, $description);
Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
getopt \@args,
'a|app-id=s' => \$app_id,
'd|description-id=s' => \$description;
die $self->usage unless $app_id || $description;
my $query = {
revoked => 0,
maybe(app_id => $app_id), maybe(description => $description)
};
my $keys = Bugzilla::User::APIKey->match($query);
foreach my $key (@$keys) {
say 'Updating ', $key->id;
$key->set_revoked(1);
$key->update();
}
}
1;
__END__
=encoding utf8
=head1 NAME
Bugzilla::App::Command::revoke_api_keys - revoke API keys command
=head1 SYNOPSIS
Usage: APPLICATION revoke_api_keys [OPTIONS]
mojo revoke_api_keys -a deadbeef
Options:
-h, --help Show this summary of available options
-a, --app-id app_id Match against a specific app_id
-d, --description desc Match against a specific description
=head1 DESCRIPTION
L<Bugzilla::App::Command::revoke_api_keys> revokes API keys.
=head1 ATTRIBUTES
L<Bugzilla::App::Command::revoke_api_keys> inherits all attributes from
L<Mojolicious::Command> and implements the following new ones.
=head2 description
my $description = $revoke_api_keys->description;
$revoke_api_keys = $revoke_api_keys->description('Foo');
Short description of this command, used for the command list.
=head2 usage
my $usage = $revoke_api_keys->usage;
$revoke_api_keys = $revoke_api_keys->usage('Foo');
Usage information for this command, used for the help screen.
=head1 METHODS
L<Bugzilla::App::Command::revoke_api_keys> inherits all methods from
L<Mojolicious::Command> and implements the following new ones.
=head2 run
$revoke_api_keys->run(@ARGV);
Run this command.
=cut
# 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::App::Main;
use Mojo::Base 'Mojolicious::Controller';
use Bugzilla::Error;
use Try::Tiny;
use Bugzilla::Constants;
sub setup_routes {
my ($class, $r) = @_;
$r->any('/')->to('Main#root');
$r->get('/testagent.cgi')->to('Main#testagent');
$r->add_type('hex32' => qr/[[:xdigit:]]{32}/);
$r->post('/announcement/hide/<checksum:hex32>')->to('Main#announcement_hide');
}
sub root {
my ($c) = @_;
$c->res->headers->cache_control('public, max-age=3600, immutable');
$c->render(handler => 'bugzilla');
}
sub testagent {
my ($self) = @_;
$self->render(text => "OK Mojolicious");
}
sub announcement_hide {
my ($self) = @_;
my $checksum = $self->param('checksum');
if ($checksum && $checksum =~ /^[[:xdigit:]]{32}$/) {
$self->session->{announcement_checksum} = $checksum;
}
$self->render(json => {});
}
1;
# 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::App::OAuth2::Clients;
use 5.10.1;
use Mojo::Base 'Mojolicious::Controller';
use List::Util qw(any first);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Token;
use Bugzilla::Util qw(generate_random_password);
sub setup_routes {
my ($class, $r) = @_;
# Manage the client list
my $client_route = $r->under(
'/admin/oauth' => sub {
my ($c) = @_;
my $user = $c->bugzilla->login(LOGIN_REQUIRED) || return undef;
$user->in_group('admin')
|| ThrowUserError('auth_failure',
{group => 'admin', action => 'edit', object => 'oauth_clients'});
return 1;
}
);
$client_route->any('/list')->to('OAuth2::Clients#list')->name('list_clients');
$client_route->any('/create')->to('OAuth2::Clients#create')
->name('create_client');
$client_route->any('/delete')->to('OAuth2::Clients#delete')
->name('delete_client');
$client_route->any('/edit')->to('OAuth2::Clients#edit')->name('edit_client');
}
# Show list of clients
sub list {
my ($self) = @_;
my $clients = Bugzilla->dbh->selectall_arrayref('SELECT * FROM oauth2_client',
{Slice => {}});
$self->stash(clients => $clients);
return $self->render(template => 'admin/oauth/list', handler => 'bugzilla');
}
# Create new client
sub create {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
my $vars = {};
if ($self->req->method ne 'POST') {
$vars->{id} = generate_random_password(20);
$vars->{secret} = generate_random_password(40);
$vars->{token} = issue_session_token('create_oauth_client');
$vars->{scopes}
= $dbh->selectall_arrayref('SELECT * FROM oauth2_scope', {Slice => {}});
$self->stash(%{$vars});
return $self->render(template => 'admin/oauth/create', handler => 'bugzilla');
}
$dbh->bz_start_transaction;
my $description = $self->param('description');
my $id = $self->param('id');
my $secret = $self->param('secret');
my @scopes = $self->param('scopes');
$description or ThrowCodeError('param_required', {param => 'description'});
$id or ThrowCodeError('param_required', {param => 'id'});
$secret or ThrowCodeError('param_required', {param => 'secret'});
any { $_ > 0 } @scopes or ThrowCodeError('param_required', {param => 'scopes'});
my $token = $self->param('token');
check_token_data($token, 'create_oauth_client');
$dbh->do('INSERT INTO oauth2_client (client_id, description, secret) VALUES (?, ?, ?)',
undef, $id, $description, $secret);
my $client_data
= $dbh->selectrow_hashref('SELECT * FROM oauth2_client WHERE client_id = ?',
undef, $id);
foreach my $scope_id (@scopes) {
$scope_id = $dbh->selectrow_array('SELECT id FROM oauth2_scope WHERE id = ?',
undef, $scope_id);
if (!$scope_id) {
ThrowCodeError('param_required', {param => 'scopes'});
}
$dbh->do(
'INSERT INTO oauth2_client_scope (client_id, scope_id) VALUES (?, ?)',
undef, $client_data->{id}, $scope_id
);
}
delete_token($token);
my $clients
= $dbh->selectall_arrayref('SELECT * FROM oauth2_client', {Slice => {}});
$dbh->bz_commit_transaction;
$vars->{'message'} = 'oauth_client_created';
$vars->{'client'} = {description => $description};
$vars->{'clients'} = $clients;
$self->stash(%{$vars});
return $self->render(template => 'admin/oauth/list', handler => 'bugzilla');
}
# Delete client
sub delete {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
my $vars = {};
my $id = $self->param('id');
my $client_data = $dbh->selectrow_hashref('SELECT * FROM oauth2_client WHERE id = ?',
undef, $id);
if (!$self->param('deleteme')) {
$vars->{'client'} = $client_data;
$vars->{'token'} = issue_session_token('delete_oauth_client');
$self->stash(%{$vars});
return $self->render(
template => 'admin/oauth/confirm-delete',
handler => 'bugzilla'
);
}
$dbh->bz_start_transaction;
my $token = $self->param('token');
check_token_data($token, 'delete_oauth_client');
$dbh->do('DELETE FROM oauth2_client WHERE id = ?', undef, $id);
delete_token($token);
my $clients
= $dbh->selectall_arrayref('SELECT * FROM oauth2_client', {Slice => {}});
$dbh->bz_commit_transaction;
$vars->{'message'} = 'oauth_client_deleted';
$vars->{'client'} = {description => $client_data->{description}};
$vars->{'clients'} = $clients;
$self->stash(%{$vars});
return $self->render(template => 'admin/oauth/list', handler => 'bugzilla');
}
# Edit client
sub edit {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
my $vars = {};
my $id = $self->param('id');
my $client_data = $dbh->selectrow_hashref('SELECT * FROM oauth2_client WHERE id = ?',
undef, $id);
my $client_scopes
= $dbh->selectall_arrayref(
'SELECT scope_id FROM oauth2_client_scope WHERE client_id = ?',
undef, $client_data->{id});
$client_data->{scopes} = [map { $_->[0] } @{$client_scopes}];
$vars->{client} = $client_data;
# All scopes
my $all_scopes
= $dbh->selectall_arrayref('SELECT * FROM oauth2_scope', {Slice => {}});
$vars->{scopes} = $all_scopes;
if ($self->req->method ne 'POST') {
$vars->{token} = issue_session_token('edit_oauth_client');
$self->stash(%{$vars});
return $self->render(template => 'admin/oauth/edit', handler => 'bugzilla');
}
$dbh->bz_start_transaction;
my $token = $self->param('token');
check_token_data($token, 'edit_oauth_client');
my $description = $self->param('description');
my $active = $self->param('active');
my @scopes = $self->param('scopes');
if ($description ne $client_data->{description}) {
$dbh->do('UPDATE oauth2_client SET description = ? WHERE id = ?',
undef, $description, $id);
}
if ($active ne $client_data->{active}) {
$dbh->do('UPDATE oauth2_client SET active = ? WHERE id = ?',
undef, $active, $id);
}
$dbh->do('DELETE FROM oauth2_client_scope WHERE client_id = ?', undef, $id);
foreach my $scope_id (@scopes) {
$dbh->do(
'INSERT INTO oauth2_client_scope (client_id, scope_id) VALUES (?, ?)',
undef, $client_data->{id}, $scope_id
);
}
delete_token($token);
my $clients
= $dbh->selectall_arrayref('SELECT * FROM oauth2_client', {Slice => {}});
$dbh->bz_commit_transaction;
$vars->{'message'} = 'oauth_client_updated';
$vars->{'client'} = {description => $description};
$vars->{'clients'} = $clients;
$self->stash(%{$vars});
return $self->render(template => 'admin/oauth/list', handler => 'bugzilla');
}
1;
package Bugzilla::App::Plugin::BlockIP;
use 5.10.1;
use Mojo::Base 'Mojolicious::Plugin';
use Bugzilla::Memcached;
use constant BLOCK_TIMEOUT => 60 * 60;
my $MEMCACHED = Bugzilla::Memcached->new()->{memcached};
my $BLOCKED_HTML = "";
sub register {
my ($self, $app, $conf) = @_;
$app->hook(before_routes => \&_before_routes);
$app->helper(block_ip => \&_block_ip);
$app->helper(unblock_ip => \&_unblock_ip);
$app->hook(
before_server_start => sub {
my $template = Bugzilla::Template->create();
$template->process('global/ip-blocked.html.tmpl',
{block_timeout => BLOCK_TIMEOUT},
\$BLOCKED_HTML);
undef $template;
utf8::encode($BLOCKED_HTML);
}
);
}
sub _block_ip {
my ($class, $ip) = @_;
$MEMCACHED->set("block_ip:$ip" => 1, BLOCK_TIMEOUT) if $MEMCACHED;
}
sub _unblock_ip {
my ($class, $ip) = @_;
$MEMCACHED->delete("block_ip:$ip") if $MEMCACHED;
}
sub _before_routes {
my ($c) = @_;
return if $c->stash->{'mojo.static'};
my $ip = $c->tx->remote_address;
if ($MEMCACHED && $MEMCACHED->get("block_ip:$ip")) {
$c->block_ip($ip);
$c->res->code(429);
$c->res->message('Too Many Requests');
$c->write($BLOCKED_HTML);
$c->finish;
}
}
1;
# 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::App::Plugin::Glue;
use 5.10.1;
use Mojo::Base 'Mojolicious::Plugin';
use Try::Tiny;
use Bugzilla::Constants;
use Bugzilla::Logging;
use Bugzilla::RNG ();
use Bugzilla::Util qw(with_writable_database);
use Mojo::Util qw(secure_compare);
use Mojo::JSON qw(decode_json);
use Scalar::Util qw(blessed);
use Scope::Guard;
our $cleanup_guard;
sub register {
my ($self, $app, $conf) = @_;
my %D;
if ($ENV{BUGZILLA_HTTPD_ARGS}) {
my $args = decode_json($ENV{BUGZILLA_HTTPD_ARGS});
foreach my $arg (@$args) {
if ($arg =~ /^-D(\w+)$/) {
$D{$1} = 1;
}
else {
die "Unknown httpd arg: $arg";
}
}
}
$app->hook(
around_dispatch => sub {
my ($next, $c) = @_;
Log::Log4perl::MDC->put(request_id => $c->req->request_id);
# Below we localize a package scoped variable, and put a scope guard in it
# this means the cleanup routine will be called when this around_dispatch
# hook returns. We do this to avoid having to handle any exceptions.
# Think of this as like a "defer cleanup()" in the Go language.
local $cleanup_guard = Scope::Guard->new(\&Bugzilla::cleanup);
# Ensure the request_cache is always cleared prior to every request,
# regardless of routing or Bugzilla::App wrapping.
# This is not an expensive operation.
Bugzilla->clear_request_cache();
# We also need to clear CGI's globals.
CGI::initialize_globals();
Bugzilla->usage_mode(USAGE_MODE_MOJO);
# This is used by Bugzilla::Util::remote_ip().
state $better_xff = Bugzilla->has_feature('better_xff');
Bugzilla->request_cache->{remote_ip} = $better_xff ? $c->forwarded_for : $c->tx->remote_address;
$next->();
}
);
$app->secrets([Bugzilla->localconfig->site_wide_secret]);
$app->renderer->add_handler(
'bugzilla' => sub {
my ($renderer, $c, $output, $options) = @_;
my %params;
# Helpers
foreach my $method (grep {m/^\w+\z/} keys %{$renderer->helpers}) {
my $sub = $renderer->helpers->{$method};
$params{$method} = sub { $c->$sub(@_) };
}
# Stash values
$params{$_} = $c->stash->{$_} for grep {m/^\w+\z/} keys %{$c->stash};
$params{self} = $params{c} = $c;
my $name = sprintf '%s.%s.tmpl', $options->{template}, $options->{format};
my $template = Bugzilla->template;
if ($options->{variant}) {
my $name_variant = sprintf '%s.%s+%s.tmpl', $options->{template}, $options->{format}, $options->{variant};
WARN("loading $name_variant");
my $rendered = $template->process($name_variant, \%params, $output);
return if $rendered;
my $error = $template->error;
die $error unless $error->type eq 'file' && $error->info =~ /not found/;
}
WARN("loading $name");
$template->process($name, \%params, $output) or die $template->error;
}
);
$app->helper(
'bugzilla.login_redirect_if_required' => sub {
my ($c, $type) = @_;
if ($type == LOGIN_REQUIRED) {
$c->redirect_to(Bugzilla->localconfig->basepath . 'login');
return undef;
}
else {
return Bugzilla->user;
}
}
);
$app->helper(
'bugzilla.login' => sub {
my ($c, $type) = @_;
$type //= LOGIN_NORMAL;
return Bugzilla->user if Bugzilla->user->id;
$type = LOGIN_REQUIRED
if $c->param('GoAheadAndLogIn') || Bugzilla->params->{requirelogin};
# Allow templates to know that we're in a page that always requires
# login.
if ($type == LOGIN_REQUIRED) {
Bugzilla->request_cache->{page_requires_login} = 1;
}
my $login_cookie = $c->cookie("Bugzilla_logincookie");
my $user_id = $c->cookie("Bugzilla_login");
return $c->bugzilla->login_redirect_if_required($type)
unless ($login_cookie && $user_id);
my $db_cookie = Bugzilla->dbh->selectrow_array(
'SELECT cookie FROM logincookies WHERE cookie = ? AND userid = ?',
undef, ($login_cookie, $user_id)
);
if (defined $db_cookie && secure_compare($login_cookie, $db_cookie)) {
my $user = Bugzilla::User->check({id => $user_id, cache => 1});
# If we logged in successfully, then update the lastused
# time on the login cookie
with_writable_database {
Bugzilla->dbh->do(
q{ UPDATE logincookies SET lastused = NOW() WHERE cookie = ? },
undef, $login_cookie);
};
Bugzilla->set_user($user);
return $user;
}
else {
return $c->bugzilla->login_redirect_if_required($type);
}
}
);
$app->helper(
'bugzilla.error_page' => sub {
my ($c, $error) = @_;
if (blessed $error && $error->isa('Bugzilla::Error::Base')) {
$c->render(
handler => 'bugzilla',
template => $error->template,
error => $error->message,
%{$error->vars}
);
}
else {
$c->reply->exception($error);
}
}
);
$app->helper(
'url_is_attachment_base' => sub {
my ($c, $id) = @_;
return 0 unless Bugzilla::Util::use_attachbase();
my $attach_base = Bugzilla->localconfig->attachment_base;
# If we're passed an id, we only want one specific attachment base
# for a particular bug. If we're not passed an ID, we just want to
# know if our current URL matches the attachment_base *pattern*.
my $regex;
if ($id) {
$attach_base =~ s/\%bugid\%/$id/;
$regex = quotemeta($attach_base);
}
else {
# In this circumstance we run quotemeta first because we need to
# insert an active regex meta-character afterward.
$regex = quotemeta($attach_base);
$regex =~ s/\\\%bugid\\\%/\\d+/;
}
$regex = "^$regex";
return ($c->req->url->to_abs =~ $regex) ? 1 : 0;
}
);
$app->helper(
'content_security_policy' => sub {
my ($c, %add_params) = @_;
my $stash = $c->stash;
if (%add_params || !$stash->{Bugzilla_csp}) {
my %params = DEFAULT_CSP();
delete $params{report_only} if %add_params && !$add_params{report_only};
delete $params{report_only} if !$c->isa('Bugzilla::App::CGI');
foreach my $key (keys %add_params) {
if (defined $add_params{$key}) {
$params{$key} = $add_params{$key};
}
else {
delete $params{$key};
}
}
$stash->{Bugzilla_csp} = Bugzilla::CGI::ContentSecurityPolicy->new(%params);
}
# force the creation of the value, and thus the nonce (if it is used)
$stash->{Bugzilla_csp}->value;
return $stash->{Bugzilla_csp};
}
);
$app->helper(
'csp_nonce' => sub {
my ($c) = @_;
my $csp = $c->content_security_policy;
return $csp->has_nonce ? $csp->nonce : '';
}
);
$app->helper(
'bz_include' => sub {
my ($self, $file, %vars) = @_;
my $template = Bugzilla->template;
my $buffer = "";
$template->process($file, \%vars, \$buffer)
or die $template->error;
return Mojo::ByteStream->new($buffer);
}
);
}
1;
# 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::App::Plugin::Helpers;
use 5.10.1;
use Mojo::Base qw(Mojolicious::Plugin);
use Bugzilla::Logging;
use Carp;
sub register {
my ($self, $app, $conf) = @_;
$app->helper(
basic_auth => sub {
my ($c, $realm, $auth_user, $auth_pass) = @_;
my $req = $c->req;
my ($user, $password) = $req->url->to_abs->userinfo =~ /^([^:]+):(.*)/;
unless ($realm && $auth_user && $auth_pass) {
croak 'basic_auth() called with missing parameters.';
}
unless ($user eq $auth_user && $password eq $auth_pass) {
WARN('username and password do not match');
$c->res->headers->www_authenticate("Basic realm=\"$realm\"");
$c->res->code(401);
$c->rendered;
return 0;
}
return 1;
}
);
$app->routes->add_shortcut(
static_file => sub {
my ($r, $path, $option) = @_;
my $file = $option->{file};
my $content_type = $option->{content_type} // 'text/plain';
unless ($file) {
$file = $path;
$file =~ s!^/!!;
}
return $r->get(
$path => sub {
my ($c) = @_;
$c->res->headers->content_type($content_type);
$c->reply->file($c->app->home->child($file));
}
);
}
);
$app->routes->add_shortcut(
page => sub {
my ($r, $path, $id) = @_;
return $r->any($path)->to('CGI#page_cgi' => {id => $id});
}
);
}
1;
package Bugzilla::App::Plugin::Hostage;
use 5.10.1;
use Mojo::Base 'Mojolicious::Plugin';
use Bugzilla::Logging;
my %HEALTH_CHECK_UA = ("GoogleHC/1.0" => 1,);
sub _attachment_root {
my ($base) = @_;
return undef unless $base;
return $base =~ m{^https?://(?:bug)?\%bugid\%\.([a-zA-Z\.-]+)} ? $1 : undef;
}
sub _attachment_host_regex {
my ($base) = @_;
return undef unless $base;
my $val = $base;
$val =~ s{^https?://}{}s;
$val =~ s{/$}{}s;
my $regex = quotemeta $val;
$regex =~ s/\\\%bugid\\\%/\\d+/g;
return qr/^$regex$/s;
}
sub register {
my ($self, $app, $conf) = @_;
$app->hook(before_routes => \&_before_routes);
}
sub _before_routes {
my ($c) = @_;
state $urlbase = Bugzilla->localconfig->urlbase;
state $urlbase_uri = URI->new($urlbase);
state $urlbase_host = $urlbase_uri->host;
state $urlbase_host_regex = qr/^bug(\d+)\.\Q$urlbase_host\E$/;
state $attachment_base = Bugzilla->localconfig->attachment_base;
state $attachment_root = _attachment_root($attachment_base);
state $attachment_host_regex = _attachment_host_regex($attachment_base);
state $urlbase_is_https = $urlbase =~ /^https/;
state $behind_proxy = Bugzilla->localconfig->inbound_proxies;
my $stash = $c->stash;
my $req = $c->req;
my $url = $req->url->to_abs;
my $hostname = $url->host;
my $ua = $req->headers->user_agent;
if ($ua && $HEALTH_CHECK_UA{$ua}) {
$c->render(text => "Hello, $ua, I am healthy.", status => 200);
return;
}
return if $stash->{'mojo.static'};
if ($urlbase_is_https && $behind_proxy && !$req->is_secure) {
my $error = "Reverse Proxy is Misconfigured";
$c->render(text => $error, status => 500);
unless ($ENV{MOJO_REVERSE_PROXY}) {
ERROR(
"$error: The environmental variable MOJO_REVERSE_PROXY should be set if operating Bugzilla behind a reverse proxy"
);
}
else {
ERROR(
"$error: Check that X-Forwarded-Proto and similar headers are being sent.");
}
return;
}
return if $hostname eq $urlbase_host;
my $path = $url->path;
return if $path eq '/__lbheartbeat__';
if ($attachment_base && $hostname eq $attachment_root) {
DEBUG("redirecting to $urlbase because $hostname is $attachment_root");
$c->redirect_to($urlbase);
return;
}
elsif ($attachment_base && $hostname =~ $attachment_host_regex) {
if ($path =~ m{^/attachment\.cgi}s) {
return;
}
else {
my $new_uri = $url->clone;
$new_uri->scheme($urlbase_uri->scheme);
$new_uri->host($urlbase_host);
DEBUG("redirecting to $new_uri because $hostname matches attachment regex");
$c->redirect_to($new_uri);
return;
}
}
elsif (my ($id) = $hostname =~ $urlbase_host_regex) {
my $new_uri = $urlbase_uri->clone;
$new_uri->path('/show_bug.cgi');
$new_uri->query_form(id => $id);
DEBUG("redirecting to $new_uri because $hostname includes bug id");
$c->redirect_to($new_uri);
return;
}
else {
DEBUG("redirecting to $urlbase because $hostname doesn't make sense");
$c->redirect_to($urlbase);
return;
}
}
1;
# 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::App::Plugin::SizeLimit;
use 5.10.1;
use Mojo::Base 'Mojolicious::Plugin';
use Mojo::JSON qw(decode_json);
use Bugzilla::Logging;
use constant MIN_SIZE_LIMIT => 750_000;
use constant HAVE_LINUX_SMAPS_TINY => eval { require Linux::Smaps::Tiny };
use constant HAVE_BSD_RESOURCE => eval { require BSD::Resource };
BEGIN {
if (HAVE_LINUX_SMAPS_TINY) {
Linux::Smaps::Tiny->import('get_smaps_summary');
}
if (HAVE_BSD_RESOURCE) {
BSD::Resource->import;
}
}
my @RESOURCES = qw(
RLIMIT_CPU RLIMIT_FSIZE RLIMIT_DATA RLIMIT_STACK RLIMIT_CORE RLIMIT_RSS RLIMIT_MEMLOCK RLIMIT_NPROC RLIMIT_NOFILE
RLIMIT_OFILE RLIMIT_OPEN_MAX RLIMIT_LOCKS RLIMIT_AS RLIMIT_VMEM RLIMIT_PTHREAD RLIMIT_TCACHE RLIMIT_AIO_MEM
RLIMIT_AIO_OPS RLIMIT_FREEMEM RLIMIT_NTHR RLIMIT_NPTS RLIMIT_RSESTACK RLIMIT_SBSIZE RLIMIT_SWAP RLIMIT_MSGQUEUE
RLIMIT_RTPRIO RLIMIT_RTTIME RLIMIT_SIGPENDING
);
my %RESOURCE;
if (HAVE_BSD_RESOURCE) {
$RESOURCE{$_} = eval $_ for @RESOURCES;
}
sub register {
my ($self, $app, $conf) = @_;
if (HAVE_BSD_RESOURCE) {
my $setrlimit = decode_json(Bugzilla->localconfig->setrlimit);
# This trick means the master process will not a size limit.
Mojo::IOLoop->next_tick(sub {
foreach my $resource (keys %$setrlimit) {
setrlimit(
$RESOURCE{$resource},
$setrlimit->{$resource},
$setrlimit->{$resource}
);
}
});
}
if (HAVE_LINUX_SMAPS_TINY) {
my $size_limit = Bugzilla->localconfig->size_limit;
return unless $size_limit;
if ($size_limit < MIN_SIZE_LIMIT) {
WARN(sprintf "size_limit cannot be smaller than %d", MIN_SIZE_LIMIT);
$size_limit = MIN_SIZE_LIMIT;
}
$app->hook(
after_dispatch => sub {
my $c = shift;
my $summary = get_smaps_summary();
if ($summary->{Size} >= $size_limit) {
my $diff = $summary->{Size} - $size_limit;
INFO("memory size exceeded $size_limit by $diff ($summary->{Size})");
$c->res->headers->connection('close');
Mojo::IOLoop->singleton->stop_gracefully;
}
}
);
}
}
1;
package Bugzilla::App::SES;
# 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.
use 5.10.1;
use Mojo::Base qw( Mojolicious::Controller );
use Bugzilla::Constants qw(BOUNCE_COUNT_MAX ERROR_MODE_DIE);
use Bugzilla::Logging;
use Bugzilla::Mailer qw(MessageToMTA);
use Bugzilla::User ();
use Bugzilla::Util qw(html_quote remote_ip);
use JSON::MaybeXS qw(decode_json);
use LWP::UserAgent ();
use Try::Tiny qw(catch try);
use Type::Library -base, -declare => qw(
Self
Notification NotificationType TypeField
BounceNotification BouncedRecipients
ComplaintNotification ComplainedRecipients
);
use Type::Utils -all;
use Types::Standard -all;
use Type::Params qw(compile);
class_type Self, {class => __PACKAGE__};
declare ComplainedRecipients,
as ArrayRef [Dict [emailAddress => Str, slurpy Any]];
declare ComplaintNotification,
as Dict [
complaint => Dict [
complainedRecipients => ComplainedRecipients,
complaintFeedbackType => Str,
slurpy Any,
],
slurpy Any,
];
declare BouncedRecipients,
as ArrayRef [
Dict [
emailAddress => Str,
action => Optional [Str],
diagnosticCode => Optional [Str],
status => Optional [Str],
slurpy Any,
],
];
declare BounceNotification,
as Dict [
bounce => Dict [
bouncedRecipients => BouncedRecipients,
reportingMTA => Optional [Str],
bounceSubType => Str,
bounceType => Str,
slurpy Any,
],
slurpy Any,
];
declare NotificationType, as Enum [qw( Bounce Complaint )];
declare TypeField, as Enum [qw(eventType notificationType)];
declare Notification,
as Dict [
eventType => Optional [NotificationType],
notificationType => Optional [NotificationType],
slurpy Any,
];
sub setup_routes {
my ($class, $r) = @_;
my $ses_auth = $r->under(
'/ses' => sub {
my ($c) = @_;
my $lc = Bugzilla->localconfig;
return $c->basic_auth('SES', $lc->{ses_username}, $lc->{ses_password});
}
);
$ses_auth->any('/index.cgi')->to('SES#main');
}
sub main {
my ($self) = @_;
try {
$self->_main;
}
catch {
FATAL("Error in SES Handler: ", $_);
$self->_respond(400 => 'Bad Request');
};
}
sub _main {
my ($self) = @_;
Bugzilla->error_mode(ERROR_MODE_DIE);
my $message = $self->_decode_json_wrapper($self->req->body) // return;
my $message_type = $self->req->headers->header('X-Amz-SNS-Message-Type')
// '(missing)';
if ($message_type eq 'SubscriptionConfirmation') {
$self->_confirm_subscription($message);
}
elsif ($message_type eq 'Notification') {
my $notification = $self->_decode_json_wrapper($message->{Message}) // return;
unless (
# https://docs.aws.amazon.com/ses/latest/DeveloperGuide/event-publishing-retrieving-sns-contents.html
$self->_handle_notification($notification, 'eventType')
# https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html
|| $self->_handle_notification($notification, 'notificationType')
)
{
WARN('Failed to find notification type');
$self->_respond(400 => 'Bad Request');
}
}
else {
WARN("Unsupported message-type: $message_type");
$self->_respond(200 => 'OK');
}
}
sub _confirm_subscription {
state $check = compile(Self, Dict [SubscribeURL => Str, slurpy Any]);
my ($self, $message) = $check->(@_);
my $subscribe_url = $message->{SubscribeURL};
if (!$subscribe_url) {
WARN('Bad SubscriptionConfirmation request: missing SubscribeURL');
$self->_respond(400 => 'Bad Request');
return;
}
my $ua = ua();
my $res = $ua->get($message->{SubscribeURL});
if (!$res->is_success) {
WARN('Bad response from SubscribeURL: ' . $res->status_line);
$self->_respond(400 => 'Bad Request');
return;
}
$self->_respond(200 => 'OK');
}
sub _handle_notification {
state $check = compile(Self, Notification, TypeField);
my ($self, $notification, $type_field) = $check->(@_);
if (!exists $notification->{$type_field}) {
return 0;
}
my $type = $notification->{$type_field};
if ($type eq 'Bounce') {
$self->_process_bounce($notification);
}
elsif ($type eq 'Complaint') {
$self->_process_complaint($notification);
}
else {
WARN("Unsupported notification-type: $type");
$self->_respond(200 => 'OK');
}
return 1;
}
sub _process_bounce {
state $check = compile(Self, BounceNotification);
my ($self, $notification) = $check->(@_);
# disable each account that is bouncing
foreach my $recipient (@{$notification->{bounce}->{bouncedRecipients}}) {
my $address = $recipient->{emailAddress};
my $reason = sprintf '(%s) %s', $recipient->{action} // 'error',
$recipient->{diagnosticCode} // 'unknown';
my $user = Bugzilla::User->new({name => $address, cache => 1});
if ($user) {
# never auto-disable admin accounts
if ($user->in_group('admin')) {
Bugzilla->audit("ignoring bounce for admin <$address>: $reason");
}
else {
my $template = Bugzilla->template_inner();
my $vars = {
mta => $notification->{bounce}->{reportingMTA} // 'unknown',
reason => $reason,
};
my $bounce_message;
$template->process('admin/users/bounce-disabled.txt.tmpl',
$vars, \$bounce_message)
|| die $template->error();
# Increment bounce count for user
my $bounce_count = $user->bounce_count + 1;
# If user has not had a bounce in less than 30 days, set the bounce count to 1 instead
my $dbh = Bugzilla->dbh;
my ($has_recent_bounce) = $dbh->selectrow_array(
"SELECT 1 FROM audit_log WHERE object_id = ? AND class = 'Bugzilla::User' AND field = 'bounce_message' AND ("
. $dbh->sql_date_math('at_time', '+', 30, 'DAY')
. ") > NOW()",
undef, $user->id
);
$bounce_count = 1 if !$has_recent_bounce;
$user->set_disable_mail(1);
$user->set_bounce_count($bounce_count);
# if we hit the max amount, go ahead and disabled the account
# and an admin will need to reactivate the account.
if ($bounce_count == BOUNCE_COUNT_MAX) {
$user->set_disabledtext($bounce_message);
}
$user->update();
# Do this outside of Object.pm as we do not want to
# store the messages anywhere else.
$dbh->do(
"INSERT INTO audit_log (user_id, class, object_id, field, added, at_time)
VALUES (?, 'Bugzilla::User', ?, 'bounce_message', ?, LOCALTIMESTAMP(0))",
undef, $user->id, $user->id, $bounce_message
);
Bugzilla->audit(
"bounce for <$address> disabled email for userid-" . $user->id . ": $reason");
}
}
else {
Bugzilla->audit("bounce for <$address> has no user: $reason");
}
}
$self->_respond(200 => 'OK');
}
sub _process_complaint {
state $check = compile(Self, ComplaintNotification);
my ($self, $notification) = $check->(@_);
my $template = Bugzilla->template_inner();
my $json = JSON::MaybeXS->new(pretty => 1, utf8 => 1, canonical => 1,);
foreach my $recipient (@{$notification->{complaint}->{complainedRecipients}}) {
my $reason = $notification->{complaint}->{complaintFeedbackType} // 'unknown';
my $address = $recipient->{emailAddress};
Bugzilla->audit("complaint for <$address> for '$reason'");
my $vars = {
email => $address,
user => Bugzilla::User->new({name => $address, cache => 1}),
reason => $reason,
notification => $json->encode($notification),
};
my $message;
$template->process('email/ses-complaint.txt.tmpl', $vars, \$message)
|| die $template->error();
MessageToMTA($message);
}
$self->_respond(200 => 'OK');
}
sub _respond {
my ($self, $code, $message) = @_;
$self->render(text => "$message\n", status => $code);
}
sub _decode_json_wrapper {
state $check = compile(Self, Str);
my ($self, $json) = $check->(@_);
my $result;
my $ok = try {
$result = decode_json($json);
}
catch {
WARN('Malformed JSON from ' . $self->tx->remote_address);
$self->_respond(400 => 'Bad Request');
return undef;
};
return $ok ? $result : undef;
}
sub ua {
my $ua = LWP::UserAgent->new();
$ua->timeout(10);
$ua->protocols_allowed(['http', 'https']);
if (my $proxy_url = Bugzilla->params->{'proxy_url'}) {
$ua->proxy(['http', 'https'], $proxy_url);
}
else {
$ua->env_proxy;
}
return $ua;
}
1;
# 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::App::Static;
use Mojo::Base 'Mojolicious::Static';
use Bugzilla::Constants qw(bz_locations);
my $LEGACY_RE = qr{
^ (?:static/v(?<version>[0-9]+\.[0-9]+)/) ?
(?<file>(?:extensions/[^/]+/web|(?:image|skin|j|graph)s|data/webdot)/.+)
$
}xs;
sub serve {
my ($self, $c, $rel) = @_;
if ($rel =~ $LEGACY_RE) {
local $self->{paths} = [bz_locations->{cgi_path}];
my $version = $+{version};
my $file = $+{file};
$c->stash->{static_file_version} = $version;
return $self->SUPER::serve($c, $file);
}
else {
return $self->SUPER::serve($c, $rel);
}
}
1;
# 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::App::Stdout;
use 5.10.1;
use Moo;
use Bugzilla::Logging;
use Encode;
use English qw(-no_match_vars);
has 'controller' => (is => 'ro', required => 1,);
has '_encoding' => (is => 'rw', default => '',);
sub TIEHANDLE { ## no critic (unpack)
my $class = shift;
return $class->new(@_);
}
sub PRINTF { ## no critic (unpack)
my $self = shift;
$self->PRINT(sprintf @_);
}
sub PRINT { ## no critic (unpack)
my $self = shift;
my $c = $self->controller;
my $bytes = join '', @_;
return unless $bytes;
if ($self->_encoding) {
$bytes = encode($self->_encoding, $bytes);
}
$c->write($bytes . ($OUTPUT_RECORD_SEPARATOR // ''));
}
sub BINMODE {
my ($self, $mode) = @_;
if ($mode) {
if ($mode eq ':bytes' or $mode eq ':raw') {
$self->_encoding('');
}
elsif ($mode eq ':utf8') {
$self->_encoding('utf8');
}
}
}
1;
# 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::App::Users;
use Mojo::Base 'Mojolicious::Controller';
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Logging;
use Bugzilla::Mailer qw(MessageToMTA);
use Date::Format qw(ctime);
use Scalar::Util qw(blessed);
use Bugzilla::User;
use List::Util qw(any);
use Try::Tiny;
sub setup_routes {
my ($class, $r) = @_;
$r->post('/signup/email')->to('Users#signup_email')->name('signup_email');
$r->get('/signup/email/:token/verify')->to('Users#signup_email_verify')
->name('signup_email_verify');
$r->post('/signup/email/:token/finish')->to('Users#signup_email_finish')
->name('signup_email_finish');
}
sub signup_email {
my ($c) = @_;
my $v = $c->validation;
try {
Bugzilla::User->new->check_account_creation_enabled;
my $email_regexp = Bugzilla->params->{createemailregexp};
$v->required('email')->like(qr/$email_regexp/);
$v->csrf_protect;
ThrowUserError('account_creation_restricted') unless $v->is_valid;
my $email = $v->param('email');
Bugzilla::User->check_login_name_for_creation($email);
Bugzilla::Hook::process("user_verify_login", {login => $email});
$c->issue_new_user_account_token($email);
$c->render(handler => 'bugzilla');
}
catch {
$c->bugzilla->error_page($_);
};
}
sub signup_email_verify {
my ($c) = @_;
my $token = $c->stash->{token};
my (undef, $issuedate, $email) = Bugzilla::Token::GetTokenData($token);
if ($email) {
$c->stash->{signup_token} = $token;
$c->stash->{email} = $email;
$c->stash->{expires} = $issuedate;
}
else {
$c->stash->{missing_token} = 1;
}
$c->render(handler => 'bugzilla');
}
sub signup_email_finish {
my ($c) = @_;
my $v = $c->validation;
try {
$v->optional('create')->equal_to('create');
$v->optional('cancel')->equal_to('cancel');
$v->csrf_protect;
$v->required('signup_token')->size(22);
my $token = $v->param('signup_token');
if ($v->is_valid) {
my (undef, undef, $email) = Bugzilla::Token::GetTokenData($token);
$v->error('signup_token', ['invalid_token']) unless $email;
if ($v->is_valid && $v->param('create') eq 'create') {
$v->optional('realname')->size(1, 255);
$v->required('etiquette');
$v->required('password')->size(8, 100);
$v->required('password_confirm')->size(8, 100);
if ($v->is_valid && $v->param('password') ne $v->param('password_confirm')) {
$v->error('password_confirm', ['password_mismatch']);
$v->error('password', ['password_mismatch']);
}
if ($v->is_valid) {
my $new_user = Bugzilla::User->create({
login_name => $email,
realname => $v->param('realname'),
cryptpassword => $v->param('password'),
});
$c->persist_login($new_user, 'signup');
$c->redirect_to('/home');
}
}
elsif ($v->is_valid && $v->param('cancel') eq 'cancel') {
my (undef, undef, $email) = Bugzilla::Token::GetTokenData($token);
my $vars = {};
$vars->{'message'} = 'account_creation_canceled';
$vars->{'account'} = $email;
Bugzilla::Token::Cancel($token, $vars->{'message'});
}
}
ThrowUserError('validation', { v => $v });
}
catch {
$c->bugzilla->error_page($_);
};
}
# This is adapted from issue_new_user_account_token from Bugzilla/Token.pm
# Creates and sends a token to create a new user account.
# It assumes that the login has the correct format and is not already in use.
sub issue_new_user_account_token {
my ($c, $email) = @_;
my $dbh = Bugzilla->dbh;
# Is there already a pending request for this login name? If yes, do not throw
# an error because the user may have lost their email with the token inside.
# But to prevent using this way to mailbomb an email address, make sure
# the last request is at least 10 minutes old before sending a new email.
my $pending_requests = $dbh->selectrow_array(
'SELECT COUNT(*)
FROM tokens
WHERE tokentype = ?
AND ' . $dbh->sql_istrcmp('eventdata', '?') . '
AND issuedate > '
. $dbh->sql_date_math('NOW()', '-', 10, 'MINUTE'), undef, ('signup', $email)
);
ThrowUserError('too_soon_for_new_token', {'type' => 'signup'})
if $pending_requests;
my ($token, $token_ts)
= Bugzilla::Token::_create_token(undef, 'signup', $email);
$c->stash->{email} = $email . Bugzilla->params->{'emailsuffix'};
$c->stash->{expires} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
$c->stash->{verify_url}
= $c->url_for('signup_email_verify', token => $token)->to_abs;
my $message = $c->render_to_string(
handler => 'bugzilla',
format => 'txt',
variant => 'email'
);
WARN("Email: is\n$message");
MessageToMTA($message->to_string);
}
# This is adapted from persist_login in Bugzilla/Auth/Persist/Cookie.pm
sub persist_login {
my ($c, $user, $auth_method) = @_;
my $dbh = Bugzilla->dbh;
$dbh->bz_start_transaction();
my $login_cookie
= Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie');
my $ip_addr = $c->forwarded_for;
$dbh->do(
'INSERT INTO logincookies (cookie, userid, ipaddr, lastused)
VALUES (?, ?, ?, NOW())', undef, $login_cookie, $user->id, $ip_addr
);
# Issuing a new cookie is a good time to clean up the old
# cookies.
$dbh->do("DELETE FROM logincookies WHERE lastused < "
. $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', MAX_LOGINCOOKIE_AGE, 'DAY'));
$dbh->bz_commit_transaction();
my %cookie_attr = (httponly => 1, path => '/', expires => time + 604800);
if (Bugzilla->localconfig->urlbase =~ /^https/) {
$cookie_attr{secure} = 1;
}
$c->cookie('Bugzilla_login', $user->id, \%cookie_attr);
$c->cookie('Bugzilla_logincookie', $login_cookie, \%cookie_attr);
my $securemail_groups
= Bugzilla->can('securemail_groups')
? Bugzilla->securemail_groups
: ['admin'];
if (any { $user->in_group($_) } @$securemail_groups) {
$auth_method //= 'unknown';
Bugzilla->audit(
sprintf "successful login of %s from %s using \"%s\", authenticated by %s",
$user->login, $ip_addr, $c->req->headers->user_agent // '', $auth_method);
}
return $login_cookie;
}
1;
# 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::Attachment::Archive;
use 5.10.1;
use Moo;
use Digest::SHA qw(sha256_hex);
use Carp;
use IO::File;
use constant HEADER_SIZE => 45;
use constant HEADER_FORMAT => 'ANNNH64';
has 'file' => (is => 'ro', required => 1);
has 'input_fh' => (is => 'lazy', predicate => 'has_input_fh');
has 'output_fh' => (is => 'lazy', predicate => 'has_output_fh');
has 'checksum' => (is => 'lazy', clearer => 'reset_checksum');
sub read_member {
my ($self) = @_;
my $header = $self->_read_header();
my ($type, $bug_id, $attach_id, $data_len, $hash) = unpack HEADER_FORMAT,
$header;
if ($type eq 'D') {
$self->checksum->add($header);
my $data = $self->_read_data($data_len, $hash);
return {
bug_id => $bug_id,
attach_id => $attach_id,
data_len => $data_len,
hash => $hash,
data => $data,
};
}
elsif ($type eq 'C') {
die "bad overall checksum\n" unless $hash eq $self->checksum->hexdigest;
$self->reset_checksum;
return undef;
}
else {
die "unknown member type: $type\n";
}
}
sub write_attachment {
my ($self, $attachment) = @_;
my $data = $attachment->data;
my $bug_id = $attachment->bug_id;
my $attach_id = $attachment->id;
if (defined $data && length($data) == $attachment->datasize) {
my $header = pack HEADER_FORMAT, 'D', $bug_id, $attach_id, length($data),
sha256_hex($data);
$self->checksum->add($header);
$self->output_fh->print($header, $data);
}
}
sub write_checksum {
my ($self) = @_;
my $header = pack HEADER_FORMAT, 'C', 0, 0, 0, $self->checksum->hexdigest;
$self->output_fh->print($header);
$self->reset_checksum;
$self->output_fh->flush;
}
sub _build_checksum {
my ($self) = @_;
return Digest::SHA->new(256);
}
sub _build_input_fh {
my ($self) = @_;
if ($self->has_output_fh) {
croak "I will not read and write a file at the same time";
}
my $file = $self->file;
return IO::File->new($self->file, '<:bytes') or die "cannot read $file: $!";
}
sub _build_output_fh {
my ($self) = @_;
if ($self->has_input_fh) {
croak "I will not read and write a file at the same time";
}
my $file = $self->file;
if (-e $file) {
croak "I will not overwrite a file (file $file already exists)";
}
return IO::File->new($file, '>:bytes') or die "cannot write $file: $!";
}
sub _read_header {
my ($self) = @_;
my $header = '' x HEADER_SIZE;
my $header_len = $self->input_fh->read($header, HEADER_SIZE);
if (!$header_len || $header_len != HEADER_SIZE) {
die "bad header\n";
}
return $header;
}
sub _read_data {
my ($self, $data_len, $hash) = @_;
my $data = '' x $data_len;
my $read_data_len = $self->input_fh->read($data, $data_len);
unless ($read_data_len == $data_len) {
die "bad data\n";
}
unless ($hash eq sha256_hex($data)) {
die "bad checksum:\n\t$hash\n\t" . sha226_hex($data) . "\n";
}
return $data;
}
1;
# 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::Attachment::Storage::Base;
use 5.10.1;
use Moo::Role;
use Types::Standard qw(Int);
requires qw(set_data get_data remove_data data_exists data_type);
has 'attach_id' => (
is => 'ro',
required => 1,
isa => Int
);
sub set_class {
my ($self) = @_;
if ($self->data_exists()) {
Bugzilla->dbh->do(
"UPDATE attachment_storage_class SET storage_class = ? WHERE id = ?",
undef, $self->data_type, $self->attach_id);
}
else {
Bugzilla->dbh->do(
"INSERT INTO attachment_storage_class (id, storage_class) VALUES (?, ?)",
undef, $self->attach_id, $self->data_type);
}
return $self;
}
sub remove_class {
my ($self) = @_;
Bugzilla->dbh->do("DELETE FROM attachment_storage_class WHERE id = ?",
undef, $self->attach_id);
return $self;
}
1;
# 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::Attachment::Storage::Database;
use 5.10.1;
use Moo;
with 'Bugzilla::Attachment::Storage::Base';
sub data_type { return 'database'; }
sub set_data {
my ($self, $data) = @_;
my $dbh = Bugzilla->dbh;
if ($self->data_exists()) {
my $sth
= $dbh->prepare(
"UPDATE attach_data SET thedata = ? WHERE id = ?"
);
$sth->bind_param(1, $data, $dbh->BLOB_TYPE);
$sth->bind_param(2, $self->attach_id);
$sth->execute();
}
else {
my $sth
= $dbh->prepare(
"INSERT INTO attach_data (id, thedata) VALUES (?, ?)"
);
$sth->bind_param(1, $self->attach_id);
$sth->bind_param(2, $data, $dbh->BLOB_TYPE);
$sth->execute();
}
return $self;
}
sub get_data {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
my ($data)
= $dbh->selectrow_array("SELECT thedata FROM attach_data WHERE id = ?",
undef, $self->attach_id);
return $data;
}
sub remove_data {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
$dbh->do("DELETE FROM attach_data WHERE id = ?", undef, $self->attach_id);
return $self;
}
sub data_exists {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
my ($exists) = $dbh->selectrow_array("SELECT 1 FROM attach_data WHERE id = ?",
undef, $self->attach_id);
return !!$exists;
}
1;
# 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::Attachment::Storage::FileSystem;
use 5.10.1;
use Moo;
use Bugzilla::Constants qw(bz_locations);
with 'Bugzilla::Attachment::Storage::Base';
sub data_type { return 'filesystem'; }
sub set_data {
my ($self, $data) = @_;
my $path = $self->_local_path();
mkdir $path, 0770 unless -d $path;
open my $fh, '>', $self->_local_file();
binmode $fh;
print $fh $data;
close $fh;
return $self;
}
sub get_data {
my ($self) = @_;
if (open my $fh, '<', $self->_local_file()) {
local $/;
binmode $fh;
my $data = <$fh>;
close $fh;
return $data;
}
}
sub remove_data {
my ($self) = @_;
unlink $self->_local_file();
return $self;
}
sub data_exists {
my ($self) = @_;
return -e $self->_local_file();
}
sub _local_path {
my ($self) = @_;
my $hash = sprintf 'group.%03d', $self->attach_id % 1000;
return bz_locations()->{attachdir} . '/' . $hash;
}
sub _local_file {
my ($self) = @_;
return $self->_local_path() . '/attachment.' . $self->attach_id;
}
1;
# 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::Attachment::Storage::S3;
use 5.10.1;
use Moo;
use Bugzilla::Error;
use Bugzilla::S3;
use Types::Standard qw(Int);
with 'Bugzilla::Attachment::Storage::Base';
has 's3' => (is => 'lazy');
has 'bucket' => (is => 'lazy');
has 'datasize' => (
is => 'ro',
required => 1,
isa => Int
);
sub _build_s3 {
my $self = shift;
$self->{s3} ||= Bugzilla::S3->new({
aws_access_key_id => Bugzilla->params->{aws_access_key_id},
aws_secret_access_key => Bugzilla->params->{aws_secret_access_key},
secure => 1,
retry => 1,
});
return $self->{s3};
}
sub _build_bucket {
my $self = shift;
$self->{bucket} ||= $self->s3->bucket(Bugzilla->params->{s3_bucket});
return $self->bucket;
}
sub data_type { return 's3'; }
sub set_data {
my ($self, $data) = @_;
my $attach_id = $self->attach_id;
# If the attachment is larger than attachment_s3_minsize,
# we instead store it in the database.
if (Bugzilla->params->{attachment_s3_minsize}
&& $self->datasize < Bugzilla->params->{attachment_s3_minsize})
{
require Bugzilla::Attachment::Storage::Database;
return Bugzilla::Attachment::Storage::Database->new({attach_id => $self->attach_id})
->set_data($data);
}
unless ($self->bucket->add_key($attach_id, $data)) {
warn "Failed to add attachment $attach_id to S3: "
. $self->bucket->errstr . "\n";
ThrowCodeError('s3_add_failed',
{attach_id => $attach_id, reason => $self->bucket->errstr});
}
return $self;
}
sub get_data {
my ($self) = @_;
my $attach_id = $self->attach_id;
my $response = $self->bucket->get_key($attach_id);
if (!$response) {
warn "Failed to retrieve attachment $attach_id from S3: "
. $self->bucket->errstr . "\n";
ThrowCodeError('s3_get_failed',
{attach_id => $attach_id, reason => $self->bucket->errstr});
}
return $response->{value};
}
sub remove_data {
my ($self) = @_;
my $attach_id = $self->attach_id;
$self->bucket->delete_key($attach_id)
or warn "Failed to remove attachment $attach_id from S3: "
. $self->bucket->errstr . "\n";
return $self;
}
sub data_exists {
my ($self) = @_;
return !!$self->bucket->head_key($self->attach_id);
}
1;
# 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::Auth::Login;
use 5.10.1;
use strict;
use warnings;
use fields qw();
# Determines whether or not a user can logout. It's really a subroutine,
# but we implement it here as a constant. Override it in subclasses if
# that particular type of login method cannot log out.
use constant can_logout => 1;
use constant can_login => 1;
use constant requires_persistence => 1;
use constant requires_verification => 1;
use constant user_can_create_account => 0;
use constant is_automatic => 0;
use constant extern_id_used => 0;
sub new {
my ($class) = @_;
my $self = fields::new($class);
return $self;
}
1;
__END__
=head1 NAME
Bugzilla::Auth::Login - Gets username/password data from the user.
=head1 DESCRIPTION
Bugzilla::Auth::Login is used to get information that uniquely identifies
a user and allows us to authorize their Bugzilla access.
It is mostly an abstract class, requiring subclasses to implement
most methods.
Note that callers outside of the C<Bugzilla::Auth> package should never
create this object directly. Just create a C<Bugzilla::Auth> object
and call C<login> on it.
=head1 LOGIN METHODS
These are methods that have to do with getting the actual login data
from the user or handling a login somehow.
These methods are abstract -- they MUST be implemented by a subclass.
=over 4
=item C<get_login_info()>
Description: Gets a username/password from the user, or some other
information that uniquely identifies them.
Params: None
Returns: A C<$login_data> hashref. (See L<Bugzilla::Auth> for details.)
The hashref MUST contain: C<user_id> *or* C<username>
If this is a login method that requires verification,
the hashref MUST contain C<password>.
The hashref MAY contain C<realname> and C<extern_id>.
=item C<fail_nodata()>
Description: This function is called when Bugzilla doesn't get
a username/password and the login type is C<LOGIN_REQUIRED>
(See L<Bugzilla::Auth> for a description of C<LOGIN_REQUIRED>).
That is, this handles C<AUTH_NODATA> in that situation.
This function MUST stop CGI execution when it is complete.
That is, it must call C<exit> or C<ThrowUserError> or some
such thing.
Params: None
Returns: Never Returns.
=back
=head1 INFO METHODS
These are methods that describe the capabilities of this
C<Bugzilla::Auth::Login> object. These are all no-parameter
methods that return either C<true> or C<false>.
=over 4
=item C<can_logout>
Whether or not users can log out if they logged in using this
object. Defaults to C<true>.
=item C<can_login>
Whether or not users can log in through the web interface using
this object. Defaults to C<true>.
=item C<requires_persistence>
Whether or not we should send the user a cookie if they logged in with
this method. Defaults to C<true>.
=item C<requires_verification>
Whether or not we should check the username/password that we
got from this login method. Defaults to C<true>.
=item C<user_can_create_account>
Whether or not users can create accounts, if this login method is
currently being used by the system. Defaults to C<false>.
=item C<is_automatic>
True if this login method requires no interaction from the user within
Bugzilla. (For example, C<Env> auth is "automatic" because the webserver
just passes us an environment variable on most page requests, and does not
ask the user for authentication information directly in Bugzilla.) Defaults
to C<false>.
=item C<extern_id_used>
Whether or not this login method uses the extern_id field. If
used, users with editusers permission will be be allowed to
edit the extern_id for all users.
The default value is C<0>.
=back
# 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::Auth::Login::APIKey;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::Auth::Login);
use Bugzilla::Constants;
use Bugzilla::User::APIKey;
use Bugzilla::Util;
use Bugzilla::Error;
use constant requires_persistence => 0;
use constant requires_verification => 0;
use constant can_login => 0;
use constant can_logout => 0;
use fields qw(app_id);
sub set_app_id {
my ($self, $app_id) = @_;
$self->{app_id} = $app_id;
}
sub app_id {
my ($self) = @_;
return $self->{app_id};
}
# This method is only available to web services. An API key can never
# be used to authenticate a Web request.
sub get_login_info {
my ($self) = @_;
my $params = Bugzilla->input_params;
my ($user_id, $login_cookie);
my $api_key_text = trim(delete $params->{'Bugzilla_api_key'});
if (!i_am_webservice() || !$api_key_text) {
return {failure => AUTH_NODATA};
}
my $api_key = Bugzilla::User::APIKey->new({name => $api_key_text});
my $remote_ip = remote_ip();
if (!$api_key or $api_key->api_key ne $api_key_text) {
# The second part checks the correct capitalization. Silly MySQL
ThrowUserError("api_key_not_valid");
}
elsif ( $api_key->sticky
&& $api_key->last_used_ip
&& $api_key->last_used_ip ne $remote_ip)
{
ThrowUserError("api_key_not_valid");
}
elsif ($api_key->revoked) {
ThrowUserError('api_key_revoked');
}
$api_key->update_last_used($remote_ip);
$self->set_app_id($api_key->app_id);
return {user_id => $api_key->user_id};
}
1;
# 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::Auth::Login::CGI;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::Auth::Login);
use constant user_can_create_account => 1;
use Bugzilla::Constants;
use Bugzilla::WebService::Constants;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Token;
sub get_login_info {
my ($self) = @_;
my $params = Bugzilla->input_params;
my $cgi = Bugzilla->cgi;
my $login = trim(delete $params->{'Bugzilla_login'});
my $password = delete $params->{'Bugzilla_password'};
# The token must match the cookie to authenticate the request.
my $login_token = delete $params->{'Bugzilla_login_token'};
my $login_cookie = $cgi->cookie('Bugzilla_login_request_cookie');
my $valid = 0;
# If the web browser accepts cookies, use them.
if ($login_token && $login_cookie) {
my ($time, undef) = split(/-/, $login_token);
# Regenerate the token based on the information we have.
my $expected_token = issue_hash_token(['login_request', $login_cookie], $time);
$valid = 1 if $expected_token eq $login_token;
$cgi->remove_cookie('Bugzilla_login_request_cookie');
}
# WebServices and other local scripts can bypass this check.
# This is safe because we won't store a login cookie in this case.
elsif (Bugzilla->usage_mode != USAGE_MODE_BROWSER) {
$valid = 1;
}
# Else falls back to the Referer header and accept local URLs.
# Attachments are served from a separate host (ideally), and so
# an evil attachment cannot abuse this check with a redirect.
elsif (my $referer = $cgi->referer) {
my $urlbase = Bugzilla->localconfig->urlbase;
$valid = 1 if $referer =~ /^\Q$urlbase\E/;
}
# If the web browser doesn't accept cookies and the Referer header
# is missing, we have no way to make sure that the authentication
# request comes from the user.
elsif ($login && $password) {
ThrowUserError('auth_untrusted_request', {login => $login});
}
if (!defined($login) || !defined($password) || !$valid) {
return {failure => AUTH_NODATA};
}
return {username => $login, password => $password};
}
sub fail_nodata {
my ($self) = @_;
my $cgi = Bugzilla->cgi;
my $template = Bugzilla->template;
if (Bugzilla->usage_mode != USAGE_MODE_BROWSER) {
ThrowUserError('login_required');
}
print $cgi->header();
$template->process("account/auth/login.html.tmpl",
{'target' => $cgi->url(-relative => 1)})
|| ThrowTemplateError($template->error());
exit;
}
1;
# 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::Auth::Login::Cookie;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::Auth::Login);
use fields qw(_login_token _cookie);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Token;
use Bugzilla::Util;
use List::Util qw(first);
use constant requires_persistence => 0;
use constant requires_verification => 0;
use constant can_login => 0;
sub is_automatic { return $_[0]->login_token ? 0 : 1; }
# Note that Cookie never consults the Verifier, it always assumes
# it has a valid DB account or it fails.
sub get_login_info {
my ($self) = @_;
my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh;
my ($user_id, $login_cookie, $is_internal);
if (!Bugzilla->request_cache->{auth_no_automatic_login}) {
$login_cookie = $cgi->cookie("Bugzilla_logincookie");
$user_id = $cgi->cookie("Bugzilla_login");
# If cookies cannot be found, this could mean that they haven't
# been made available yet. In this case, look at Bugzilla_cookie_list.
unless ($login_cookie) {
my $cookie = first { $_->name eq 'Bugzilla_logincookie' }
@{$cgi->{'Bugzilla_cookie_list'}};
$login_cookie = $cookie->value if $cookie;
}
unless ($user_id) {
my $cookie = first { $_->name eq 'Bugzilla_login' }
@{$cgi->{'Bugzilla_cookie_list'}};
$user_id = $cookie->value if $cookie;
}
$self->cookie($login_cookie);
# If the call is for a web service, and an api token is provided, check
# it is valid.
if (i_am_webservice()) {
if (exists Bugzilla->input_params->{Bugzilla_api_token}) {
my $api_token = delete Bugzilla->input_params->{Bugzilla_api_token};
my ($token_user_id, undef, undef, $token_type)
= Bugzilla::Token::GetTokenData($api_token);
if ( !defined $token_type
|| $token_type ne 'api_token'
|| $user_id != $token_user_id)
{
ThrowUserError('auth_invalid_token', {token => $api_token});
}
$is_internal = 1;
}
elsif ($login_cookie && Bugzilla->usage_mode == USAGE_MODE_REST) {
# REST requires an api-token when using cookie authentication
# fall back to a non-authenticated request
$login_cookie = '';
}
}
}
# If no cookies were provided, we also look for a login token
# passed in the parameters of a webservice
my $token = $self->login_token;
if ($token && (!$login_cookie || !$user_id)) {
($user_id, $login_cookie) = ($token->{'user_id'}, $token->{'login_token'});
}
if ($login_cookie && $user_id) {
# Anything goes for these params - they're just strings which
# we're going to verify against the db
detaint_natural($user_id);
my $db_cookie = $dbh->selectrow_array(
'SELECT cookie FROM logincookies WHERE cookie = ? AND userid = ?',
undef, ($login_cookie, $user_id)
);
# If the cookie is valid, return a valid username.
if (defined $db_cookie && $login_cookie eq $db_cookie) {
# forbid logging in with a cookie if only api-keys are allowed
if (i_am_webservice() && !$is_internal) {
my $user = Bugzilla::User->new({id => $user_id, cache => 1});
if ($user->settings->{api_key_only}->{value} eq 'on') {
ThrowUserError('invalid_cookies_or_token');
}
}
# If we logged in successfully, then update the lastused
# time on the login cookie
$dbh->do(
"UPDATE logincookies SET lastused = NOW()
WHERE cookie = ?", undef, $login_cookie
);
return {user_id => $user_id};
}
elsif (i_am_webservice()) {
ThrowUserError('invalid_cookies_or_token');
}
}
# Either the cookie or token is invalid and we are not authenticating
# via a webservice, or we did not receive a cookie or token. We don't
# want to ever return AUTH_LOGINFAILED, because we don't want Bugzilla to
# actually throw an error when it gets a bad cookie or token. It should just
# look like there was no cookie or token to begin with.
return {failure => AUTH_NODATA};
}
sub login_token {
my ($self) = @_;
my $input = Bugzilla->input_params;
my $usage_mode = Bugzilla->usage_mode;
return $self->{'_login_token'} if exists $self->{'_login_token'};
if (!i_am_webservice()) {
return $self->{'_login_token'} = undef;
}
# Check if a token was passed in via requests for WebServices
my $token = trim(delete $input->{'Bugzilla_token'});
return $self->{'_login_token'} = undef if !$token;
my ($user_id, $login_token) = split('-', $token, 2);
if (!detaint_natural($user_id) || !$login_token) {
return $self->{'_login_token'} = undef;
}
return $self->{'_login_token'}
= {user_id => $user_id, login_token => $login_token};
}
sub cookie {
my ($self, $val) = @_;
$self->{_cookie} = $val if @_ > 1;
return $self->{_cookie};
}
1;
# 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::Auth::Login::Env;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::Auth::Login);
use Bugzilla::Constants;
use Bugzilla::Error;
use constant can_logout => 0;
use constant can_login => 0;
use constant requires_persistence => 0;
use constant requires_verification => 0;
use constant is_automatic => 1;
use constant extern_id_used => 1;
sub get_login_info {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
my $env_id = $ENV{Bugzilla->params->{"auth_env_id"}} || '';
my $env_email = $ENV{Bugzilla->params->{"auth_env_email"}} || '';
my $env_realname = $ENV{Bugzilla->params->{"auth_env_realname"}} || '';
return {failure => AUTH_NODATA} if !$env_email;
return {
username => $env_email,
extern_id => $env_id,
realname => $env_realname
};
}
sub fail_nodata {
ThrowCodeError('env_no_email');
}
1;
# 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::Auth::Login::Stack;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::Auth::Login);
use fields qw(
_stack
successful
);
use Hash::Util qw(lock_keys);
use Bugzilla::Hook;
use Bugzilla::Constants;
use List::MoreUtils qw(any);
sub new {
my $class = shift;
my $self = $class->SUPER::new(@_);
my $list = shift;
my %methods = map { $_ => "Bugzilla/Auth/Login/$_.pm" } split(',', $list);
lock_keys(%methods);
Bugzilla::Hook::process('auth_login_methods', {modules => \%methods});
$self->{_stack} = [];
foreach my $login_method (split(',', $list)) {
my $module = $methods{$login_method};
require $module;
$module =~ s|/|::|g;
$module =~ s/.pm$//;
push(@{$self->{_stack}}, $module->new(@_));
}
return $self;
}
sub get_login_info {
my $self = shift;
my $result;
foreach my $object (@{$self->{_stack}}) {
# See Bugzilla::WebService::Server::JSONRPC for where and why
# auth_no_automatic_login is used.
if (Bugzilla->request_cache->{auth_no_automatic_login}) {
next if $object->is_automatic;
}
$result = $object->get_login_info(@_);
$self->{successful} = $object;
# We only carry on down the stack if this method denied all knowledge.
last
unless ($result->{failure}
&& ( $result->{failure} eq AUTH_NODATA
|| $result->{failure} eq AUTH_NO_SUCH_USER));
# If none of the methods succeed, it's undef.
$self->{successful} = undef;
}
return $result;
}
sub fail_nodata {
my $self = shift;
# We fail from the bottom of the stack.
my @reverse_stack = reverse @{$self->{_stack}};
foreach my $object (@reverse_stack) {
# We pick the first object that actually has the method
# implemented.
if ($object->can('fail_nodata')) {
$object->fail_nodata(@_);
}
}
}
sub can_login {
my ($self) = @_;
# We return true if any method can log in.
foreach my $object (@{$self->{_stack}}) {
return 1 if $object->can_login;
}
return 0;
}
sub user_can_create_account {
my ($self) = @_;
# We return true if any method allows users to create accounts.
foreach my $object (@{$self->{_stack}}) {
return 1 if $object->user_can_create_account;
}
return 0;
}
sub extern_id_used {
my ($self) = @_;
return any { $_->extern_id_used } @{$self->{_stack}};
}
1;
# 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::Auth::Persist::Cookie;
use 5.10.1;
use strict;
use warnings;
use fields qw();
use Bugzilla::Constants;
use Bugzilla::Util;
use Bugzilla::Token;
use List::Util qw(first);
use List::MoreUtils qw(any);
sub new {
my ($class) = @_;
my $self = fields::new($class);
return $self;
}
sub persist_login {
my ($self, $user) = @_;
my $dbh = Bugzilla->dbh;
my $cgi = Bugzilla->cgi;
my $input_params = Bugzilla->input_params;
$dbh->bz_start_transaction();
my $login_cookie
= Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie');
my $ip_addr = remote_ip();
$dbh->do('INSERT INTO logincookies (cookie, userid, ipaddr, lastused)
VALUES (?, ?, ?, NOW())', undef, $login_cookie, $user->id, $ip_addr);
# Issuing a new cookie is a good time to clean up the old
# cookies.
$dbh->do("DELETE FROM logincookies WHERE lastused < "
. $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', MAX_LOGINCOOKIE_AGE, 'DAY'));
$dbh->bz_commit_transaction();
# Prevent JavaScript from accessing login cookies.
my %cookieargs = ('-httponly' => 1);
# Remember cookie only if admin has told so
# or admin didn't forbid it and user told to remember.
if (
Bugzilla->params->{'rememberlogin'} eq 'on'
|| ( Bugzilla->params->{'rememberlogin'} ne 'off'
&& $input_params->{'Bugzilla_remember'}
&& $input_params->{'Bugzilla_remember'} eq 'on')
)
{
# Not a session cookie, so set an infinite expiry
$cookieargs{'-expires'} = 'Fri, 01-Jan-2038 00:00:00 GMT';
}
if (Bugzilla->params->{'ssl_redirect'}) {
# Make these cookies only be sent to us by the browser during
# HTTPS sessions, if we're using SSL.
$cookieargs{'-secure'} = 1;
}
$cgi->remove_cookie('github_secret');
$cgi->remove_cookie('Bugzilla_login_request_cookie');
$cgi->send_cookie(-name => 'Bugzilla_login', -value => $user->id, %cookieargs);
$cgi->send_cookie(
-name => 'Bugzilla_logincookie',
-value => $login_cookie,
%cookieargs
);
my $securemail_groups
= Bugzilla->can('securemail_groups')
? Bugzilla->securemail_groups
: ['admin'];
if (any { $user->in_group($_) } 'mozilla-employee-confidential',
@$securemail_groups)
{
my $auth_method
= eval { ref($user->authorizer->successful_info_getter) } // 'unknown';
Bugzilla->audit(
sprintf "successful login of %s from %s using \"%s\", authenticated by %s",
$user->login, $ip_addr, $cgi->user_agent // '', $auth_method);
}
return $login_cookie;
}
sub logout {
my ($self, $param) = @_;
my $dbh = Bugzilla->dbh;
my $cgi = Bugzilla->cgi;
my $input = Bugzilla->input_params;
$param = {} unless $param;
my $user = $param->{user} || Bugzilla->user;
my $type = $param->{type} || LOGOUT_ALL;
if ($type == LOGOUT_ALL) {
$dbh->do("DELETE FROM logincookies WHERE userid = ?", undef, $user->id);
return;
}
# The LOGOUT_*_CURRENT options require the current login cookie.
# If a new cookie has been issued during this run, that's the current one.
# If not, it's the one we've received.
my @login_cookies;
my $cookie = first { $_->name eq 'Bugzilla_logincookie' }
@{$cgi->{'Bugzilla_cookie_list'}};
if ($cookie) {
push(@login_cookies, $cookie->value);
}
else {
push(@login_cookies, $cgi->cookie("Bugzilla_logincookie"));
}
# If we are a webservice using a token instead of cookie
# then add that as well to the login cookies to delete
if (my $login_token = $user->authorizer->login_token) {
push(@login_cookies, $login_token->{'login_token'});
}
# Make sure that @login_cookies is not empty to not break SQL statements.
push(@login_cookies, '') unless @login_cookies;
# These queries use both the cookie ID and the user ID as keys. Even
# though we know the userid must match, we still check it in the SQL
# as a sanity check, since there is no locking here, and if the user
# logged out from two machines simultaneously, while someone else
# logged in and got the same cookie, we could be logging the other
# user out here. Yes, this is very very very unlikely, but why take
# chances? - bbaetz
@login_cookies = map { $dbh->quote($_) } @login_cookies;
if ($type == LOGOUT_KEEP_CURRENT) {
$dbh->do(
"DELETE FROM logincookies WHERE "
. $dbh->sql_in('cookie', \@login_cookies, 1)
. " AND userid = ?",
undef, $user->id
);
}
elsif ($type == LOGOUT_CURRENT) {
$dbh->do(
"DELETE FROM logincookies WHERE "
. $dbh->sql_in('cookie', \@login_cookies)
. " AND userid = ?",
undef, $user->id
);
}
else {
die("Invalid type $type supplied to logout()");
}
if ($type != LOGOUT_KEEP_CURRENT) {
clear_browser_cookies();
}
}
sub clear_browser_cookies {
my $cgi = Bugzilla->cgi;
$cgi->remove_cookie('Bugzilla_login');
$cgi->remove_cookie('Bugzilla_logincookie');
$cgi->remove_cookie('sudo');
}
1;
# 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::Auth::Verify;
use 5.10.1;
use strict;
use warnings;
use fields qw();
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::User;
use Bugzilla::Util;
use constant user_can_create_account => 1;
use constant extern_id_used => 0;
sub new {
my ($class, $login_type) = @_;
my $self = fields::new($class);
return $self;
}
sub can_change_password {
return $_[0]->can('change_password');
}
sub create_or_update_user {
my ($self, $params) = @_;
my $dbh = Bugzilla->dbh;
my $extern_id = $params->{extern_id};
my $username = $params->{bz_username} || $params->{username};
my $password = $params->{password} || '*';
my $real_name = $params->{realname} || '';
my $user_id = $params->{user_id};
# A passed-in user_id always overrides anything else, for determining
# what account we should return.
if (!$user_id) {
my $username_user_id = login_to_id($username || '');
my $extern_user_id;
if ($extern_id) {
$extern_user_id = $dbh->selectrow_array(
'SELECT userid
FROM profiles WHERE extern_id = ?', undef, $extern_id
);
}
# If we have both a valid extern_id and a valid username, and they are
# not the same id, then we have a conflict.
if ( $username_user_id
&& $extern_user_id
&& $username_user_id ne $extern_user_id)
{
my $extern_name = Bugzilla::User->new($extern_user_id)->login;
return {
failure => AUTH_ERROR,
error => "extern_id_conflict",
details =>
{extern_id => $extern_id, extern_user => $extern_name, username => $username}
};
}
# If we have a valid username, but no valid id,
# then we have to create the user. This happens when we're
# passed only a username, and that username doesn't exist already.
if ($username && !$username_user_id && !$extern_user_id) {
validate_email_syntax($username) || return {
failure => AUTH_ERROR,
error => 'auth_invalid_email',
details => {addr => $username}
};
# external authentication
# systems might follow different standards than ours. So in this
# XXX Theoretically this could fail with an error, but the fix for
# that is too involved to be done right now.
my $user
= Bugzilla::User->create({
login_name => $username, cryptpassword => $password, realname => $real_name
});
$username_user_id = $user->id;
}
# If we have a valid username id and an extern_id, but no valid
# extern_user_id, then we have to set the user's extern_id.
if ($extern_id && $username_user_id && !$extern_user_id) {
$dbh->do('UPDATE profiles SET extern_id = ? WHERE userid = ?',
undef, $extern_id, $username_user_id);
Bugzilla->memcached->clear({table => 'profiles', id => $username_user_id});
}
# Finally, at this point, one of these will give us a valid user id.
$user_id = $extern_user_id || $username_user_id;
}
# If we still don't have a valid user_id, then we weren't passed
# enough information in $params, and we should die right here.
ThrowCodeError(
'bad_arg',
{
argument => 'params',
function => 'Bugzilla::Auth::Verify::create_or_update_user'
}
) unless $user_id;
my $user = new Bugzilla::User({id => $user_id, cache => 1});
# Now that we have a valid User, we need to see if any data has to be
# updated.
my $user_updated = 0;
if ($username && lc($user->login) ne lc($username)) {
validate_email_syntax($username) || return {
failure => AUTH_ERROR,
error => 'auth_invalid_email',
details => {addr => $username}
};
$user->set_login($username);
$user_updated = 1;
}
if ($real_name && $user->name ne $real_name) {
# $real_name is more than likely tainted, but we only use it
# in a placeholder and we never use it after this.
$user->set_name($real_name);
$user_updated = 1;
}
$user->update() if $user_updated;
return {user => $user};
}
1;
__END__
=head1 NAME
Bugzilla::Auth::Verify - An object that verifies usernames and passwords.
=head1 DESCRIPTION
Bugzilla::Auth::Verify provides the "Verifier" part of the Bugzilla
login process. (For details, see the "STRUCTURE" section of
L<Bugzilla::Auth>.)
It is mostly an abstract class, requiring subclasses to implement
most methods.
Note that callers outside of the C<Bugzilla::Auth> package should never
create this object directly. Just create a C<Bugzilla::Auth> object
and call C<login> on it.
=head1 VERIFICATION METHODS
These are the methods that have to do with the actual verification.
Subclasses MUST implement these methods.
=over 4
=item C<check_credentials($login_data)>
Description: Checks whether or not a username is valid.
Params: $login_data - A C<$login_data> hashref, as described in
L<Bugzilla::Auth>.
This C<$login_data> hashref MUST contain
C<username>, and SHOULD also contain
C<password>.
Returns: A C<$login_data> hashref with C<bz_username> set. This
method may also set C<realname>. It must avoid changing
anything that is already set.
=back
=head1 MODIFICATION METHODS
These are methods that change data in the actual authentication backend.
These methods are optional, they do not have to be implemented by
subclasses.
=over 4
=item C<create_or_update_user($login_data)>
Description: Automatically creates a user account in the database
if it doesn't already exist, or updates the account
data if C<$login_data> contains newer information.
Params: $login_data - A C<$login_data> hashref, as described in
L<Bugzilla::Auth>.
This C<$login_data> hashref MUST contain
either C<user_id>, C<bz_username>, or
C<username>. If both C<username> and C<bz_username>
are specified, C<bz_username> is used as the
login name of the user to create in the database.
It MAY also contain C<extern_id>, in which
case it still MUST contain C<bz_username> or
C<username>.
It MAY contain C<password> and C<realname>.
Returns: A hashref with one element, C<user>, which is a
L<Bugzilla::User> object. May also return a login error
as described in L<Bugzilla::Auth>.
Note: This method is not abstract, it is actually implemented
and creates accounts in the Bugzilla database. Subclasses
should probably all call the C<Bugzilla::Auth::Verify>
version of this function at the end of their own
C<create_or_update_user>.
=item C<change_password($user, $password)>
Description: Modifies the user's password in the authentication backend.
Params: $user - A L<Bugzilla::User> object representing the user
whose password we want to change.
$password - The user's new password.
Returns: Nothing.
=back
=head1 INFO METHODS
These are methods that describe the capabilities of this object.
These are all no-parameter methods that return either C<true> or
C<false>.
=over 4
=item C<user_can_create_account>
Whether or not users can manually create accounts in this type of
account source. Defaults to C<true>.
=item C<extern_id_used>
Whether or not this verifier method uses the extern_id field. If
used, users with editusers permission will be be allowed to
edit the extern_id for all users.
The default value is C<false>.
=back
# 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::Auth::Verify::DB;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::Auth::Verify);
use Bugzilla::Constants;
use Bugzilla::Token;
use Bugzilla::Util;
use Bugzilla::User;
sub check_credentials {
my ($self, $login_data) = @_;
my $dbh = Bugzilla->dbh;
my $username = $login_data->{username};
my $user = new Bugzilla::User({name => $username});
return {failure => AUTH_NO_SUCH_USER} unless $user;
$login_data->{user} = $user;
$login_data->{bz_username} = $user->login;
if ($user->account_is_locked_out) {
return {failure => AUTH_LOCKOUT, user => $user};
}
my $password = $login_data->{password};
return {failure => AUTH_NODATA} unless defined $login_data->{password};
my $real_password_crypted = $user->cryptpassword;
# Using the internal crypted password as the salt,
# crypt the password the user entered.
my $entered_password_crypted = bz_crypt($password, $real_password_crypted);
if ($entered_password_crypted ne $real_password_crypted) {
# Record the login failure
$user->note_login_failure();
# Immediately check if we are locked out
if ($user->account_is_locked_out) {
return {failure => AUTH_LOCKOUT, user => $user, just_locked_out => 1};
}
return {
failure => AUTH_LOGINFAILED,
failure_count => scalar(@{$user->account_ip_login_failures}),
};
}
# The user's credentials are okay, so delete any outstanding
# password tokens or login failures they may have generated.
Bugzilla::Token::DeletePasswordTokens($user->id, "user_logged_in");
$user->clear_login_failures();
# If their old password was using crypt() or some different hash
# than we're using now, convert the stored password to using
# whatever hashing system we're using now.
my $current_algorithm = PASSWORD_DIGEST_ALGORITHM;
if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/) {
# We can't call $user->set_password because we don't want the password
# complexity rules to apply here.
$user->{cryptpassword} = bz_crypt($password);
$user->update();
}
if (i_am_webservice() && $user->settings->{api_key_only}->{value} eq 'on') {
# api-key verification happens in Auth/Login/APIKey
# token verification happens in Auth/Login/Cookie
# if we get here from an api call then we must be using user/pass
return {failure => AUTH_ERROR, user_error => 'invalid_auth_method',};
}
return $login_data;
}
sub change_password {
my ($self, $user, $password) = @_;
my $dbh = Bugzilla->dbh;
my $cryptpassword = bz_crypt($password);
$dbh->do("UPDATE profiles SET cryptpassword = ? WHERE userid = ?",
undef, $cryptpassword, $user->id);
Bugzilla->memcached->clear({table => 'profiles', id => $user->id});
}
1;
# 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::Auth::Verify::LDAP;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::Auth::Verify);
use fields qw(
ldap
);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::User;
use Bugzilla::Util;
use Net::LDAP;
use Net::LDAP::Util qw(escape_filter_value);
use constant admin_can_create_account => 0;
use constant user_can_create_account => 0;
sub check_credentials {
my ($self, $params) = @_;
my $dbh = Bugzilla->dbh;
# We need to bind anonymously to the LDAP server. This is
# because we need to get the Distinguished Name of the user trying
# to log in. Some servers (such as iPlanet) allow you to have unique
# uids spread out over a subtree of an area (such as "People"), so
# just appending the Base DN to the uid isn't sufficient to get the
# user's DN. For servers which don't work this way, there will still
# be no harm done.
$self->_bind_ldap_for_search();
# Now, we verify that the user exists, and get a LDAP Distinguished
# Name for the user.
my $username = $params->{username};
my $dn_result
= $self->ldap->search(_bz_search_params($username), attrs => ['dn']);
return {
failure => AUTH_ERROR,
error => "ldap_search_error",
details => {errstr => $dn_result->error, username => $username}
}
if $dn_result->code;
return {failure => AUTH_NO_SUCH_USER} if !$dn_result->count;
my $dn = $dn_result->shift_entry->dn;
# Check the password.
my $pw_result = $self->ldap->bind($dn, password => $params->{password});
return {failure => AUTH_LOGINFAILED} if $pw_result->code;
# And now we fill in the user's details.
# First try the search as the (already bound) user in question.
my $user_entry;
my $error_string;
my $detail_result = $self->ldap->search(_bz_search_params($username));
if ($detail_result->code) {
# Stash away the original error, just in case
$error_string = $detail_result->error;
}
else {
$user_entry = $detail_result->shift_entry;
}
# If that failed (either because the search failed, or returned no
# results) then try re-binding as the initial search user, but only
# if the LDAPbinddn parameter is set.
if (!$user_entry && Bugzilla->params->{"LDAPbinddn"}) {
$self->_bind_ldap_for_search();
$detail_result = $self->ldap->search(_bz_search_params($username));
if (!$detail_result->code) {
$user_entry = $detail_result->shift_entry;
}
}
# If we *still* don't have anything in $user_entry then give up.
return {
failure => AUTH_ERROR,
error => "ldap_search_error",
details => {errstr => $error_string, username => $username}
}
if !$user_entry;
my $mail_attr = Bugzilla->params->{"LDAPmailattribute"};
if ($mail_attr) {
if (!$user_entry->exists($mail_attr)) {
return {
failure => AUTH_ERROR,
error => "ldap_cannot_retrieve_attr",
details => {attr => $mail_attr}
};
}
my @emails = $user_entry->get_value($mail_attr);
# Default to the first email address returned.
$params->{bz_username} = $emails[0];
if (@emails > 1) {
# Cycle through the adresses and check if they're Bugzilla logins.
# Use the first one that returns a valid id.
foreach my $email (@emails) {
if (login_to_id($email)) {
$params->{bz_username} = $email;
last;
}
}
}
}
else {
$params->{bz_username} = $username;
}
$params->{realname} ||= $user_entry->get_value("displayName");
$params->{realname} ||= $user_entry->get_value("cn");
$params->{extern_id} = $username;
return $params;
}
sub _bz_search_params {
my ($username) = @_;
$username = escape_filter_value($username);
return (
base => Bugzilla->params->{"LDAPBaseDN"},
scope => "sub",
filter => '(&('
. Bugzilla->params->{"LDAPuidattribute"}
. "=$username)"
. Bugzilla->params->{"LDAPfilter"} . ')'
);
}
sub _bind_ldap_for_search {
my ($self) = @_;
my $bind_result;
if (Bugzilla->params->{"LDAPbinddn"}) {
my ($LDAPbinddn, $LDAPbindpass) = split(":", Bugzilla->params->{"LDAPbinddn"});
$bind_result = $self->ldap->bind($LDAPbinddn, password => $LDAPbindpass);
}
else {
$bind_result = $self->ldap->bind();
}
ThrowCodeError("ldap_bind_failed", {errstr => $bind_result->error})
if $bind_result->code;
}
# We can't just do this in new(), because we're not allowed to throw any
# error from anywhere under Bugzilla::Auth::new -- otherwise we
# could create a situation where the admin couldn't get to editparams
# to fix their mistake. (Because Bugzilla->login always calls
# Bugzilla::Auth->new, and almost every page calls Bugzilla->login.)
sub ldap {
my ($self) = @_;
return $self->{ldap} if $self->{ldap};
my @servers = split(/[\s,]+/, Bugzilla->params->{"LDAPserver"});
ThrowCodeError("ldap_server_not_defined") unless @servers;
foreach (@servers) {
$self->{ldap} = new Net::LDAP(trim($_));
last if $self->{ldap};
}
ThrowCodeError("ldap_connect_failed", {server => join(", ", @servers)})
unless $self->{ldap};
# try to start TLS if needed
if (Bugzilla->params->{"LDAPstarttls"}) {
my $mesg = $self->{ldap}->start_tls();
ThrowCodeError("ldap_start_tls_failed", {error => $mesg->error()})
if $mesg->code();
}
return $self->{ldap};
}
1;
# 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::Auth::Verify::RADIUS;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::Auth::Verify);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Util;
use Authen::Radius;
use constant admin_can_create_account => 0;
use constant user_can_create_account => 0;
sub check_credentials {
my ($self, $params) = @_;
my $dbh = Bugzilla->dbh;
my $address_suffix = Bugzilla->params->{'RADIUS_email_suffix'};
my $username = $params->{username};
# If we're using RADIUS_email_suffix, we may need to cut it off from
# the login name.
if ($address_suffix) {
$username =~ s/\Q$address_suffix\E$//i;
}
# Create RADIUS object.
my $radius = new Authen::Radius(
Host => Bugzilla->params->{'RADIUS_server'},
Secret => Bugzilla->params->{'RADIUS_secret'}
)
|| return {
failure => AUTH_ERROR,
error => 'radius_preparation_error',
details => {errstr => Authen::Radius::strerror()}
};
# Check the password.
$radius->check_pwd($username, $params->{password},
Bugzilla->params->{'RADIUS_NAS_IP'} || undef)
|| return {failure => AUTH_LOGINFAILED};
# Build the user account's e-mail address.
$params->{bz_username} = $username . $address_suffix;
return $params;
}
1;
# 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::Auth::Verify::Stack;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::Auth::Verify);
use fields qw(
_stack
successful
);
use Bugzilla::Hook;
use Hash::Util qw(lock_keys);
use List::MoreUtils qw(any);
sub new {
my $class = shift;
my $list = shift;
my $self = $class->SUPER::new(@_);
my %methods = map { $_ => "Bugzilla/Auth/Verify/$_.pm" } split(',', $list);
lock_keys(%methods);
Bugzilla::Hook::process('auth_verify_methods', {modules => \%methods});
$self->{_stack} = [];
foreach my $verify_method (split(',', $list)) {
my $module = $methods{$verify_method};
require $module;
$module =~ s|/|::|g;
$module =~ s/.pm$//;
push(@{$self->{_stack}}, $module->new(@_));
}
return $self;
}
sub can_change_password {
my ($self) = @_;
# We return true if any method can change passwords.
foreach my $object (@{$self->{_stack}}) {
return 1 if $object->can_change_password;
}
return 0;
}
sub check_credentials {
my $self = shift;
my $result;
foreach my $object (@{$self->{_stack}}) {
$result = $object->check_credentials(@_);
$self->{successful} = $object;
last if !$result->{failure};
# So that if none of them succeed, it's undef.
$self->{successful} = undef;
}
# Returns the result at the bottom of the stack if they all fail.
return $result;
}
sub create_or_update_user {
my $self = shift;
my $result;
foreach my $object (@{$self->{_stack}}) {
$result = $object->create_or_update_user(@_);
last if !$result->{failure};
}
# Returns the result at the bottom of the stack if they all fail.
return $result;
}
sub user_can_create_account {
my ($self) = @_;
# We return true if any method allows the user to create an account.
foreach my $object (@{$self->{_stack}}) {
return 1 if $object->user_can_create_account;
}
return 0;
}
sub extern_id_used {
my ($self) = @_;
return any { $_->extern_id_used } @{$self->{_stack}};
}
1;
# 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::Bloomfilter;
use 5.10.1;
use strict;
use warnings;
use Bugzilla::Constants;
use Algorithm::BloomFilter;
use Mojo::File qw(path);
use File::Spec::Functions qw(catfile);
sub _new_bloom_filter {
my ($n) = @_;
my $p = 0.01;
my $m = $n * abs(log $p) / log(2)**2;
my $k = $m / $n * log(2);
return Algorithm::BloomFilter->new($m, $k);
}
sub _file {
my ($name, $type) = @_;
my $datadir = bz_locations->{datadir};
return path(catfile($datadir, "$name.$type"));
}
sub populate {
my ($class, $name) = @_;
my $memcached = Bugzilla->memcached;
my @items = split(/\n/, _file($name, 'list')->slurp);
my $filter = _new_bloom_filter(@items + 0);
$filter->add($_) foreach @items;
_file($name, 'bloom')->spurt($filter->serialize);
$memcached->clear_bloomfilter({name => $name});
}
sub lookup {
my ($class, $name) = @_;
my $memcached = Bugzilla->memcached;
my $file = _file($name, 'bloom');
my $filter_data = $memcached->get_bloomfilter({name => $name});
if (!$filter_data && -f $file) {
$filter_data = $file->slurp;
$memcached->set_bloomfilter({name => $name, filter => $filter_data});
}
return Algorithm::BloomFilter->deserialize($filter_data) if $filter_data;
return undef;
}
1;
This source diff could not be displayed because it is too large. You can view the blob instead.
# 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::BugUrl;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::Object);
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Constants;
use Module::Runtime qw(require_module);
use URI::QueryParam;
###############################
#### Initialization ####
###############################
use constant DB_TABLE => 'bug_see_also';
use constant NAME_FIELD => 'value';
use constant LIST_ORDER => 'id';
# See Also is tracked in bugs_activity.
use constant AUDIT_CREATES => 0;
use constant AUDIT_UPDATES => 0;
use constant AUDIT_REMOVES => 0;
use constant DB_COLUMNS => qw(
id
bug_id
value
class
);
# This must be strings with the names of the validations,
# instead of coderefs, because subclasses override these
# validators with their own.
use constant VALIDATORS => {
value => '_check_value',
bug_id => '_check_bug_id',
class => \&_check_class,
};
# This is the order we go through all of subclasses and
# pick the first one that should handle the URL. New
# subclasses should be added at the end of the list.
use constant SUB_CLASSES => qw(
Bugzilla::BugUrl::Bugzilla::Local
Bugzilla::BugUrl::Bugzilla
Bugzilla::BugUrl::Launchpad
Bugzilla::BugUrl::Google
Bugzilla::BugUrl::Chromium
Bugzilla::BugUrl::Edge
Bugzilla::BugUrl::Debian
Bugzilla::BugUrl::JIRA
Bugzilla::BugUrl::Trac
Bugzilla::BugUrl::MantisBT
Bugzilla::BugUrl::MozSentry
Bugzilla::BugUrl::SourceForge
Bugzilla::BugUrl::GitHub
Bugzilla::BugUrl::GitLab
Bugzilla::BugUrl::MozSupport
Bugzilla::BugUrl::Aha
Bugzilla::BugUrl::WebCompat
Bugzilla::BugUrl::ServiceNow
Bugzilla::BugUrl::Splat
Bugzilla::BugUrl::Phabricator
);
###############################
#### Accessors ######
###############################
sub class { return $_[0]->{class} }
sub bug_id { return $_[0]->{bug_id} }
###############################
#### Methods ####
###############################
sub new {
my $class = shift;
my $param = shift;
if (ref $param) {
my $bug_id = $param->{bug_id};
my $name = $param->{name} || $param->{value};
if (!defined $bug_id) {
ThrowCodeError('bad_arg', {argument => 'bug_id', function => "${class}::new"});
}
if (!defined $name) {
ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"});
}
my $condition = 'bug_id = ? AND value = ?';
my @values = ($bug_id, $name);
$param = {condition => $condition, values => \@values};
}
unshift @_, $param;
return $class->SUPER::new(@_);
}
sub _do_list_select {
my $class = shift;
my $objects = $class->SUPER::_do_list_select(@_);
foreach my $object (@$objects) {
require_module($object->class);
bless $object, $object->class;
}
return $objects;
}
# This is an abstract method. It must be overridden
# in every subclass.
sub should_handle {
my ($class, $input) = @_;
ThrowCodeError('unknown_method', {method => "${class}::should_handle"});
}
sub class_for {
my ($class, $value) = @_;
my $uri = URI->new($value);
foreach my $subclass ($class->SUB_CLASSES) {
require_module($subclass);
return wantarray ? ($subclass, $uri) : $subclass
if $subclass->should_handle($uri);
}
ThrowUserError('bug_url_invalid', {url => $value, reason => 'show_bug'});
}
sub _check_class {
my ($class, $subclass) = @_;
require_module($subclass);
return $subclass;
}
sub _check_bug_id {
my ($class, $bug_id) = @_;
my $bug;
if (blessed $bug_id) {
# We got a bug object passed in, use it
$bug = $bug_id;
$bug->check_is_visible;
}
else {
# We got a bug id passed in, check it and get the bug object
$bug = Bugzilla::Bug->check({id => $bug_id});
}
return $bug->id;
}
sub _check_value {
my ($class, $uri) = @_;
my $value = $uri->as_string;
if (!$value) {
ThrowCodeError('param_required',
{function => 'add_see_also', param => '$value'});
}
# We assume that the URL is an HTTP URL if there is no (something)://
# in front.
if (!$uri->scheme) {
# This works better than setting $uri->scheme('http'), because
# that creates URLs like "http:domain.com" and doesn't properly
# differentiate the path from the domain.
$uri = new URI("http://$value");
}
elsif ($uri->scheme ne 'http' && $uri->scheme ne 'https') {
ThrowUserError('bug_url_invalid', {url => $value, reason => 'http'});
}
# This stops the following edge cases from being accepted:
# * show_bug.cgi?id=1
# * /show_bug.cgi?id=1
# * http:///show_bug.cgi?id=1
if (!$uri->authority or $uri->path !~ m{/}) {
ThrowUserError('bug_url_invalid', {url => $value, reason => 'path_only'});
}
if (length($uri->path) > MAX_BUG_URL_LENGTH) {
ThrowUserError('bug_url_too_long', {url => $uri->path});
}
return $uri;
}
1;
# 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::BugUrl::Aha;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::BugUrl);
###############################
#### Methods ####
###############################
sub should_handle {
my ($class, $uri) = @_;
return $uri =~ m!^https?://[^.]+\.aha\.io/features/(\w+-\d+)!;
}
sub get_feature_id {
my ($self) = @_;
if ($self->{value} =~ m!^https?://[^.]+\.aha\.io/features/(\w+-\d+)!) {
return $1;
}
}
sub _check_value {
my ($class, $uri) = @_;
$uri = $class->SUPER::_check_value($uri);
# Aha HTTP URLs redirect to HTTPS, so just use the HTTPS scheme.
$uri->scheme('https');
return $uri;
}
1;
# 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::BugUrl::Bugzilla;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::BugUrl);
use Bugzilla::Error;
use Bugzilla::Util;
###############################
#### Methods ####
###############################
sub should_handle {
my ($class, $uri) = @_;
return ($uri->path =~ /show_bug\.cgi$/) ? 1 : 0;
}
sub _check_value {
my ($class, $uri) = @_;
$uri = $class->SUPER::_check_value($uri);
my $bug_id = $uri->query_param('id');
# We don't currently allow aliases, because we can't check to see
# if somebody's putting both an alias link and a numeric ID link.
# When we start validating the URL by accessing the other Bugzilla,
# we can allow aliases.
detaint_natural($bug_id);
if (!$bug_id) {
my $value = $uri->as_string;
ThrowUserError('bug_url_invalid', {url => $value, reason => 'id'});
}
# Make sure that "id" is the only query parameter.
$uri->query("id=$bug_id");
# And remove any # part if there is one.
$uri->fragment(undef);
return $uri;
}
sub target_bug_id {
my ($self) = @_;
return new URI($self->name)->query_param('id');
}
1;
# 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::BugUrl::Bugzilla::Local;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::BugUrl::Bugzilla);
use Bugzilla::Error;
use Bugzilla::Util;
###############################
#### Initialization ####
###############################
use constant VALIDATOR_DEPENDENCIES => {value => ['bug_id'],};
###############################
#### Methods ####
###############################
sub ref_bug_url {
my $self = shift;
if (!exists $self->{ref_bug_url}) {
my $ref_bug_id = new URI($self->name)->query_param('id');
my $ref_bug = Bugzilla::Bug->check($ref_bug_id);
my $ref_value = $self->local_uri($self->bug_id);
$self->{ref_bug_url} = new Bugzilla::BugUrl::Bugzilla::Local(
{bug_id => $ref_bug->id, value => $ref_value});
}
return $self->{ref_bug_url};
}
sub should_handle {
my ($class, $uri) = @_;
# Check if it is either a bug id number or an alias.
return 1 if $uri->as_string =~ m/^\w+$/;
# Check if it is a local Bugzilla uri and call
# Bugzilla::BugUrl::Bugzilla to check if it's a valid Bugzilla
# See Also URL.
my $canonical_local = URI->new($class->local_uri)->canonical;
if ( $canonical_local->authority eq $uri->canonical->authority
and $canonical_local->path eq $uri->canonical->path)
{
return $class->SUPER::should_handle($uri);
}
return 0;
}
sub _check_value {
my ($class, $uri, undef, $params) = @_;
# At this point we are going to treat any word as a
# bug id/alias to the local Bugzilla.
my $value = $uri->as_string;
if ($value =~ m/^\w+$/) {
$uri = new URI($class->local_uri($value));
}
else {
# It's not a word, then we have to check
# if it's a valid Bugzilla URL.
$uri = $class->SUPER::_check_value($uri);
}
my $ref_bug_id = $uri->query_param('id');
my $ref_bug = Bugzilla::Bug->check($ref_bug_id);
my $self_bug_id = $params->{bug_id};
$params->{ref_bug} = $ref_bug;
if ($ref_bug->id == $self_bug_id) {
ThrowUserError('see_also_self_reference');
}
my $product = $ref_bug->product_obj;
if (!Bugzilla->user->can_edit_product($product->id)) {
ThrowUserError("product_edit_denied", {product => $product->name});
}
return $uri;
}
sub local_uri {
my ($self, $bug_id) = @_;
$bug_id ||= '';
return Bugzilla->localconfig->urlbase . "show_bug.cgi?id=$bug_id";
}
sub bug {
my ($self) = @_;
return Bugzilla::Bug->new({id => $self->target_bug_id, cache => 1});
}
1;
# 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::BugUrl::Chromium;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::BugUrl);
use Bugzilla::Error;
use Bugzilla::Util;
###############################
#### Methods ####
###############################
sub should_handle {
my ($class, $uri) = @_;
return ($uri->authority =~ /^bugs.chromium.org$/i) ? 1 : 0;
}
sub _check_value {
my ($class, $uri) = @_;
$uri = $class->SUPER::_check_value($uri);
my $value = $uri->as_string;
my $project_name;
if ($uri->path =~ m|^/p/([^/]+)/issues/detail$|) {
$project_name = $1;
}
else {
ThrowUserError('bug_url_invalid', {url => $value});
}
my $bug_id = $uri->query_param('id');
detaint_natural($bug_id);
if (!$bug_id) {
ThrowUserError('bug_url_invalid', {url => $value, reason => 'id'});
}
return URI->new($value);
}
1;
# 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::BugUrl::Debian;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::BugUrl);
use Bugzilla::Error;
use Bugzilla::Util;
###############################
#### Methods ####
###############################
sub should_handle {
my ($class, $uri) = @_;
return ($uri->authority =~ /^bugs.debian.org$/i) ? 1 : 0;
}
sub _check_value {
my $class = shift;
my $uri = $class->SUPER::_check_value(@_);
# Debian BTS URLs can look like various things:
# http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234
# http://bugs.debian.org/1234
my $bug_id;
if ($uri->path =~ m|^/(\d+)$|) {
$bug_id = $1;
}
elsif ($uri->path =~ /bugreport\.cgi$/) {
$bug_id = $uri->query_param('bug');
detaint_natural($bug_id);
}
if (!$bug_id) {
ThrowUserError('bug_url_invalid', {url => $uri->path, reason => 'id'});
}
# This is the shortest standard URL form for Debian BTS URLs,
# and so we reduce all URLs to this.
return new URI("http://bugs.debian.org/" . $bug_id);
}
1;
# 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::BugUrl::Edge;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::BugUrl);
use Bugzilla::Error;
use Bugzilla::Util;
use List::MoreUtils qw( any );
###############################
#### Methods ####
###############################
# Example: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/9713176/
# Example 2: https://wpdev.uservoice.com/forums/257854/
# https://wpdev.uservoice.com/forums/257854/suggestions/17420707
# https://wpdev.uservoice.com/forums/257854-microsoft-edge-developer/suggestions/17420707-implement-css-display-flow-root-modern-clearfi
sub should_handle {
my ($class, $uri) = @_;
return any { lc($uri->authority) eq $_ }
qw( developer.microsoft.com wpdev.uservoice.com );
}
sub _check_value {
my ($class, $uri) = @_;
$uri = $class->SUPER::_check_value($uri);
return $uri if $uri->path =~ m{^/en-us/microsoft-edge/platform/issues/\d+/$};
return $uri
if $uri->path =~ m{^/forums/\d+(?:-[^/]+)?/suggestions/\d+(?:-[^/]+)?};
ThrowUserError('bug_url_invalid', {url => "$uri"});
}
1;
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment