diff options
author | Dylan William Hardison <dylan@hardison.net> | 2018-10-14 18:19:50 +0200 |
---|---|---|
committer | Dylan William Hardison <dylan@hardison.net> | 2018-10-14 18:19:50 +0200 |
commit | ce00a61057535d49aa0e83181a1d317d7842571b (patch) | |
tree | 280243de9ff791449fb2c82f3f0f2b9bd931d5b2 /Bugzilla | |
parent | 6367a26da4093a8379e370ef328e9507c98b2e7e (diff) | |
parent | 6657fa9f5210d5b5a9b14c0cba6882bd56232054 (diff) | |
download | bugzilla-ce00a61057535d49aa0e83181a1d317d7842571b.tar.gz bugzilla-ce00a61057535d49aa0e83181a1d317d7842571b.tar.xz |
Merge remote-tracking branch 'bmo/master'
Diffstat (limited to 'Bugzilla')
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); |