summaryrefslogtreecommitdiffstats
path: root/Bugzilla
diff options
context:
space:
mode:
authorDylan William Hardison <dylan@hardison.net>2018-10-14 18:19:50 +0200
committerDylan William Hardison <dylan@hardison.net>2018-10-14 18:19:50 +0200
commitce00a61057535d49aa0e83181a1d317d7842571b (patch)
tree280243de9ff791449fb2c82f3f0f2b9bd931d5b2 /Bugzilla
parent6367a26da4093a8379e370ef328e9507c98b2e7e (diff)
parent6657fa9f5210d5b5a9b14c0cba6882bd56232054 (diff)
downloadbugzilla-ce00a61057535d49aa0e83181a1d317d7842571b.tar.gz
bugzilla-ce00a61057535d49aa0e83181a1d317d7842571b.tar.xz
Merge remote-tracking branch 'bmo/master'
Diffstat (limited to 'Bugzilla')
-rw-r--r--Bugzilla/BugUrl.pm8
-rw-r--r--Bugzilla/CGI.pm12
-rw-r--r--Bugzilla/Config.pm3
-rw-r--r--Bugzilla/Config/Reports.pm37
-rw-r--r--Bugzilla/Constants.pm6
-rw-r--r--Bugzilla/DB.pm83
-rw-r--r--Bugzilla/DB/Schema.pm11
-rw-r--r--Bugzilla/Error.pm12
-rw-r--r--Bugzilla/Error/Base.pm21
-rw-r--r--Bugzilla/Error/Code.pm14
-rw-r--r--Bugzilla/Error/User.pm13
-rw-r--r--Bugzilla/Error/disabled0
-rw-r--r--Bugzilla/Install/Requirements.pm1
-rw-r--r--Bugzilla/Markdown/GFM.pm4
-rw-r--r--Bugzilla/Quantum.pm187
-rw-r--r--Bugzilla/Quantum/CGI.pm246
-rw-r--r--Bugzilla/Quantum/Home.pm28
-rw-r--r--Bugzilla/Quantum/Plugin/BlockIP.pm38
-rw-r--r--Bugzilla/Quantum/Plugin/Glue.pm217
-rw-r--r--Bugzilla/Quantum/Plugin/Helpers.pm66
-rw-r--r--Bugzilla/Quantum/Static.pm18
-rw-r--r--Bugzilla/Quantum/Stdout.pm50
-rw-r--r--Bugzilla/Report/SecurityRisk.pm314
-rw-r--r--Bugzilla/Template.pm2
-rw-r--r--Bugzilla/Test/Util.pm39
-rw-r--r--Bugzilla/Token.pm15
-rw-r--r--Bugzilla/User.pm2
-rw-r--r--Bugzilla/WebService/Bug.pm20
-rw-r--r--Bugzilla/WebService/Bugzilla.pm4
-rw-r--r--Bugzilla/WebService/JSON.pm64
-rw-r--r--Bugzilla/WebService/JSON/Box.pm43
-rw-r--r--Bugzilla/WebService/Product.pm21
-rw-r--r--Bugzilla/WebService/README4
-rw-r--r--Bugzilla/WebService/Server.pm18
-rw-r--r--Bugzilla/WebService/Server/JSONRPC.pm14
-rw-r--r--Bugzilla/WebService/Server/REST.pm1
-rw-r--r--Bugzilla/WebService/Util.pm24
37 files changed, 1244 insertions, 416 deletions
diff --git a/Bugzilla/BugUrl.pm b/Bugzilla/BugUrl.pm
index 4724ae71a..a824d286d 100644
--- a/Bugzilla/BugUrl.pm
+++ b/Bugzilla/BugUrl.pm
@@ -16,6 +16,7 @@ use base qw(Bugzilla::Object);
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Constants;
+use Module::Runtime qw(require_module);
use URI::QueryParam;
@@ -113,7 +114,7 @@ sub _do_list_select {
my $objects = $class->SUPER::_do_list_select(@_);
foreach my $object (@$objects) {
- eval "use " . $object->class; die $@ if $@;
+ require_module($object->class);
bless $object, $object->class;
}
@@ -133,8 +134,7 @@ sub class_for {
my $uri = URI->new($value);
foreach my $subclass ($class->SUB_CLASSES) {
- eval "use $subclass";
- die $@ if $@;
+ require_module($subclass);
return wantarray ? ($subclass, $uri) : $subclass
if $subclass->should_handle($uri);
}
@@ -145,7 +145,7 @@ sub class_for {
sub _check_class {
my ($class, $subclass) = @_;
- eval "use $subclass"; die $@ if $@;
+ require_module($subclass);
return $subclass;
}
diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm
index 2cbb02e3e..4be384b67 100644
--- a/Bugzilla/CGI.pm
+++ b/Bugzilla/CGI.pm
@@ -89,12 +89,6 @@ sub SHOW_BUG_MODAL_CSP {
push @{ $policy{img_src} }, $attach_base;
}
- # MozReview API calls
- my $mozreview_url = Bugzilla->params->{mozreview_base_url};
- if ($mozreview_url) {
- push @{ $policy{connect_src} }, $mozreview_url . 'api/extensions/mozreview.extension.MozReviewExtension/summary/';
- }
-
return %policy;
}
@@ -103,12 +97,6 @@ sub _init_bz_cgi_globals {
# We need to disable output buffering - see bug 179174
$| = 1;
- # Ignore SIGTERM and SIGPIPE - this prevents DB corruption. If the user closes
- # their browser window while a script is running, the web server sends these
- # signals, and we don't want to die half way through a write.
- $SIG{TERM} = 'IGNORE';
- $SIG{PIPE} = 'IGNORE';
-
# We don't precompile any functions here, that's done specially in
# mod_perl code.
$invocant->_setup_symbols(qw(:no_xhtml :oldstyle_urls :private_tempfiles
diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm
index 85779fa6b..1016d51e4 100644
--- a/Bugzilla/Config.pm
+++ b/Bugzilla/Config.pm
@@ -16,6 +16,7 @@ use Bugzilla::Constants;
use Bugzilla::Hook;
use Data::Dumper;
use File::Temp;
+use Module::Runtime qw(require_module);
# Don't export localvars by default - people should have to explicitly
# ask for it, as a (probably futile) attempt to stop code using it
@@ -35,7 +36,7 @@ sub _load_params {
my %hook_panels;
foreach my $panel (keys %$panels) {
my $module = $panels->{$panel};
- eval("require $module") || die $@;
+ require_module($module);
my @new_param_list = $module->get_param_list();
$hook_panels{lc($panel)} = { params => \@new_param_list };
}
diff --git a/Bugzilla/Config/Reports.pm b/Bugzilla/Config/Reports.pm
new file mode 100644
index 000000000..26c5aad57
--- /dev/null
+++ b/Bugzilla/Config/Reports.pm
@@ -0,0 +1,37 @@
+# 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::Config::Reports;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla::Config::Common;
+
+our $sortkey = 1100;
+
+sub get_param_list {
+ my $class = shift;
+ my @param_list = (
+ {
+ name => 'report_secbugs_active',
+ type => 'b',
+ default => 1,
+ },
+ {
+ name => 'report_secbugs_emails',
+ type => 't',
+ default => 'bugzilla-admin@mozilla.org'
+ },
+ {
+ name => 'report_secbugs_products',
+ type => 'l',
+ default => '[]'
+ },
+ );
+}
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index 34e4a4cfe..cd478c33e 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -131,6 +131,7 @@ use Memoize;
USAGE_MODE_JSON
USAGE_MODE_TEST
USAGE_MODE_REST
+ USAGE_MODE_MOJO
ERROR_MODE_WEBPAGE
ERROR_MODE_DIE
@@ -138,6 +139,7 @@ use Memoize;
ERROR_MODE_JSON_RPC
ERROR_MODE_TEST
ERROR_MODE_REST
+ ERROR_MODE_MOJO
COLOR_ERROR
COLOR_SUCCESS
@@ -212,7 +214,7 @@ sub BUGZILLA_VERSION {
}
# Location of the remote and local XML files to track new releases.
-use constant REMOTE_FILE => 'http://updates.bugzilla.org/bugzilla-update.xml';
+use constant REMOTE_FILE => 'https://updates.bugzilla.org/bugzilla-update.xml';
use constant LOCAL_FILE => 'bugzilla-update.xml'; # Relative to datadir.
# These are unique values that are unlikely to match a string or a number,
@@ -490,6 +492,7 @@ use constant USAGE_MODE_EMAIL => 3;
use constant USAGE_MODE_JSON => 4;
use constant USAGE_MODE_TEST => 5;
use constant USAGE_MODE_REST => 6;
+use constant USAGE_MODE_MOJO => 7;
# Error modes. Default set by Bugzilla->usage_mode (so ERROR_MODE_WEBPAGE
# usually). Use with Bugzilla->error_mode.
@@ -499,6 +502,7 @@ use constant ERROR_MODE_DIE_SOAP_FAULT => 2;
use constant ERROR_MODE_JSON_RPC => 3;
use constant ERROR_MODE_TEST => 4;
use constant ERROR_MODE_REST => 5;
+use constant ERROR_MODE_MOJO => 6;
# The ANSI colors of messages that command-line scripts use
use constant COLOR_ERROR => 'red';
diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm
index 80404131a..87110aaaa 100644
--- a/Bugzilla/DB.pm
+++ b/Bugzilla/DB.pm
@@ -12,19 +12,13 @@ use Moo;
use DBI;
use DBIx::Connector;
-our %Connector;
-has 'dbh' => (
+has 'connector' => (
is => 'lazy',
- handles => [
- qw[
- begin_work column_info commit disconnect do errstr get_info last_insert_id ping prepare
- primary_key quote_identifier rollback selectall_arrayref selectall_hashref
- selectcol_arrayref selectrow_array selectrow_arrayref selectrow_hashref table_info
- ]
- ],
+ handles => [ qw( dbh ) ],
);
+use Bugzilla::Logging;
use Bugzilla::Constants;
use Bugzilla::Install::Requirements;
use Bugzilla::Install::Util qw(install_string);
@@ -37,13 +31,39 @@ use Bugzilla::DB::Schema;
use Bugzilla::Version;
use List::Util qw(max);
+use Scalar::Util qw(weaken);
use Storable qw(dclone);
+use English qw(-no_match_vars);
+use Module::Runtime qw(require_module);
has [qw(dsn user pass attrs)] => (
is => 'ro',
required => 1,
);
+
+# Install proxy methods to the DBI object.
+# We can't use handles() as DBIx::Connector->dbh has to be called each
+# time we need a DBI handle to ensure the connection is alive.
+{
+ my @DBI_METHODS = qw(
+ begin_work column_info commit do errstr get_info last_insert_id ping prepare
+ primary_key quote_identifier rollback selectall_arrayref selectall_hashref
+ selectcol_arrayref selectrow_array selectrow_arrayref selectrow_hashref table_info
+ );
+ my $stash = Package::Stash->new(__PACKAGE__);
+
+ foreach my $method (@DBI_METHODS) {
+ my $symbol = '&' . $method;
+ $stash->add_symbol(
+ $symbol => sub {
+ my $self = shift;
+ return $self->dbh->$method(@_);
+ }
+ );
+ }
+}
+
#####################################################################
# Constants
#####################################################################
@@ -113,6 +133,12 @@ sub quote {
#####################################################################
sub connect_shadow {
+ state $shadow_dbh;
+ if ($shadow_dbh && $shadow_dbh->bz_in_transaction) {
+ FATAL("Somehow in a transaction at connection time");
+ $shadow_dbh->bz_rollback_transaction();
+ }
+ return $shadow_dbh if $shadow_dbh;
my $params = Bugzilla->params;
die "Tried to connect to non-existent shadowdb"
unless Bugzilla->get_param_with_override('shadowdb');
@@ -130,13 +156,16 @@ sub connect_shadow {
$connect_params->{db_user} = Bugzilla->localconfig->{'shadowdb_user'};
$connect_params->{db_pass} = Bugzilla->localconfig->{'shadowdb_pass'};
}
-
- return _connect($connect_params);
+ return $shadow_dbh = _connect($connect_params);
}
sub connect_main {
- my $lc = Bugzilla->localconfig;
- return _connect(Bugzilla->localconfig);
+ state $main_dbh = _connect(Bugzilla->localconfig);
+ if ($main_dbh->bz_in_transaction) {
+ FATAL("Somehow in a transaction at connection time");
+ $main_dbh->bz_rollback_transaction();
+ }
+ return $main_dbh;
}
sub _connect {
@@ -146,15 +175,13 @@ sub _connect {
my $pkg_module = DB_MODULE->{lc($driver)}->{db};
# do the actual import
- eval ("require $pkg_module")
+ eval { require_module($pkg_module) }
|| die ("'$driver' is not a valid choice for \$db_driver in "
. " localconfig: " . $@);
# instantiate the correct DB specific module
- my $dbh = $pkg_module->new($params);
-
- return $dbh;
+ return $pkg_module->new($params);
}
sub _handle_error {
@@ -278,7 +305,6 @@ sub _get_no_db_connection {
my $dbh;
my %connect_params = %{ Bugzilla->localconfig };
$connect_params{db_name} = '';
- local %Connector = ();
my $conn_success = eval {
$dbh = _connect(\%connect_params);
};
@@ -1263,7 +1289,7 @@ sub bz_rollback_transaction {
# Subclass Helpers
#####################################################################
-sub _build_dbh {
+sub _build_connector {
my ($self) = @_;
my ($dsn, $user, $pass, $override_attrs) =
map { $self->$_ } qw(dsn user pass attrs);
@@ -1289,15 +1315,18 @@ sub _build_dbh {
}
}
my $class = ref $self;
- if ($class->can('on_dbi_connected')) {
- $attributes->{Callbacks} = {
- connected => sub { $class->on_dbi_connected(@_); return },
- }
- }
-
- my $connector = $Connector{"$user.$dsn"} //= DBIx::Connector->new($dsn, $user, $pass, $attributes);
+ weaken($self);
+ $attributes->{Callbacks} = {
+ connected => sub {
+ my ($dbh, $dsn) = @_;
+ INFO("$PROGRAM_NAME connected mysql $dsn");
+ ThrowCodeError('not_in_transaction') if $self && $self->bz_in_transaction;
+ $class->on_dbi_connected(@_) if $class->can('on_dbi_connected');
+ return
+ },
+ };
- return $connector->dbh;
+ return DBIx::Connector->new($dsn, $user, $pass, $attributes);
}
#####################################################################
diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm
index 67ee9071c..e1c19fa51 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -28,6 +28,8 @@ use Carp qw(confess);
use Digest::MD5 qw(md5_hex);
use Hash::Util qw(lock_value unlock_hash lock_keys unlock_keys);
use List::MoreUtils qw(firstidx natatime);
+use Try::Tiny;
+use Module::Runtime qw(require_module);
use Safe;
# Historical, needed for SCHEMA_VERSION = '1.00'
use Storable qw(dclone freeze thaw);
@@ -1876,9 +1878,12 @@ sub new {
if ($driver) {
(my $subclass = $driver) =~ s/^(\S)/\U$1/;
$class .= '::' . $subclass;
- eval "require $class;";
- die "The $class class could not be found ($subclass " .
- "not supported?): $@" if ($@);
+ try {
+ require_module($class);
+ }
+ catch {
+ die "The $class class could not be found ($subclass not supported?): $_";
+ };
}
die "$class is an abstract base class. Instantiate a subclass instead."
if ($class eq __PACKAGE__);
diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm
index f932294b0..70430d40d 100644
--- a/Bugzilla/Error.pm
+++ b/Bugzilla/Error.pm
@@ -20,6 +20,8 @@ our @EXPORT = qw( ThrowCodeError ThrowTemplateError ThrowUserError ThrowErrorPag
use Bugzilla::Constants;
use Bugzilla::WebService::Constants;
use Bugzilla::Util;
+use Bugzilla::Error::User;
+use Bugzilla::Error::Code;
use Carp;
use Data::Dumper;
@@ -40,7 +42,6 @@ sub _in_eval {
sub _throw_error {
my ($name, $error, $vars, $logfunc) = @_;
$vars ||= {};
- $vars->{error} = $error;
# Make sure any transaction is rolled back (if supported).
# If we are within an eval(), do not roll back transactions as we are
@@ -48,6 +49,15 @@ sub _throw_error {
my $dbh = eval { Bugzilla->dbh };
$dbh->bz_rollback_transaction() if ($dbh && $dbh->bz_in_transaction() && !_in_eval());
+ if (Bugzilla->error_mode == ERROR_MODE_MOJO) {
+ my ($type) = $name =~ /^global\/(user|code)-error/;
+ my $class = $type ? 'Bugzilla::Error::' . ucfirst($type) : 'Mojo::Exception';
+ my $e = $class->new($error)->trace(2);
+ $e->vars($vars) if $e->can('vars');
+ CORE::die $e->inspect;
+ }
+
+ $vars->{error} = $error;
my $template = Bugzilla->template;
my $message;
diff --git a/Bugzilla/Error/Base.pm b/Bugzilla/Error/Base.pm
new file mode 100644
index 000000000..ea44c272a
--- /dev/null
+++ b/Bugzilla/Error/Base.pm
@@ -0,0 +1,21 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Error::Base;
+
+use 5.10.1;
+use Mojo::Base 'Mojo::Exception';
+
+has 'vars' => sub { {} };
+
+has 'template' => sub {
+ my $self = shift;
+ my $type = lc( (split(/::/, ref $self))[-1] );
+ return "global/$type-error";
+};
+
+1;
diff --git a/Bugzilla/Error/Code.pm b/Bugzilla/Error/Code.pm
new file mode 100644
index 000000000..27393fd17
--- /dev/null
+++ b/Bugzilla/Error/Code.pm
@@ -0,0 +1,14 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Error::Code;
+
+use 5.10.1;
+use Mojo::Base 'Bugzilla::Error::Base';
+
+
+1;
diff --git a/Bugzilla/Error/User.pm b/Bugzilla/Error/User.pm
new file mode 100644
index 000000000..aa87c9752
--- /dev/null
+++ b/Bugzilla/Error/User.pm
@@ -0,0 +1,13 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Error::User;
+
+use 5.10.1;
+use Mojo::Base 'Bugzilla::Error::Base';
+
+1;
diff --git a/Bugzilla/Error/disabled b/Bugzilla/Error/disabled
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/Bugzilla/Error/disabled
diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm
index c4813abde..beed721f3 100644
--- a/Bugzilla/Install/Requirements.pm
+++ b/Bugzilla/Install/Requirements.pm
@@ -96,7 +96,6 @@ use constant FEATURE_FILES => (
patch_viewer => ['Bugzilla/Attachment/PatchReader.pm'],
updates => ['Bugzilla/Update.pm'],
mfa => ['Bugzilla/MFA/*.pm'],
- markdown => ['Bugzilla/Markdown.pm'],
memcached => ['Bugzilla/Memcache.pm'],
s3 => ['Bugzilla/S3.pm', 'Bugzilla/S3/Bucket.pm', 'Bugzilla/Attachment/S3.pm']
);
diff --git a/Bugzilla/Markdown/GFM.pm b/Bugzilla/Markdown/GFM.pm
index f3f24fc6a..367dc7a53 100644
--- a/Bugzilla/Markdown/GFM.pm
+++ b/Bugzilla/Markdown/GFM.pm
@@ -69,9 +69,9 @@ $FFI->attach(cmark_markdown_to_html => ['opaque', 'int', 'markdown_options_t'] =
);
# This has to happen after something from the main lib is loaded
-$FFI->attach('core_extensions_ensure_registered' => [] => 'void');
+$FFI->attach('cmark_gfm_core_extensions_ensure_registered' => [] => 'void');
-core_extensions_ensure_registered();
+cmark_gfm_core_extensions_ensure_registered();
Bugzilla::Markdown::GFM::SyntaxExtension->SETUP($FFI);
Bugzilla::Markdown::GFM::SyntaxExtensionList->SETUP($FFI);
diff --git a/Bugzilla/Quantum.pm b/Bugzilla/Quantum.pm
index 34d6fc45d..4fddb8da9 100644
--- a/Bugzilla/Quantum.pm
+++ b/Bugzilla/Quantum.pm
@@ -10,6 +10,8 @@ use Mojo::Base 'Mojolicious';
# Needed for its exit() overload, must happen early in execution.
use CGI::Compile;
+use utf8;
+use Encode;
use Bugzilla ();
use Bugzilla::BugMail ();
@@ -20,100 +22,125 @@ use Bugzilla::Install::Requirements ();
use Bugzilla::Logging;
use Bugzilla::Quantum::CGI;
use Bugzilla::Quantum::SES;
+use Bugzilla::Quantum::Home;
use Bugzilla::Quantum::Static;
use Mojo::Loader qw( find_modules );
use Module::Runtime qw( require_module );
use Bugzilla::Util ();
use Cwd qw(realpath);
use MojoX::Log::Log4perl::Tiny;
+use Bugzilla::WebService::Server::REST;
has 'static' => sub { Bugzilla::Quantum::Static->new };
sub startup {
- my ($self) = @_;
-
- DEBUG('Starting up');
- $self->plugin('Bugzilla::Quantum::Plugin::Glue');
- $self->plugin('Bugzilla::Quantum::Plugin::Hostage');
- $self->plugin('Bugzilla::Quantum::Plugin::BlockIP');
- $self->plugin('Bugzilla::Quantum::Plugin::BasicAuth');
-
- 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) = @_;
- $c->res->headers->cache_control('public, max-age=31536000');
- }
- );
- }
-
- my $r = $self->routes;
- Bugzilla::Quantum::CGI->load_all($r);
- Bugzilla::Quantum::CGI->load_one( 'bzapi_cgi', 'extensions/BzAPI/bin/rest.cgi' );
-
- Bugzilla::WebService::Server::REST->preload;
-
- $r->any('/')->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->any('/bzapi')->to('CGI#bzapi_cgi');
- $r->any('/bzapi/*PATH_INFO')->to('CGI#bzapi_cgi');
- $r->any('/extensions/BzAPI/bin/rest.cgi/*PATH_INFO')->to('CGI#bzapi_cgi');
-
- $r->get(
- '/__lbheartbeat__' => sub {
- my $c = shift;
- $c->reply->file( $c->app->home->child('__lbheartbeat__') );
- },
- );
-
- $r->get(
- '/__version__' => sub {
- my $c = shift;
- $c->reply->file( $c->app->home->child('version.json') );
- },
+ my ($self) = @_;
+
+ DEBUG('Starting up');
+ $self->plugin('Bugzilla::Quantum::Plugin::Glue');
+ $self->plugin('Bugzilla::Quantum::Plugin::Hostage')
+ unless $ENV{BUGZILLA_DISABLE_HOSTAGE};
+ $self->plugin('Bugzilla::Quantum::Plugin::BlockIP');
+ $self->plugin('Bugzilla::Quantum::Plugin::Helpers');
+
+ # 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} // 15,
+ clients => $ENV{MOJO_CLIENTS} // 10,
+ spare => $ENV{MOJO_SPARE} // 5,
+ 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) = @_;
+ $c->res->headers->cache_control('public, max-age=31536000');
+ }
);
+ }
+ Bugzilla::WebService::Server::REST->preload;
- $r->get(
- '/version.json' => sub {
- my $c = shift;
- $c->reply->file( $c->app->home->child('version.json') );
- },
- );
+ $self->setup_routes;
- $r->get('/__heartbeat__')->to('CGI#heartbeat_cgi');
- $r->get('/robots.txt')->to('CGI#robots_cgi');
-
- $r->any('/review')->to( 'CGI#page_cgi' => { 'id' => 'splinter.html' } );
- $r->any('/user_profile')->to( 'CGI#page_cgi' => { 'id' => 'user_profile.html' } );
- $r->any('/userprofile')->to( 'CGI#page_cgi' => { 'id' => 'user_profile.html' } );
- $r->any('/request_defer')->to( 'CGI#page_cgi' => { 'id' => 'request_defer.html' } );
- $r->any('/login')->to( 'CGI#index_cgi' => { 'GoAheadAndLogIn' => '1' } );
-
- $r->any( '/:new_bug' => [ new_bug => qr{new[-_]bug} ] )->to('CGI#new_bug_cgi');
-
- 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');
+ Bugzilla::Hook::process('app_startup', {app => $self});
+}
- Bugzilla::Hook::process( 'app_startup', { app => $self } );
+sub setup_routes {
+ my ($self) = @_;
+
+ my $r = $self->routes;
+ Bugzilla::Quantum::CGI->load_all($r);
+ Bugzilla::Quantum::CGI->load_one('bzapi_cgi',
+ 'extensions/BzAPI/bin/rest.cgi');
+
+ $r->get('/home')->to('Home#index');
+ $r->any('/')->to('CGI#index_cgi');
+ $r->any('/bug/<id:num>')->to('CGI#show_bug_cgi');
+ $r->any('/<id:num>')->to('CGI#show_bug_cgi');
+ $r->get(
+ '/testagent.cgi' => sub {
+ my $c = shift;
+ $c->render(text => "OK Mojolicious");
+ }
+ );
+
+ $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->any('/extensions/BzAPI/bin/rest.cgi/*PATH_INFO')->to('CGI#bzapi_cgi');
+ $r->any('/latest/*PATH_INFO')->to('CGI#bzapi_cgi');
+ $r->any('/bzapi/*PATH_INFO')->to('CGI#bzapi_cgi');
+
+ $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');
+
+ $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('/:new_bug' => [new_bug => qr{new[-_]bug}])->to('CGI#new_bug_cgi');
+
+ 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');
}
1;
diff --git a/Bugzilla/Quantum/CGI.pm b/Bugzilla/Quantum/CGI.pm
index 7548c0809..79fbcfde6 100644
--- a/Bugzilla/Quantum/CGI.pm
+++ b/Bugzilla/Quantum/CGI.pm
@@ -19,143 +19,159 @@ use File::Spec::Functions qw(catfile);
use File::Slurper qw(read_text);
use English qw(-no_match_vars);
use Bugzilla::Quantum::Stdout;
-use Bugzilla::Constants qw(bz_locations);
+use Bugzilla::Constants qw(bz_locations USAGE_MODE_BROWSER);
our $C;
my %SEEN;
sub load_all {
- my ( $class, $r ) = @_;
+ 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");
- }
+ foreach my $file (glob '*.cgi') {
+ my $name = _file_to_method($file);
+ $class->load_one($name, $file);
+ $r->any("/$file")->to("CGI#$name");
+ }
}
sub load_one {
- my ( $class, $name, $file ) = @_;
- my $package = __PACKAGE__ . "::$name", my $inner_name = "_$name";
- my $content = read_text( catfile( bz_locations->{cgi_path}, $file ) );
- $content = "package $package; $content";
- untaint($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::Quantum::Stdout', controller => $c; ## no critic (tie)
- try {
- Bugzilla->init_page();
- $inner->();
- }
- catch {
- die $_ unless ref $_ eq 'ARRAY' && $_->[0] eq "EXIT\n";
- }
- finally {
- my $error = shift;
- untie *STDOUT;
- $c->finish unless $error;
- Bugzilla->cleanup;
- CGI::initialize_globals();
- };
+ my ($class, $name, $file) = @_;
+ my $package = __PACKAGE__ . "::$name", my $inner_name = "_$name";
+ my $content = read_text(catfile(bz_locations->{cgi_path}, $file));
+ $content = "package $package; $content";
+ untaint($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::Quantum::Stdout',
+ controller => $c; ## no critic (tie)
+
+ # the finally block calls cleanup.
+ $c->stash->{cleanup_guard}->dismiss;
+ Bugzilla->usage_mode(USAGE_MODE_BROWSER);
+ try {
+ Bugzilla->init_page();
+ $inner->();
+ }
+ catch {
+ die $_ unless _is_exit($_);
+ }
+ finally {
+ my $error = shift;
+ untie *STDOUT;
+ $c->finish if !$error || _is_exit($error);
+ Bugzilla->cleanup;
+ CGI::initialize_globals();
};
+ };
- no strict 'refs'; ## no critic (strict)
- *{$name} = subname( $name, $wrapper );
- return 1;
+ 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 => '' );
-
- 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 ($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 );
- my $prefix = $c->stash->{bmo_prefix} ? '/bmo/' : '/';
-
- 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 => "$prefix$script_name",
- SERVER_NAME => hostname,
- SERVER_PORT => $tx->local_port,
- SERVER_PROTOCOL => $req->is_secure ? 'HTTPS' : 'HTTP', # TODO: Version is missing
- SERVER_SOFTWARE => __PACKAGE__,
- );
+ }
+ my $cgi_query = Mojo::Parameters->new(%captures);
+ $cgi_query->append($req->url->query);
+ my $prefix = $c->stash->{bmo_prefix} ? '/bmo/' : '/';
+
+ 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 => "$prefix$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 );
+ 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;
+ 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;
diff --git a/Bugzilla/Quantum/Home.pm b/Bugzilla/Quantum/Home.pm
new file mode 100644
index 000000000..48d5e47bd
--- /dev/null
+++ b/Bugzilla/Quantum/Home.pm
@@ -0,0 +1,28 @@
+# 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::Quantum::Home;
+use Mojo::Base 'Mojolicious::Controller';
+
+use Bugzilla::Error;
+use Try::Tiny;
+use Bugzilla::Constants;
+
+sub index {
+ my ($c) = @_;
+ $c->bugzilla->login(LOGIN_REQUIRED) or return;
+ try {
+ ThrowUserError('invalid_username', {login => 'batman'})
+ if $c->param('error');
+ $c->render(handler => 'bugzilla', template => 'index');
+ }
+ catch {
+ $c->bugzilla->error_page($_);
+ };
+}
+
+1;
diff --git a/Bugzilla/Quantum/Plugin/BlockIP.pm b/Bugzilla/Quantum/Plugin/BlockIP.pm
index 058ecbf64..974eebff9 100644
--- a/Bugzilla/Quantum/Plugin/BlockIP.pm
+++ b/Bugzilla/Quantum/Plugin/BlockIP.pm
@@ -9,35 +9,35 @@ use constant BLOCK_TIMEOUT => 60 * 60;
my $MEMCACHED = Bugzilla::Memcached->new()->{memcached};
sub register {
- my ( $self, $app, $conf ) = @_;
+ 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_routes => \&_before_routes);
+ $app->helper(block_ip => \&_block_ip);
+ $app->helper(unblock_ip => \&_unblock_ip);
}
sub _block_ip {
- my ( $class, $ip ) = @_;
- $MEMCACHED->set( "block_ip:$ip" => 1, BLOCK_TIMEOUT ) if $MEMCACHED;
+ 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;
+ 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->res->body('Too Many Requests');
- $c->finish;
- }
+ 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->res->body('Too Many Requests');
+ $c->finish;
+ }
}
1;
diff --git a/Bugzilla/Quantum/Plugin/Glue.pm b/Bugzilla/Quantum/Plugin/Glue.pm
index ded4daf15..f04b9c025 100644
--- a/Bugzilla/Quantum/Plugin/Glue.pm
+++ b/Bugzilla/Quantum/Plugin/Glue.pm
@@ -13,89 +13,152 @@ use Try::Tiny;
use Bugzilla::Constants;
use Bugzilla::Logging;
use Bugzilla::RNG ();
-use JSON::MaybeXS qw(decode_json);
+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;
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";
- }
- }
+ 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";
+ }
}
+ }
- # hypnotoad is weird and doesn't look for MOJO_LISTEN itself.
- $app->config(
- hypnotoad => {
- proxy => 1,
- listen => [ $ENV{MOJO_LISTEN} ],
- },
- );
-
- # 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();
- try { Bugzilla->dbh->ping };
- }
- );
-
- $app->hook(
- before_dispatch => sub {
- my ($c) = @_;
- if ( $D{HTTPD_IN_SUBDIR} ) {
- my $path = $c->req->url->path;
- if ( $path =~ s{^/bmo}{}s ) {
- $c->stash->{bmo_prefix} = 1;
- $c->req->url->path($path);
- }
- }
- Log::Log4perl::MDC->put( request_id => $c->req->request_id );
- }
- );
-
-
- $app->secrets( [ Bugzilla->localconfig->{side_wide_secret} ] );
-
- $app->renderer->add_handler(
- 'bugzilla' => sub {
- my ( $renderer, $c, $output, $options ) = @_;
- my $vars = delete $c->stash->{vars};
-
- # Helpers
- my %helper;
- foreach my $method ( grep {m/^\w+\z/} keys %{ $renderer->helpers } ) {
- my $sub = $renderer->helpers->{$method};
- $helper{$method} = sub { $c->$sub(@_) };
- }
- $vars->{helper} = \%helper;
-
- # The controller
- $vars->{c} = $c;
- my $name = $options->{template};
- unless ( $name =~ /\./ ) {
- $name = sprintf '%s.%s.tmpl', $options->{template}, $options->{format};
- }
- my $template = Bugzilla->template;
- $template->process( $name, $vars, $output )
- or die $template->error;
+ $app->hook(
+ before_dispatch => sub {
+ my ($c) = @_;
+ if ($D{HTTPD_IN_SUBDIR}) {
+ my $path = $c->req->url->path;
+ if ($path =~ s{^/bmo}{}s) {
+ $c->stash->{bmo_prefix} = 1;
+ $c->req->url->path($path);
}
- );
+ }
+ Log::Log4perl::MDC->put(request_id => $c->req->request_id);
+ $c->stash->{cleanup_guard} = Scope::Guard->new(\&Bugzilla::cleanup);
+ Bugzilla->usage_mode(USAGE_MODE_MOJO);
+ }
+ );
+
+ $app->secrets([Bugzilla->localconfig->{side_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;
+ $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('/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");
+ my $ip_addr = $c->tx->remote_address;
+
+ return $c->bugzilla->login_redirect_if_required($type)
+ unless ($login_cookie && $user_id);
+
+ my $db_cookie = Bugzilla->dbh->selectrow_array(
+ q{
+ SELECT cookie
+ FROM logincookies
+ WHERE cookie = ?
+ AND userid = ?
+ AND (restrict_ipaddr = 0 OR ipaddr = ?)
+ }, undef, ($login_cookie, $user_id, $ip_addr)
+ );
+
+ 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->log( MojoX::Log::Log4perl::Tiny->new( logger => Log::Log4perl->get_logger( ref $app ) ) );
+ $app->log(MojoX::Log::Log4perl::Tiny->new(
+ logger => Log::Log4perl->get_logger(ref $app)
+ ));
}
1;
diff --git a/Bugzilla/Quantum/Plugin/Helpers.pm b/Bugzilla/Quantum/Plugin/Helpers.pm
new file mode 100644
index 000000000..72dd96cf9
--- /dev/null
+++ b/Bugzilla/Quantum/Plugin/Helpers.pm
@@ -0,0 +1,66 @@
+# 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::Quantum::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;
diff --git a/Bugzilla/Quantum/Static.pm b/Bugzilla/Quantum/Static.pm
index c01f062a4..6ac803e96 100644
--- a/Bugzilla/Quantum/Static.pm
+++ b/Bugzilla/Quantum/Static.pm
@@ -11,20 +11,20 @@ use Bugzilla::Constants qw(bz_locations);
my $LEGACY_RE = qr{
^ (?:static/v[0-9]+\.[0-9]+/) ?
- ( (?:extensions/[^/]+/web|(?:image|graph|skin|j)s)/.+)
+ ( (?:extensions/[^/]+/web|(?:image|skin|j|graph)s)/.+)
$
}xs;
sub file {
- my ( $self, $rel ) = @_;
+ my ($self, $rel) = @_;
- if ( my ($legacy_rel) = $rel =~ $LEGACY_RE ) {
- local $self->{paths} = [ bz_locations->{cgi_path} ];
- return $self->SUPER::file($legacy_rel);
- }
- else {
- return $self->SUPER::file($rel);
- }
+ if (my ($legacy_rel) = $rel =~ $LEGACY_RE) {
+ local $self->{paths} = [bz_locations->{cgi_path}];
+ return $self->SUPER::file($legacy_rel);
+ }
+ else {
+ return $self->SUPER::file($rel);
+ }
}
1;
diff --git a/Bugzilla/Quantum/Stdout.pm b/Bugzilla/Quantum/Stdout.pm
index 9cf19992c..10be0b664 100644
--- a/Bugzilla/Quantum/Stdout.pm
+++ b/Bugzilla/Quantum/Stdout.pm
@@ -13,48 +13,42 @@ use Bugzilla::Logging;
use Encode;
use English qw(-no_match_vars);
-has 'controller' => (
- is => 'ro',
- required => 1,
-);
+has 'controller' => (is => 'ro', required => 1,);
-has '_encoding' => (
- is => 'rw',
- default => '',
-);
+has '_encoding' => (is => 'rw', default => '',);
sub TIEHANDLE { ## no critic (unpack)
- my $class = shift;
+ my $class = shift;
- return $class->new(@_);
+ return $class->new(@_);
}
sub PRINTF { ## no critic (unpack)
- my $self = shift;
- $self->PRINT( sprintf @_ );
+ 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 // '' ) );
+ 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');
- }
+ my ($self, $mode) = @_;
+ if ($mode) {
+ if ($mode eq ':bytes' or $mode eq ':raw') {
+ $self->_encoding('');
+ }
+ elsif ($mode eq ':utf8') {
+ $self->_encoding('utf8');
}
+ }
}
1;
diff --git a/Bugzilla/Report/SecurityRisk.pm b/Bugzilla/Report/SecurityRisk.pm
new file mode 100644
index 000000000..5eb98fd7f
--- /dev/null
+++ b/Bugzilla/Report/SecurityRisk.pm
@@ -0,0 +1,314 @@
+# 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::Report::SecurityRisk;
+
+use 5.10.1;
+use Moo;
+use MooX::StrictConstructor;
+
+use Bugzilla::Error;
+use Bugzilla::Status qw(is_open_state);
+use Bugzilla::Util qw(datetime_from);
+use Bugzilla;
+use DateTime;
+use List::Util qw(any first sum);
+use POSIX qw(ceil);
+use Type::Utils;
+use Types::Standard qw(Num Int Bool Str HashRef ArrayRef CodeRef Map Dict Enum);
+
+my $DateTime = class_type { class => 'DateTime' };
+
+has 'start_date' => (
+ is => 'ro',
+ required => 1,
+ isa => $DateTime,
+);
+
+has 'end_date' => (
+ is => 'ro',
+ required => 1,
+ isa => $DateTime,
+);
+
+has 'products' => (
+ is => 'ro',
+ required => 1,
+ isa => ArrayRef [Str],
+);
+
+has 'sec_keywords' => (
+ is => 'ro',
+ required => 1,
+ isa => ArrayRef [Str],
+);
+
+has 'initial_bug_ids' => (
+ is => 'lazy',
+ isa => ArrayRef [Int],
+);
+
+has 'initial_bugs' => (
+ is => 'lazy',
+ isa => HashRef [
+ Dict [
+ id => Int,
+ product => Str,
+ sec_level => Str,
+ is_open => Bool,
+ created_at => $DateTime,
+ ],
+ ],
+);
+
+has 'check_open_state' => (
+ is => 'ro',
+ isa => CodeRef,
+ default => sub { return \&is_open_state; },
+);
+
+has 'events' => (
+ is => 'lazy',
+ isa => ArrayRef [
+ Dict [
+ bug_id => Int,
+ bug_when => $DateTime,
+ field_name => Enum [qw(bug_status keywords)],
+ removed => Str,
+ added => Str,
+ ],
+ ],
+);
+
+has 'results' => (
+ is => 'lazy',
+ isa => ArrayRef [
+ Dict [
+ date => $DateTime,
+ bugs_by_product => HashRef [
+ Dict [
+ open => ArrayRef [Int],
+ closed => ArrayRef [Int],
+ median_age_open => Num
+ ]
+ ],
+ bugs_by_sec_keyword => HashRef [
+ Dict [
+ open => ArrayRef [Int],
+ closed => ArrayRef [Int],
+ median_age_open => Num
+ ]
+ ],
+ ],
+ ],
+);
+
+sub _build_initial_bug_ids {
+ # TODO: Handle changes in product (e.g. gravyarding) by searching the events table
+ # for changes to the 'product' field where one of $self->products is found in
+ # the 'removed' field, add the related bug id to the list of initial bugs.
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $products = join ', ', map { $dbh->quote($_) } @{ $self->products };
+ my $sec_keywords = join ', ', map { $dbh->quote($_) } @{ $self->sec_keywords };
+ my $query = qq{
+ SELECT
+ bug_id
+ FROM
+ bugs AS bug
+ JOIN products AS product ON bug.product_id = product.id
+ JOIN components AS component ON bug.component_id = component.id
+ JOIN keywords USING (bug_id)
+ JOIN keyworddefs AS keyword ON keyword.id = keywords.keywordid
+ WHERE
+ keyword.name IN ($sec_keywords)
+ AND product.name IN ($products)
+ };
+ return Bugzilla->dbh->selectcol_arrayref($query);
+}
+
+sub _build_initial_bugs {
+ my ($self) = @_;
+ my $bugs = {};
+ my $bugs_list = Bugzilla::Bug->new_from_list( $self->initial_bug_ids );
+ for my $bug (@$bugs_list) {
+ $bugs->{ $bug->id } = {
+ id => $bug->id,
+ product => $bug->product,
+ sec_level => (
+ # Select the first keyword matching one of the target keywords
+ # (of which there _should_ only be one found anyway).
+ first {
+ my $x = $_;
+ grep { lc($_) eq lc( $x->name ) } @{ $self->sec_keywords }
+ }
+ @{ $bug->keyword_objects }
+ )->name,
+ is_open => $self->check_open_state->( $bug->status->name ),
+ created_at => datetime_from( $bug->creation_ts ),
+ };
+ }
+ return $bugs;
+}
+
+sub _build_events {
+ my ($self) = @_;
+ return [] if !(@{$self->initial_bug_ids});
+ my $bug_ids = join ', ', @{ $self->initial_bug_ids };
+ my $start_date = $self->start_date->ymd('-');
+ my $query = qq{
+ SELECT
+ bug_id,
+ bug_when,
+ field.name AS field_name,
+ CONCAT(removed) AS removed,
+ CONCAT(added) AS added
+ FROM
+ bugs_activity
+ JOIN fielddefs AS field ON fieldid = field.id
+ JOIN bugs AS bug USING (bug_id)
+ WHERE
+ bug_id IN ($bug_ids)
+ AND field.name IN ('keywords' , 'bug_status')
+ AND bug_when >= '$start_date 00:00:00'
+ GROUP BY bug_id , bug_when , field.name
+ };
+ my $result = Bugzilla->dbh->selectall_hashref( $query, 'bug_id' );
+ my @events = values %$result;
+ foreach my $event (@events) {
+ $event->{bug_when} = datetime_from( $event->{bug_when} );
+ }
+
+ # We sort by reverse chronological order instead of ORDER BY
+ # since values %hash doesn't guareentee any order.
+ @events = sort { $b->{bug_when} cmp $a->{bug_when} } @events;
+ return \@events;
+}
+
+sub _build_results {
+ my ($self) = @_;
+ my $e = 0;
+ my $bugs = $self->initial_bugs;
+ my @results = ();
+
+ # We must generate a report for each week in the target time interval, regardless of
+ # whether anything changed. The for loop here ensures that we do so.
+ for ( my $report_date = $self->end_date; $report_date >= $self->start_date; $report_date->subtract( weeks => 1 ) ) {
+ # We rewind events while there are still events existing which occured after the start
+ # of the report week. The bugs will reflect a snapshot of how they were at the start of the week.
+ # $self->events is ordered reverse chronologically, so the end of the array is the earliest event.
+ while ( $e < @{ $self->events }
+ && ( @{ $self->events }[$e] )->{bug_when} > $report_date )
+ {
+ my $event = @{ $self->events }[$e];
+ my $bug = $bugs->{ $event->{bug_id} };
+
+ # Undo bug status changes
+ if ( $event->{field_name} eq 'bug_status' ) {
+ $bug->{is_open} = $self->check_open_state->( $event->{removed} );
+ }
+
+ # Undo keyword changes
+ if ( $event->{field_name} eq 'keywords' ) {
+ my $bug_sec_level = $bug->{sec_level};
+ if ( $event->{added} =~ /\b\Q$bug_sec_level\E\b/ ) {
+ # If the currently set sec level was added in this event, remove it.
+ $bug->{sec_level} = undef;
+ }
+ if ( $event->{removed} ) {
+ # If a target sec keyword was removed, add the first one back.
+ my $removed_sec = first { $event->{removed} =~ /\b\Q$_\E\b/ } @{ $self->sec_keywords };
+ $bug->{sec_level} = $removed_sec if ($removed_sec);
+ }
+ }
+
+ $e++;
+ }
+
+ # Remove uncreated bugs
+ foreach my $bug_key ( keys %$bugs ) {
+ if ( $bugs->{$bug_key}->{created_at} > $report_date ) {
+ delete $bugs->{$bug_key};
+ }
+ }
+
+ # Report!
+ my $date_snapshot = $report_date->clone();
+ my @bugs_snapshot = values %$bugs;
+ my $result = {
+ date => $date_snapshot,
+ bugs_by_product => $self->_bugs_by_product( $date_snapshot, @bugs_snapshot ),
+ bugs_by_sec_keyword => $self->_bugs_by_sec_keyword( $date_snapshot, @bugs_snapshot ),
+ };
+ push @results, $result;
+ }
+
+ return [reverse @results];
+}
+
+sub _bugs_by_product {
+ my ( $self, $report_date, @bugs ) = @_;
+ my $result = {};
+ my $groups = {};
+ foreach my $product ( @{ $self->products } ) {
+ $groups->{$product} = [];
+ }
+ foreach my $bug (@bugs) {
+ # We skip over bugs with no sec level which can happen during event rewinding.
+ if ( $bug->{sec_level} ) {
+ push @{ $groups->{ $bug->{product} } }, $bug;
+ }
+ }
+ foreach my $product ( @{ $self->products } ) {
+ my @open = map { $_->{id} } grep { ( $_->{is_open} ) } @{ $groups->{$product} };
+ my @closed = map { $_->{id} } grep { !( $_->{is_open} ) } @{ $groups->{$product} };
+ my @ages = map { $_->{created_at}->subtract_datetime_absolute($report_date)->seconds / 86_400; }
+ grep { ( $_->{is_open} ) } @{ $groups->{$product} };
+ $result->{$product} = {
+ open => \@open,
+ closed => \@closed,
+ median_age_open => @ages ? _median(@ages) : 0,
+ };
+ }
+
+ return $result;
+}
+
+sub _bugs_by_sec_keyword {
+ my ( $self, $report_date, @bugs ) = @_;
+ my $result = {};
+ my $groups = {};
+ foreach my $sec_keyword ( @{ $self->sec_keywords } ) {
+ $groups->{$sec_keyword} = [];
+ }
+ foreach my $bug (@bugs) {
+ # We skip over bugs with no sec level which can happen during event rewinding.
+ if ( $bug->{sec_level} ) {
+ push @{ $groups->{ $bug->{sec_level} } }, $bug;
+ }
+ }
+ foreach my $sec_keyword ( @{ $self->sec_keywords } ) {
+ my @open = map { $_->{id} } grep { ( $_->{is_open} ) } @{ $groups->{$sec_keyword} };
+ my @closed = map { $_->{id} } grep { !( $_->{is_open} ) } @{ $groups->{$sec_keyword} };
+ my @ages = map { $_->{created_at}->subtract_datetime_absolute($report_date)->seconds / 86_400 }
+ grep { ( $_->{is_open} ) } @{ $groups->{$sec_keyword} };
+ $result->{$sec_keyword} = {
+ open => \@open,
+ closed => \@closed,
+ median_age_open => @ages ? _median(@ages) : 0,
+ };
+ }
+
+ return $result;
+}
+
+sub _median {
+ # From tlm @ https://www.perlmonks.org/?node_id=474564. Jul 14, 2005
+ return sum( ( sort { $a <=> $b } @_ )[ int( $#_ / 2 ), ceil( $#_ / 2 ) ] ) / 2;
+}
+
+1;
diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm
index cdeb54a50..36435d637 100644
--- a/Bugzilla/Template.pm
+++ b/Bugzilla/Template.pm
@@ -1031,7 +1031,7 @@ sub create {
return '' unless @sigs;
# use a URI object to encode the query string part.
- my $uri = URI->new(Bugzilla->localconfig->{urlbase} . 'static/metricsgraphics/socorro-lens.html');
+ my $uri = URI->new(Bugzilla->localconfig->{urlbase} . 'metricsgraphics/socorro-lens.html');
$uri->query_form('s' => join("\\", @sigs));
return $uri;
},
diff --git a/Bugzilla/Test/Util.pm b/Bugzilla/Test/Util.pm
index 02c842658..9fbc151f7 100644
--- a/Bugzilla/Test/Util.pm
+++ b/Bugzilla/Test/Util.pm
@@ -12,9 +12,12 @@ use strict;
use warnings;
use base qw(Exporter);
-our @EXPORT = qw(create_user);
+our @EXPORT = qw(create_user issue_api_key mock_useragent_tx);
use Bugzilla::User;
+use Bugzilla::User::APIKey;
+use Mojo::Message::Response;
+use Test2::Tools::Mock qw(mock);
sub create_user {
my ($login, $password, %extra) = @_;
@@ -29,4 +32,38 @@ sub create_user {
});
}
+sub issue_api_key {
+ my ($login, $given_api_key) = @_;
+ my $user = Bugzilla::User->check({ name => $login });
+
+ my $params = {
+ user_id => $user->id,
+ description => 'Bugzilla::Test::Util::issue_api_key',
+ api_key => $given_api_key,
+ };
+
+ if ($given_api_key) {
+ return Bugzilla::User::APIKey->create_special($params);
+ } else {
+ return Bugzilla::User::APIKey->create($params);
+ }
+}
+
+sub _json_content_type { $_->headers->content_type('application/json') }
+
+sub mock_useragent_tx {
+ my ($body, $modify) = @_;
+ $modify //= \&_json_content_type;
+
+ my $res = Mojo::Message::Response->new;
+ $res->code(200);
+ $res->body($body);
+ if ($modify) {
+ local $_ = $res;
+ $modify->($res);
+ }
+
+ return mock({result => $res});
+}
+
1;
diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm
index 4b12f836b..8e51db45d 100644
--- a/Bugzilla/Token.pm
+++ b/Bugzilla/Token.pm
@@ -20,7 +20,6 @@ use Bugzilla::User;
use Date::Format;
use Date::Parse;
use File::Basename;
-use Digest::MD5 qw(md5_hex);
use Digest::SHA qw(hmac_sha256_base64);
use Encode;
use JSON qw(encode_json decode_json);
@@ -254,15 +253,15 @@ sub issue_hash_token {
my $user_id = Bugzilla->user->id || remote_ip();
# The concatenated string is of the form
- # token creation time + site-wide secret + user ID (either ID or remote IP) + data
- my @args = ($time, Bugzilla->localconfig->{'site_wide_secret'}, $user_id, @$data);
+ # token creation time + user ID (either ID or remote IP) + data
+ my @args = ($time, $user_id, @$data);
my $token = join('*', @args);
- # Wide characters cause md5_hex() to die.
- if (Bugzilla->params->{'utf8'}) {
- utf8::encode($token) if utf8::is_utf8($token);
- }
- $token = md5_hex($token);
+ # $token needs to be a byte string.
+ utf8::encode($token);
+ $token = hmac_sha256_base64($token, Bugzilla->localconfig->{'site_wide_secret'});
+ $token =~ s/\+/-/g;
+ $token =~ s/\//_/g;
# Prepend the token creation time, unencrypted, so that the token
# lifetime can be validated.
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index 4a58043a0..afd310eb0 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -34,7 +34,7 @@ use Role::Tiny::With;
use base qw(Bugzilla::Object Exporter);
@Bugzilla::User::EXPORT = qw(is_available_username
- login_to_id user_id_to_login
+ login_to_id user_id_to_login
USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS
MATCH_SKIP_CONFIRM
);
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm
index feb541c2e..61a95e07d 100644
--- a/Bugzilla/WebService/Bug.pm
+++ b/Bugzilla/WebService/Bug.pm
@@ -666,7 +666,7 @@ sub possible_duplicates {
include_fields => Optional [ ArrayRef [Str] ],
Bugzilla_api_token => Optional [Str]
];
-
+
ThrowCodeError( 'param_invalid', { function => 'Bug.possible_duplicates', param => 'A param' } )
if !$params_type->check($params);
@@ -674,10 +674,10 @@ sub possible_duplicates {
if ($params->{id}) {
my $bug = Bugzilla::Bug->check({ id => $params->{id}, cache => 1 });
$summary = $bug->short_desc;
- }
+ }
elsif ($params->{summary}) {
$summary = $params->{summary};
- }
+ }
else {
ThrowCodeError('param_required',
{ function => 'Bug.possible_duplicates', param => 'id or summary' });
@@ -701,7 +701,7 @@ sub possible_duplicates {
if ($params->{id}) {
@$possible_dupes = grep { $_->id != $params->{id} } @$possible_dupes;
}
-
+
my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes;
$self->_add_update_tokens($params, $possible_dupes, \@hashes);
return { bugs => \@hashes };
@@ -1412,6 +1412,9 @@ sub _bug_to_hash {
if (filter_wants $params, 'dupe_of') {
$item{'dupe_of'} = $self->type('int', $bug->dup_id);
}
+ if (filter_wants $params, 'duplicates') {
+ $item{'duplicates'} = [ map { $self->type('int', $_->id) } @{ $bug->duplicates } ];
+ }
if (filter_wants $params, 'groups') {
my @groups = map { $self->type('string', $_->name) }
@{ $bug->groups_in };
@@ -1459,7 +1462,8 @@ sub _bug_to_hash {
elsif ($field->type == FIELD_TYPE_DATETIME
|| $field->type == FIELD_TYPE_DATE)
{
- $item{$name} = $self->type('dateTime', $bug->$name);
+ my $value = $bug->$name;
+ $item{$name} = defined($value) ? $self->type('dateTime', $value) : undef;
}
elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
my @values = map { $self->type('string', $_) } @{ $bug->$name };
@@ -2594,6 +2598,10 @@ C<array> of C<int>s. The ids of bugs that this bug "depends on".
C<int> The bug ID of the bug that this bug is a duplicate of. If this bug
isn't a duplicate of any bug, this will be null.
+=item C<duplicates>
+
+C<array> of C<int>s. The ids of bugs that are marked as duplicate of this bug.
+
=item C<estimated_time>
C<double> The number of hours that it was estimated that this bug would
@@ -2911,6 +2919,8 @@ and all custom fields.
=item The C<actual_time> item was added to the C<bugs> return value
in Bugzilla B<4.4>.
+=item The C<duplicates> array was added in Bugzilla B<6.0>.
+
=back
=back
diff --git a/Bugzilla/WebService/Bugzilla.pm b/Bugzilla/WebService/Bugzilla.pm
index 145502445..8e95028cb 100644
--- a/Bugzilla/WebService/Bugzilla.pm
+++ b/Bugzilla/WebService/Bugzilla.pm
@@ -87,7 +87,7 @@ sub time {
sub jobqueue_status {
my ( $self, $params ) = @_;
-
+
Bugzilla->login(LOGIN_REQUIRED);
my $dbh = Bugzilla->dbh;
@@ -98,7 +98,7 @@ sub jobqueue_status {
(SELECT COUNT(*)
FROM ts_error
WHERE ts_error.jobid = j.jobid
- )
+ )
, 0) AS errors
FROM ts_job j
INNER JOIN ts_funcmap f
diff --git a/Bugzilla/WebService/JSON.pm b/Bugzilla/WebService/JSON.pm
new file mode 100644
index 000000000..5c28b20f4
--- /dev/null
+++ b/Bugzilla/WebService/JSON.pm
@@ -0,0 +1,64 @@
+# 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::WebService::JSON;
+use 5.10.1;
+use Moo;
+
+use Bugzilla::Logging;
+use Bugzilla::WebService::JSON::Box;
+use JSON::MaybeXS;
+use Scalar::Util qw(refaddr blessed);
+use Package::Stash;
+
+use constant Box => 'Bugzilla::WebService::JSON::Box';
+
+has 'json' => (
+ init_arg => undef,
+ is => 'lazy',
+ handles => {_encode => 'encode', _decode => 'decode'},
+);
+
+sub encode {
+ my ($self, $value) = @_;
+ return Box->new(json => $self, value => $value);
+}
+
+sub decode {
+ my ($self, $box) = @_;
+
+ if (blessed($box) && $box->isa(Box)) {
+ return $box->value;
+ }
+ else {
+ return $self->_decode($box);
+ }
+}
+
+sub _build_json { JSON::MaybeXS->new }
+
+# delegation all the json options to the real json encoder.
+{
+ my @json_methods = qw(
+ utf8 ascii pretty canonical
+ allow_nonref allow_blessed convert_blessed
+ );
+ my $stash = Package::Stash->new(__PACKAGE__);
+ foreach my $method (@json_methods) {
+ my $symbol = '&' . $method;
+ $stash->add_symbol(
+ $symbol => sub {
+ my $self = shift;
+ $self->json->$method(@_);
+ return $self;
+ }
+ );
+ }
+}
+
+
+1;
diff --git a/Bugzilla/WebService/JSON/Box.pm b/Bugzilla/WebService/JSON/Box.pm
new file mode 100644
index 000000000..fc39aeee8
--- /dev/null
+++ b/Bugzilla/WebService/JSON/Box.pm
@@ -0,0 +1,43 @@
+# 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::WebService::JSON::Box;
+use 5.10.1;
+use Moo;
+
+use overload '${}' => 'value', '""' => 'to_string', fallback => 1;
+
+has 'value' => (is => 'ro', required => 1);
+has 'json' => (is => 'ro', required => 1);
+has 'label' => (is => 'lazy');
+has 'encode' => (init_arg => undef, is => 'lazy', predicate => 'is_encoded');
+
+sub TO_JSON {
+ my ($self) = @_;
+
+ return $self->to_string;
+}
+
+sub to_string {
+ my ($self) = @_;
+
+ return $self->is_encoded ? $self->encode : $self->label;
+}
+
+sub _build_encode {
+ my ($self) = @_;
+
+ return $self->json->_encode($self->value);
+}
+
+sub _build_label {
+ my ($self) = @_;
+
+ return "" . $self->value;
+}
+
+1;
diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm
index 6ca3fee90..cdd8a0a92 100644
--- a/Bugzilla/WebService/Product.pm
+++ b/Bugzilla/WebService/Product.pm
@@ -64,10 +64,13 @@ sub get_accessible_products {
}
# Get a list of actual products, based on list of ids or names
+our %FLAG_CACHE;
sub get {
my ($self, $params) = validate(@_, 'ids', 'names', 'type');
my $user = Bugzilla->user;
+ Bugzilla->request_cache->{bz_etag_disable} = 1;
+
defined $params->{ids} || defined $params->{names} || defined $params->{type}
|| ThrowCodeError("params_required", { function => "Product.get",
params => ['ids', 'names', 'type'] });
@@ -134,6 +137,7 @@ sub get {
}
# Now create a result entry for each.
+ local %FLAG_CACHE = ();
my @products = map { $self->_product_to_hash($params, $_) }
@requested_products;
return { products => \@products };
@@ -215,6 +219,8 @@ sub _component_to_hash {
$self->type('email', $component->default_assignee->login),
default_qa_contact =>
$self->type('email', $component->default_qa_contact->login),
+ triage_owner =>
+ $self->type('email', $component->triage_owner->login),
sort_key => # sort_key is returned to match Bug.fields
0,
is_active =>
@@ -225,11 +231,11 @@ sub _component_to_hash {
$field_data->{flag_types} = {
bug =>
[map {
- $self->_flag_type_to_hash($_)
+ $FLAG_CACHE{ $_->id } //= $self->_flag_type_to_hash($_)
} @{$component->flag_types->{'bug'}}],
attachment =>
[map {
- $self->_flag_type_to_hash($_)
+ $FLAG_CACHE{ $_->id } //= $self->_flag_type_to_hash($_)
} @{$component->flag_types->{'attachment'}}],
};
}
@@ -238,8 +244,8 @@ sub _component_to_hash {
}
sub _flag_type_to_hash {
- my ($self, $flag_type, $params) = @_;
- return filter $params, {
+ my ($self, $flag_type) = @_;
+ return {
id =>
$self->type('int', $flag_type->id),
name =>
@@ -262,7 +268,7 @@ sub _flag_type_to_hash {
$self->type('int', $flag_type->grant_group_id),
request_group =>
$self->type('int', $flag_type->request_group_id),
- }, undef, 'flag_types';
+ };
}
sub _version_to_hash {
@@ -548,6 +554,11 @@ default.
C<string> The login name of the user who will be set as the QA Contact for
new bugs by default.
+=item C<triage_owner>
+
+C<string> The login name of the user who is named as the Triage Owner of the
+component.
+
=item C<sort_key>
C<int> Components, when displayed in a list, are sorted first by this integer
diff --git a/Bugzilla/WebService/README b/Bugzilla/WebService/README
index bbe320979..788c550bb 100644
--- a/Bugzilla/WebService/README
+++ b/Bugzilla/WebService/README
@@ -7,11 +7,11 @@ Our goal is to make JSON::RPC and XMLRPC::Lite both work with the same code.
The problem is that these both pass different things for $self to WebService
methods.
-When XMLRPC::Lite calls a method, $self is the name of the *class* the
+When XMLRPC::Lite calls a method, $self is the name of the *class* the
method is in. For example, if we call Bugzilla.version(), the first argument
is Bugzilla::WebService::Bugzilla. So in order to have $self
(our first argument) act correctly in XML-RPC, we make all WebService
-classes use base qw(Bugzilla::WebService).
+classes use base qw(Bugzilla::WebService).
When JSON::RPC calls a method, $self is the JSON-RPC *server object*. In other
words, it's an instance of Bugzilla::WebService::Server::JSONRPC. So we have
diff --git a/Bugzilla/WebService/Server.pm b/Bugzilla/WebService/Server.pm
index a76c4c48c..c4bd3e605 100644
--- a/Bugzilla/WebService/Server.pm
+++ b/Bugzilla/WebService/Server.pm
@@ -11,12 +11,15 @@ use 5.10.1;
use strict;
use warnings;
+use Bugzilla::Logging;
use Bugzilla::Error;
use Bugzilla::Util qw(datetime_from);
use Digest::MD5 qw(md5_base64);
use Scalar::Util qw(blessed);
use Storable qw(freeze);
+use Module::Runtime qw(require_module);
+use Try::Tiny;
sub handle_login {
my ($self, $class, $method, $full_method) = @_;
@@ -30,8 +33,13 @@ sub handle_login {
Bugzilla->request_cache->{dont_persist_session} = 1;
}
- eval "require $class";
- ThrowCodeError('unknown_method', {method => $full_method}) if $@;
+ try {
+ require_module($class);
+ }
+ catch {
+ ThrowCodeError('unknown_method', {method => $full_method});
+ FATAL($_);
+ };
return if ($class->login_exempt($method)
and !defined Bugzilla->input_params->{Bugzilla_login});
Bugzilla->login();
@@ -73,7 +81,11 @@ sub datetime_format_outbound {
sub bz_etag {
my ($self, $data) = @_;
my $cache = Bugzilla->request_cache;
- if (defined $data) {
+
+ if (Bugzilla->request_cache->{bz_etag_disable}) {
+ return undef;
+ }
+ elsif (defined $data) {
# Serialize the data if passed a reference
local $Storable::canonical = 1;
$data = freeze($data) if ref $data;
diff --git a/Bugzilla/WebService/Server/JSONRPC.pm b/Bugzilla/WebService/Server/JSONRPC.pm
index 093167048..12a3143cc 100644
--- a/Bugzilla/WebService/Server/JSONRPC.pm
+++ b/Bugzilla/WebService/Server/JSONRPC.pm
@@ -31,7 +31,9 @@ use Bugzilla::Util;
use HTTP::Message;
use MIME::Base64 qw(decode_base64 encode_base64);
+use Scalar::Util qw(blessed);
use List::MoreUtils qw(none);
+use Bugzilla::WebService::JSON;
#####################################
# Public JSON::RPC Method Overrides #
@@ -48,7 +50,7 @@ sub new {
sub create_json_coder {
my $self = shift;
- my $json = $self->SUPER::create_json_coder(@_);
+ my $json = Bugzilla::WebService::JSON->new;
$json->allow_blessed(1);
$json->convert_blessed(1);
$json->allow_nonref(1);
@@ -83,6 +85,9 @@ sub response {
# Implement JSONP.
if (my $callback = $self->_bz_callback) {
my $content = $response->content;
+ if (blessed $content) {
+ $content = $content->encode;
+ }
# Prepend the JSONP response with /**/ in order to protect
# against possible encoding attacks (e.g., affecting Flash).
$response->content("/**/$callback($content)");
@@ -110,7 +115,12 @@ sub response {
else {
push(@header_args, "-ETag", $etag) if $etag;
print $cgi->header(-status => $response->code, @header_args);
- print $response->content;
+ my $content = $response->content;
+ if (blessed $content) {
+ $content = $content->encode;
+ utf8::encode($content);
+ }
+ print $content;
}
}
diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm
index 13896b248..5d8367410 100644
--- a/Bugzilla/WebService/Server/REST.pm
+++ b/Bugzilla/WebService/Server/REST.pm
@@ -165,6 +165,7 @@ sub response {
my $template = Bugzilla->template;
$content = "";
+ $result->encode if blessed $result;
$template->process("rest.html.tmpl", { result => $result }, \$content)
|| ThrowTemplateError($template->error());
diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm
index d462c884a..ce5586911 100644
--- a/Bugzilla/WebService/Util.pm
+++ b/Bugzilla/WebService/Util.pm
@@ -11,6 +11,7 @@ use 5.10.1;
use strict;
use warnings;
+use Bugzilla::Logging;
use Bugzilla::Flag;
use Bugzilla::FlagType;
use Bugzilla::Error;
@@ -18,6 +19,8 @@ use Bugzilla::WebService::Constants;
use Storable qw(dclone);
use URI::Escape qw(uri_unescape);
+use Type::Params qw( compile );
+use Types::Standard -all;
use base qw(Exporter);
@@ -217,6 +220,17 @@ sub _delete_bad_keys {
sub validate {
my ($self, $params, @keys) = @_;
+ my $cache_key = join('|', (caller(1))[3], sort @keys);
+ # Type->of() is the same as Type[], used here because it is easier
+ # to chain with plus_coercions.
+ state $array_of_nonrefs = ArrayRef->of(Maybe[Value])->plus_coercions(
+ Maybe[Value], q{ [ $_ ] },
+ );
+ state $type_cache = {};
+ my $params_type = $type_cache->{$cache_key} //= do {
+ my %fields = map { $_ => Optional[$array_of_nonrefs] } @keys;
+ Maybe[ Dict[%fields, slurpy Any] ];
+ };
# If $params is defined but not a reference, then we weren't
# sent any parameters at all, and we're getting @keys where
@@ -226,12 +240,10 @@ sub validate {
# If @keys is not empty then we convert any named
# parameters that have scalar values to arrayrefs
# that match.
- foreach my $key (@keys) {
- if (exists $params->{$key}) {
- $params->{$key} = ref $params->{$key}
- ? $params->{$key}
- : [ $params->{$key} ];
- }
+ $params = $params_type->coerce($params);
+ if (my $type_error = $params_type->validate($params)) {
+ FATAL("validate() found type error: $type_error");
+ ThrowUserError('invalid_params', { type_error => $type_error } ) if $type_error;
}
return ($self, $params);