From 2e6e3e13587ee526ba94faabd5551e67508518f5 Mon Sep 17 00:00:00 2001 From: dklawren Date: Fri, 7 Sep 2018 06:04:50 -0400 Subject: Bug 1488292 - Remove MozReview extension from BMO code tree as MozReview is being decommissioned --- Bugzilla/CGI.pm | 6 ------ 1 file changed, 6 deletions(-) (limited to 'Bugzilla') diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm index 6236b015a..985e504f7 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; } -- cgit v1.2.3-24-g4f1b From a91453b19c462929b3ab77927b0d0a6807558b92 Mon Sep 17 00:00:00 2001 From: Israel Madueme Date: Mon, 10 Sep 2018 12:34:56 -0400 Subject: Bug 1479466 - Add Security Bugs Report Adds the security bugs report with open count and median age open of sec-critical and sec-high bugs. --- Bugzilla/Config/Reports.pm | 37 +++++ Bugzilla/Report/SecurityRisk.pm | 318 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 355 insertions(+) create mode 100644 Bugzilla/Config/Reports.pm create mode 100644 Bugzilla/Report/SecurityRisk.pm (limited to 'Bugzilla') 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/Report/SecurityRisk.pm b/Bugzilla/Report/SecurityRisk.pm new file mode 100644 index 000000000..1b62d476c --- /dev/null +++ b/Bugzilla/Report/SecurityRisk.pm @@ -0,0 +1,318 @@ +# 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 Bugzilla; +use Bugzilla::Error; +use Bugzilla::Status qw(is_open_state); +use Bugzilla::Util qw(datetime_from); + +use DateTime; +use List::Util qw(any first sum); +use Moo; +use MooX::StrictConstructor; +use POSIX qw(ceil); +use Types::Standard qw(Num Int Bool Str HashRef ArrayRef CodeRef Map Dict Enum); +use Type::Utils; + +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 < scalar @{ $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; + unshift @results, + { + 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 ), + }; + } + + return \@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; -- cgit v1.2.3-24-g4f1b From d3e00529333fb2eadac9c22a45f20c1050401952 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Thu, 13 Sep 2018 22:37:55 +0200 Subject: Bug 1490708 - Ensure we always call DBIx::Connector->dbh before any DBI method (#744) The code didn't allow a way of doing this without a lot of work. So I had to take the following approach: The 'dbh' attribute is now a method that delegates to DBIx::Connector's dbh method. Per the docs, ->dbh() "Returns the connection's database handle. It will use a an existing handle if there is one, if the process has not been forked or a new thread spawned, and if the database is pingable. Otherwise, it will instantiate, cache, and return a new handle." Then there is the matter of the 'handles' on dbh. I've used Package::Stash to insert proxy methods into the class when it is loaded. --- Bugzilla/DB.pm | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) (limited to 'Bugzilla') diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm index 80404131a..cd5954219 100644 --- a/Bugzilla/DB.pm +++ b/Bugzilla/DB.pm @@ -14,15 +14,9 @@ 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::Constants; @@ -44,6 +38,29 @@ has [qw(dsn user pass attrs)] => ( 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 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 + ); + 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 ##################################################################### @@ -152,9 +169,7 @@ sub _connect { # instantiate the correct DB specific module - my $dbh = $pkg_module->new($params); - - return $dbh; + return $pkg_module->new($params); } sub _handle_error { @@ -1263,7 +1278,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); @@ -1295,9 +1310,7 @@ sub _build_dbh { } } - my $connector = $Connector{"$user.$dsn"} //= DBIx::Connector->new($dsn, $user, $pass, $attributes); - - return $connector->dbh; + return $Connector{"$user.$dsn"} //= DBIx::Connector->new($dsn, $user, $pass, $attributes); } ##################################################################### -- cgit v1.2.3-24-g4f1b From 54ed8cf5c8a97f9aeccbac30870acebb0059a964 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Thu, 13 Sep 2018 21:23:30 -0400 Subject: no bug - cleanup a few nits in the SecurityRiskReport (#746) - sorted imports, with Moo and MooX::StrictConstructor at the top because they change the behavior of the code. - removed 'scalar' when comparing an array to an integer as it isn't required. - adjusted multi-line first { } to single line since it still fits and perltidy makes it look ugly. - store each 'result' hash in a $result variable, again to make perltidy format better. - change use of 'unshift ARRAY' to 'push ARRAY' and reverse(). The later performs fewer mallocs (push is much more effficient than unshift). Please check if this logic is right. --- Bugzilla/Report/SecurityRisk.pm | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) (limited to 'Bugzilla') diff --git a/Bugzilla/Report/SecurityRisk.pm b/Bugzilla/Report/SecurityRisk.pm index 1b62d476c..5eb98fd7f 100644 --- a/Bugzilla/Report/SecurityRisk.pm +++ b/Bugzilla/Report/SecurityRisk.pm @@ -8,19 +8,18 @@ package Bugzilla::Report::SecurityRisk; use 5.10.1; +use Moo; +use MooX::StrictConstructor; -use Bugzilla; 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 Moo; -use MooX::StrictConstructor; use POSIX qw(ceil); -use Types::Standard qw(Num Int Bool Str HashRef ArrayRef CodeRef Map Dict Enum); use Type::Utils; +use Types::Standard qw(Num Int Bool Str HashRef ArrayRef CodeRef Map Dict Enum); my $DateTime = class_type { class => 'DateTime' }; @@ -202,7 +201,7 @@ sub _build_results { # 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 < scalar @{ $self->events } + while ( $e < @{ $self->events } && ( @{ $self->events }[$e] )->{bug_when} > $report_date ) { my $event = @{ $self->events }[$e]; @@ -222,10 +221,7 @@ sub _build_results { } 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 }; + my $removed_sec = first { $event->{removed} =~ /\b\Q$_\E\b/ } @{ $self->sec_keywords }; $bug->{sec_level} = $removed_sec if ($removed_sec); } } @@ -243,15 +239,15 @@ sub _build_results { # Report! my $date_snapshot = $report_date->clone(); my @bugs_snapshot = values %$bugs; - unshift @results, - { + 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 \@results; + return [reverse @results]; } sub _bugs_by_product { -- cgit v1.2.3-24-g4f1b From b8b2a943056adbb112474df7bdf766970a56b2dc Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Tue, 18 Sep 2018 18:19:03 -0400 Subject: Bug 1455495 - Replace apache with Mojolicious --- Bugzilla/Attachment/PatchReader.pm | 15 +-- Bugzilla/CGI.pm | 36 +++-- Bugzilla/Config/General.pm | 6 - Bugzilla/DaemonControl.pm | 26 ++-- Bugzilla/Error.pm | 4 +- Bugzilla/Install/Filesystem.pm | 135 +------------------ Bugzilla/Memcached.pm | 2 + Bugzilla/ModPerl.pm | 118 ---------------- Bugzilla/ModPerl/BasicAuth.pm | 65 --------- Bugzilla/ModPerl/BlockIP.pm | 65 --------- Bugzilla/ModPerl/Hostage.pm | 71 ---------- Bugzilla/ModPerl/StartupFix.pm | 51 ------- Bugzilla/Quantum.pm | 118 ++++++++++++++++ Bugzilla/Quantum/CGI.pm | 160 ++++++++++++++++++++++ Bugzilla/Quantum/Plugin/BasicAuth.pm | 40 ++++++ Bugzilla/Quantum/Plugin/BlockIP.pm | 43 ++++++ Bugzilla/Quantum/Plugin/Glue.pm | 101 ++++++++++++++ Bugzilla/Quantum/Plugin/Hostage.pm | 86 ++++++++++++ Bugzilla/Quantum/SES.pm | 254 +++++++++++++++++++++++++++++++++++ Bugzilla/Quantum/Static.pm | 30 +++++ Bugzilla/Quantum/Stdout.pm | 60 +++++++++ Bugzilla/Template.pm | 1 + Bugzilla/Util.pm | 3 +- Bugzilla/WebService/Server/XMLRPC.pm | 6 +- Bugzilla/WebService/Util.pm | 8 +- 25 files changed, 951 insertions(+), 553 deletions(-) delete mode 100644 Bugzilla/ModPerl.pm delete mode 100644 Bugzilla/ModPerl/BasicAuth.pm delete mode 100644 Bugzilla/ModPerl/BlockIP.pm delete mode 100644 Bugzilla/ModPerl/Hostage.pm delete mode 100644 Bugzilla/ModPerl/StartupFix.pm create mode 100644 Bugzilla/Quantum.pm create mode 100644 Bugzilla/Quantum/CGI.pm create mode 100644 Bugzilla/Quantum/Plugin/BasicAuth.pm create mode 100644 Bugzilla/Quantum/Plugin/BlockIP.pm create mode 100644 Bugzilla/Quantum/Plugin/Glue.pm create mode 100644 Bugzilla/Quantum/Plugin/Hostage.pm create mode 100644 Bugzilla/Quantum/SES.pm create mode 100644 Bugzilla/Quantum/Static.pm create mode 100644 Bugzilla/Quantum/Stdout.pm (limited to 'Bugzilla') diff --git a/Bugzilla/Attachment/PatchReader.pm b/Bugzilla/Attachment/PatchReader.pm index 2cfbf2c6b..8025f5b82 100644 --- a/Bugzilla/Attachment/PatchReader.pm +++ b/Bugzilla/Attachment/PatchReader.pm @@ -116,18 +116,9 @@ sub process_interdiff { $ENV{'PATH'} = $lc->{diffpath}; my ($pid, $interdiff_stdout, $interdiff_stderr); - if ($ENV{MOD_PERL}) { - require Apache2::RequestUtil; - require Apache2::SubProcess; - my $request = Apache2::RequestUtil->request; - (undef, $interdiff_stdout, $interdiff_stderr) = $request->spawn_proc_prog( - $lc->{interdiffbin}, [$old_filename, $new_filename] - ); - } else { - $interdiff_stderr = gensym; - my $pid = open3(gensym, $interdiff_stdout, $interdiff_stderr, - $lc->{interdiffbin}, $old_filename, $new_filename); - } + $interdiff_stderr = gensym; + $pid = open3(gensym, $interdiff_stdout, $interdiff_stderr, + $lc->{interdiffbin}, $old_filename, $new_filename); binmode $interdiff_stdout; # Check for errors diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm index 985e504f7..e76b1c609 100644 --- a/Bugzilla/CGI.pm +++ b/Bugzilla/CGI.pm @@ -117,7 +117,7 @@ sub new { # Under mod_perl, CGI's global variables get reset on each request, # so we need to set them up again every time. - $class->_init_bz_cgi_globals() if $ENV{MOD_PERL}; + $class->_init_bz_cgi_globals(); my $self = $class->SUPER::new(@args); @@ -135,6 +135,7 @@ sub new { # apache collapses // to / in $ENV{PATH_INFO} but not in $self->path_info. # url() requires the full path in ENV in order to generate the correct url. $ENV{PATH_INFO} = $path; + DEBUG("redirecting because we see PATH_INFO and don't like it"); print $self->redirect($self->url(-path => 0, -query => 1)); exit; } @@ -145,6 +146,7 @@ sub new { # Redirect to urlbase if we are not viewing an attachment. if ($self->url_is_attachment_base and $script ne 'attachment.cgi') { + DEBUG("Redirecting to urlbase because the url is in the attachment base and not attachment.cgi"); $self->redirect_to_urlbase(); } @@ -475,11 +477,6 @@ sub _prevent_unsafe_response { print $self->SUPER::header(-type => 'text/html', -status => '403 Forbidden'); if ($content_type ne 'text/html') { print "Untrusted Referer Header\n"; - if ($ENV{MOD_PERL}) { - my $r = $self->r; - $r->rflush; - $r->status(200); - } } exit; } @@ -597,8 +594,25 @@ sub header { $headers{'-link'} .= ', ; rel="preconnect"; crossorigin'; } } - - return $self->SUPER::header(%headers) || ""; + my $headers = $self->SUPER::header(%headers) || ''; + if ($self->server_software eq 'Bugzilla::Quantum::CGI') { + my $c = $Bugzilla::Quantum::CGI::C; + $c->res->headers->parse($headers); + my $status = $c->res->headers->status; + if ($status && $status =~ /^([0-9]+)/) { + $c->res->code($1); + } + elsif ($c->res->headers->location) { + $c->res->code(302); + } + else { + $c->res->code(200); + } + return ''; + } + else { + LOGDIE("Bugzilla::CGI->header() should only be called from inside Bugzilla::Quantum::CGI!"); + } } sub param { @@ -715,6 +729,7 @@ sub redirect { return $self->SUPER::redirect(@_); } +use Bugzilla::Logging; # This helps implement Bugzilla::Search::Recent, and also shortens search # URLs that get POSTed to buglist.cgi. sub redirect_search_url { @@ -763,6 +778,7 @@ sub redirect_search_url { # are only redirected if they're under the CGI_URI_LIMIT though. my $self_url = $self->self_url(); if ($self->request_method() ne 'POST' or length($self_url) < CGI_URI_LIMIT) { + DEBUG("Redirecting search url"); print $self->redirect(-url => $self_url); exit; } @@ -784,10 +800,8 @@ sub redirect_to_https { # XML-RPC clients (SOAP::Lite at least) require a 301 to redirect properly # and do not work with 302. Our redirect really is permanent anyhow, so # it doesn't hurt to make it a 301. + DEBUG("Redirecting to https"); print $self->redirect(-location => $url, -status => 301); - - # When using XML-RPC with mod_perl, we need the headers sent immediately. - $self->r->rflush if $ENV{MOD_PERL}; exit; } diff --git a/Bugzilla/Config/General.pm b/Bugzilla/Config/General.pm index 15688dfd3..c870c7376 100644 --- a/Bugzilla/Config/General.pm +++ b/Bugzilla/Config/General.pm @@ -39,12 +39,6 @@ use constant get_param_list => ( checker => \&check_utf8 }, - { - name => 'shutdownhtml', - type => 'l', - default => '' - }, - { name => 'announcehtml', type => 'l', diff --git a/Bugzilla/DaemonControl.pm b/Bugzilla/DaemonControl.pm index 6ff883af0..5cb32973f 100644 --- a/Bugzilla/DaemonControl.pm +++ b/Bugzilla/DaemonControl.pm @@ -23,7 +23,8 @@ use IO::Async::Protocol::LineStream; use IO::Async::Signal; use IO::Socket; use LWP::Simple qw(get); -use POSIX qw(setsid WEXITSTATUS); +use JSON::MaybeXS qw(encode_json); +use POSIX qw(WEXITSTATUS); use base qw(Exporter); @@ -43,8 +44,14 @@ our %EXPORT_TAGS = ( my $BUGZILLA_DIR = bz_locations->{cgi_path}; my $JOBQUEUE_BIN = catfile( $BUGZILLA_DIR, 'jobqueue.pl' ); my $CEREAL_BIN = catfile( $BUGZILLA_DIR, 'scripts', 'cereal.pl' ); -my $HTTPD_BIN = '/usr/sbin/httpd'; -my $HTTPD_CONFIG = catfile( bz_locations->{confdir}, 'httpd.conf' ); +my $BUGZILLA_BIN = catfile( $BUGZILLA_DIR, 'bugzilla.pl' ); +my $HYPNOTOAD_BIN = catfile( $BUGZILLA_DIR, 'local', 'bin', 'hypnotoad' ); +my @PERL5LIB = ( $BUGZILLA_DIR, catdir($BUGZILLA_DIR, 'lib'), catdir($BUGZILLA_DIR, 'local', 'lib', 'perl5') ); + +my %HTTP_BACKENDS = ( + hypnotoad => [ $HYPNOTOAD_BIN, $BUGZILLA_BIN, '-f' ], + simple => [ $BUGZILLA_BIN, 'daemon' ], +); sub catch_signal { my ($name, @done) = @_; @@ -98,13 +105,12 @@ sub run_httpd { my $exit_f = $loop->new_future; my $httpd = IO::Async::Process->new( code => sub { - - # we have to setsid() to make a new process group - # or else apache will kill its parent. - setsid(); - my @command = ( $HTTPD_BIN, '-DFOREGROUND', '-f' => $HTTPD_CONFIG, @args ); - exec @command - or die "failed to exec $command[0] $!"; + $ENV{BUGZILLA_HTTPD_ARGS} = encode_json(\@args); + $ENV{PERL5LIB} = join(':', @PERL5LIB); + my $backend = $ENV{HTTP_BACKEND} // 'hypnotoad'; + my $command = $HTTP_BACKENDS{ $backend }; + exec @$command + or die "failed to exec $command->[0] $!"; }, on_finish => on_finish($exit_f), on_exception => on_exception( 'httpd', $exit_f ), diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm index 9fcd16386..f932294b0 100644 --- a/Bugzilla/Error.pm +++ b/Bugzilla/Error.pm @@ -31,7 +31,7 @@ use Scalar::Util qw(blessed); sub _in_eval { my $in_eval = 0; for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) { - last if $sub =~ /^ModPerl/; + last if $sub =~ /^Bugzilla::Quantum::CGI::try/; $in_eval = 1 if $sub =~ /^\(eval\)/; } return $in_eval; @@ -196,7 +196,7 @@ sub ThrowTemplateError { # mod_perl overrides exit to call die with this string # we never want to display this to the user - exit if $template_err =~ /\bModPerl::Util::exit\b/; + die $template_err if ref($template_err) eq 'ARRAY' && $template_err->[0] eq "EXIT\n"; state $logger = Log::Log4perl->get_logger('Bugzilla.Error.Template'); $logger->error($template_err); diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm index 003be22e4..317152962 100644 --- a/Bugzilla/Install/Filesystem.pm +++ b/Bugzilla/Install/Filesystem.pm @@ -41,56 +41,11 @@ use English qw(-no_match_vars $OSNAME); use base qw(Exporter); our @EXPORT = qw( update_filesystem - create_htaccess fix_all_file_permissions fix_dir_permissions fix_file_permissions ); -use constant HT_DEFAULT_DENY => <<'EOT'; -# nothing in this directory is retrievable unless overridden by an .htaccess -# in a subdirectory -deny from all -EOT - -use constant HT_GRAPHS_DIR => <<'EOT'; -# Allow access to .png and .gif files. - - Allow from all - - -# And no directory listings, either. -Deny from all -EOT - -use constant HT_WEBDOT_DIR => <<'EOT'; -# Restrict access to .dot files to the public webdot server at research.att.com -# if research.att.com ever changes their IP, or if you use a different -# webdot server, you'll need to edit this - - Allow from 192.20.225.0/24 - Deny from all - - -# Allow access to .png files created by a local copy of 'dot' - - Allow from all - - -# And no directory listings, either. -Deny from all -EOT - -use constant HT_ASSETS_DIR => <<'EOT'; -# Allow access to .css and js files - - Allow from all - - -# And no directory listings, either. -Deny from all -EOT - use constant INDEX_HTML => <<'EOT'; @@ -112,11 +67,6 @@ use constant HTTPD_ENV => qw( NYTPROF_DIR ); -sub HTTPD_ENV_CONF { - my @env = (ENV_KEYS, HTTPD_ENV); - return join( "\n", map { "PerlPassEnv " . $_ } @env ) . "\n"; -} - ############### # Permissions # ############### @@ -230,13 +180,13 @@ sub FILESYSTEM { 'jobqueue-worker.pl' => { perms => OWNER_EXECUTE }, 'clean-bug-user-last-visit.pl' => { perms => WS_EXECUTE }, + 'bugzilla.pl' => { perms => OWNER_EXECUTE }, 'Bugzilla.pm' => { perms => CGI_READ }, "$localconfig*" => { perms => CGI_READ }, 'META.*' => { perms => CGI_READ }, 'MYMETA.*' => { perms => CGI_READ }, 'bugzilla.dtd' => { perms => WS_SERVE }, 'mod_perl.pl' => { perms => WS_SERVE }, - '.htaccess' => { perms => WS_SERVE }, 'cvs-update.log' => { perms => WS_SERVE }, 'scripts/sendunsentbugmail.pl' => { perms => WS_EXECUTE }, 'docs/bugzilla.ent' => { perms => OWNER_WRITE }, @@ -427,9 +377,6 @@ sub FILESYSTEM { "skins/yui3.css" => { perms => CGI_READ, overwrite => 1, contents => $yui3_all_css }, - "$confdir/env.conf" => { perms => CGI_READ, - overwrite => 1, - contents => \&HTTPD_ENV_CONF }, ); # Because checksetup controls the creation of index.html separately @@ -438,54 +385,15 @@ sub FILESYSTEM { 'index.html' => { perms => WS_SERVE, contents => INDEX_HTML } ); - # Because checksetup controls the .htaccess creation separately - # by a localconfig variable, these go in a separate variable from - # %create_files. - # - # Note that these get WS_SERVE as their permission - # because they're *read* by the webserver, even though they're not - # actually, themselves, served. - my %htaccess = ( - "$attachdir/.htaccess" => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - "$libdir/Bugzilla/.htaccess" => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - "$extlib/.htaccess" => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - "$templatedir/.htaccess" => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - 'contrib/.htaccess' => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - 'scripts/.htaccess' => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - 't/.htaccess' => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - 'xt/.htaccess' => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - '.circleci/.htaccess' => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - "$confdir/.htaccess" => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - "$datadir/.htaccess" => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - "$graphsdir/.htaccess" => { perms => WS_SERVE, - contents => HT_GRAPHS_DIR }, - "$webdotdir/.htaccess" => { perms => WS_SERVE, - contents => HT_WEBDOT_DIR }, - "$assetsdir/.htaccess" => { perms => WS_SERVE, - contents => HT_ASSETS_DIR }, - ); - Bugzilla::Hook::process('install_filesystem', { files => \%files, create_dirs => \%create_dirs, non_recurse_dirs => \%non_recurse_dirs, recurse_dirs => \%recurse_dirs, create_files => \%create_files, - htaccess => \%htaccess, }); - my %all_files = (%create_files, %htaccess, %index_html, %files); + my %all_files = (%create_files, %index_html, %files); my %all_dirs = (%create_dirs, %non_recurse_dirs); return { @@ -494,7 +402,6 @@ sub FILESYSTEM { all_dirs => \%all_dirs, create_files => \%create_files, - htaccess => \%htaccess, index_html => \%index_html, all_files => \%all_files, }; @@ -542,13 +449,6 @@ sub update_filesystem { _rename_file($oldparamsfile, "$datadir/$oldparamsfile"); } - # Remove old assets htaccess file to force recreation with correct values. - if (-e "$assetsdir/.htaccess") { - if (read_file("$assetsdir/.htaccess") =~ //) { - unlink("$assetsdir/.htaccess"); - } - } - _create_files(%files); if ($params->{index_html}) { _create_files(%{$fs->{index_html}}); @@ -653,27 +553,6 @@ sub _convert_single_file_skins { } } -sub create_htaccess { - _create_files(%{FILESYSTEM()->{htaccess}}); - - # Repair old .htaccess files - - my $webdot_dir = bz_locations()->{'webdotdir'}; - # The public webdot IP address changed. - my $webdot = new IO::File("$webdot_dir/.htaccess", 'r') - || die "$webdot_dir/.htaccess: $!"; - my $webdot_data; - { local $/; $webdot_data = <$webdot>; } - $webdot->close; - if ($webdot_data =~ /192\.20\.225\.10/) { - print "Repairing $webdot_dir/.htaccess...\n"; - $webdot_data =~ s/192\.20\.225\.10/192.20.225.0\/24/g; - $webdot = new IO::File("$webdot_dir/.htaccess", 'w') || die $!; - print $webdot $webdot_data; - $webdot->close; - } -} - sub _rename_file { my ($from, $to) = @_; print install_string('file_rename', { from => $from, to => $to }), "\n"; @@ -984,16 +863,6 @@ Params: C - Whether or not we should create Returns: nothing -=item C - -Description: Creates all of the .htaccess files for Apache, - in the various Bugzilla directories. Also updates - the .htaccess files if they need updating. - -Params: none - -Returns: nothing - =item C Description: Sets all the file permissions on all of Bugzilla's files diff --git a/Bugzilla/Memcached.pm b/Bugzilla/Memcached.pm index 40755aa29..6bbef080a 100644 --- a/Bugzilla/Memcached.pm +++ b/Bugzilla/Memcached.pm @@ -25,6 +25,8 @@ use Sys::Syslog qw(:DEFAULT); use constant MAX_KEY_LENGTH => 250; use constant RATE_LIMIT_PREFIX => "rate:"; +*new = \&_new; + sub _new { my $invocant = shift; my $class = ref($invocant) || $invocant; diff --git a/Bugzilla/ModPerl.pm b/Bugzilla/ModPerl.pm deleted file mode 100644 index 19cd1128f..000000000 --- a/Bugzilla/ModPerl.pm +++ /dev/null @@ -1,118 +0,0 @@ -# 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::ModPerl; - -use 5.10.1; -use strict; -use warnings; - -use File::Find (); -use Cwd (); -use Carp (); - -# We don't need (or want) to use Bugzilla's template subclass. -# it is easier to reason with the code without all the extra things Bugzilla::Template adds -# (and there might be side-effects, since this code is loaded very early in the httpd startup) -use Template (); - -use Bugzilla::ModPerl::BlockIP; -use Bugzilla::ModPerl::Hostage; - -sub apache_config { - my ($class, $cgi_path) = @_; - - Carp::croak "\$cgi_path is required" unless $cgi_path; - - my %htaccess; - $cgi_path = Cwd::realpath($cgi_path); - my $wanted = sub { - package File::Find; - our ($name, $dir); - - if ($name =~ m#/\.htaccess$#) { - open my $fh, '<', $name or die "cannot open $name $!"; - my $contents = do { - local $/ = undef; - <$fh>; - }; - close $fh; - $htaccess{$dir} = { file => $name, contents => $contents, dir => $dir }; - } - }; - - File::Find::find( { wanted => $wanted, no_chdir => 1 }, $cgi_path ); - my $template = Template->new; - my $conf; - my %vars = ( - root_htaccess => delete $htaccess{$cgi_path}, - htaccess_files => [ map { $htaccess{$_} } sort { length $a <=> length $b } keys %htaccess ], - cgi_path => $cgi_path, - ); - $template->process(\*DATA, \%vars, \$conf); - my $apache_version = Apache2::ServerUtil::get_server_version(); - if ($apache_version =~ m!Apache/(\d+)\.(\d+)\.(\d+)!) { - my ($major, $minor, $patch) = ($1, $2, $3); - if ($major > 2 || $major == 2 && $minor >= 4) { - $conf =~ s{^\s+deny\s+from\s+all.*$}{Require all denied}gmi; - $conf =~ s{^\s+allow\s+from\s+all.*$}{Require all granted}gmi; - $conf =~ s{^\s+allow\s+from\s+(\S+).*$}{Require host $1}gmi; - } - } - - return $conf; -} - -1; - -__DATA__ -# 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.) -PerlChildInitHandler "sub { Bugzilla::RNG::srand(); srand(); eval { Bugzilla->dbh->ping } }" -PerlInitHandler Bugzilla::ModPerl::Hostage -PerlAccessHandler Bugzilla::ModPerl::BlockIP - -# It is important to specify ErrorDocuments outside of all directories. -# These used to be in .htaccess, but then things like "AllowEncodedSlashes no" -# mean that urls containing %2f are unstyled. -ErrorDocument 401 /errors/401.html -ErrorDocument 403 /errors/403.html -ErrorDocument 404 /errors/404.html -ErrorDocument 500 /errors/500.html - - - AddHandler perl-script .cgi - # No need to PerlModule these because they're already defined in mod_perl.pl - PerlResponseHandler Bugzilla::ModPerl::ResponseHandler - PerlCleanupHandler Bugzilla::ModPerl::CleanupHandler Apache2::SizeLimit - PerlOptions +ParseHeaders - Options +ExecCGI +FollowSymLinks - DirectoryIndex index.cgi index.html - AllowOverride none - # from [% root_htaccess.file %] - [% root_htaccess.contents FILTER indent %] - - -# AWS SES endpoint for handling mail bounces/complaints - - PerlSetEnv AUTH_VAR_NAME ses_username - PerlSetEnv AUTH_VAR_PASS ses_password - PerlAuthenHandler Bugzilla::ModPerl::BasicAuth - AuthName SES - AuthType Basic - require valid-user - - -# directory rules for all the other places we have .htaccess files -[% FOREACH htaccess IN htaccess_files %] -# from [% htaccess.file %] - - [% htaccess.contents FILTER indent %] - -[% END %] diff --git a/Bugzilla/ModPerl/BasicAuth.pm b/Bugzilla/ModPerl/BasicAuth.pm deleted file mode 100644 index 7248a19f3..000000000 --- a/Bugzilla/ModPerl/BasicAuth.pm +++ /dev/null @@ -1,65 +0,0 @@ -# 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::ModPerl::BasicAuth; -use 5.10.1; -use strict; -use warnings; - -# Protects a mod_perl with Basic HTTP authentication. -# -# Example use: -# -# -# PerlAuthenHandler Bugzilla::ModPerl::BasicAuth -# PerlSetEnv AUTH_VAR_NAME ses_username -# PerlSetEnv AUTH_VAR_PASS ses_password -# AuthName SES -# AuthType Basic -# require valid-user -# -# -# AUTH_VAR_NAME and AUTH_VAR_PASS are the names of variables defined in -# `localconfig` which hold the authentication credentials. - -use Apache2::Const -compile => qw(OK HTTP_UNAUTHORIZED); ## no critic (Freenode::ModPerl) -use Bugzilla::Logging; -use Bugzilla (); - -sub handler { - my $r = shift; - my ($status, $password) = $r->get_basic_auth_pw; - if ($status != Apache2::Const::OK) { - WARN("Got non-OK status: $status when trying to get password"); - return $status - } - - my $auth_var_name = $ENV{AUTH_VAR_NAME}; - my $auth_var_pass = $ENV{AUTH_VAR_PASS}; - unless ($auth_var_name && $auth_var_pass) { - ERROR('AUTH_VAR_NAME and AUTH_VAR_PASS environmental vars not set'); - $r->note_basic_auth_failure; - return Apache2::Const::HTTP_UNAUTHORIZED; - } - - my $auth_user = Bugzilla->localconfig->{$auth_var_name}; - my $auth_pass = Bugzilla->localconfig->{$auth_var_pass}; - unless ($auth_user && $auth_pass) { - ERROR("$auth_var_name and $auth_var_pass not configured"); - $r->note_basic_auth_failure; - return Apache2::Const::HTTP_UNAUTHORIZED; - } - - unless ($r->user eq $auth_user && $password eq $auth_pass) { - $r->note_basic_auth_failure; - WARN('username and password do not match'); - return Apache2::Const::HTTP_UNAUTHORIZED; - } - - return Apache2::Const::OK; -} - -1; diff --git a/Bugzilla/ModPerl/BlockIP.pm b/Bugzilla/ModPerl/BlockIP.pm deleted file mode 100644 index 4e9a4be5c..000000000 --- a/Bugzilla/ModPerl/BlockIP.pm +++ /dev/null @@ -1,65 +0,0 @@ -package Bugzilla::ModPerl::BlockIP; -use 5.10.1; -use strict; -use warnings; - -use Apache2::RequestRec (); -use Apache2::Connection (); - -use Apache2::Const -compile => qw(OK); -use Cache::Memcached::Fast; - -use constant BLOCK_TIMEOUT => 60*60; - -my $MEMCACHED = Bugzilla::Memcached->_new()->{memcached}; -my $STATIC_URI = qr{ - ^/ - (?: extensions/[^/]+/web - | robots\.txt - | __heartbeat__ - | __lbheartbeat__ - | __version__ - | images - | skins - | js - | errors - ) -}xms; - -sub block_ip { - my ($class, $ip) = @_; - $MEMCACHED->set("block_ip:$ip" => 1, BLOCK_TIMEOUT) if $MEMCACHED; -} - -sub unblock_ip { - my ($class, $ip) = @_; - $MEMCACHED->delete("block_ip:$ip") if $MEMCACHED; -} - -sub handler { - my $r = shift; - return Apache2::Const::OK if $r->uri =~ $STATIC_URI; - - my $ip = $r->headers_in->{'X-Forwarded-For'}; - if ($ip) { - $ip = (split(/\s*,\s*/ms, $ip))[-1]; - } - else { - $ip = $r->connection->remote_ip; - } - - if ($MEMCACHED && $MEMCACHED->get("block_ip:$ip")) { - __PACKAGE__->block_ip($ip); - $r->status_line("429 Too Many Requests"); - # 500 is used here because apache 2.2 doesn't understand 429. - # the above line and the return value together mean we produce 429. - # Any other variation doesn't work. - $r->custom_response(500, "Too Many Requests"); - return 429; - } - else { - return Apache2::Const::OK; - } -} - -1; diff --git a/Bugzilla/ModPerl/Hostage.pm b/Bugzilla/ModPerl/Hostage.pm deleted file mode 100644 index a3bdfac58..000000000 --- a/Bugzilla/ModPerl/Hostage.pm +++ /dev/null @@ -1,71 +0,0 @@ -package Bugzilla::ModPerl::Hostage; -use 5.10.1; -use strict; -use warnings; - -use Apache2::Const qw(:common); ## no critic (Freenode::ModPerl) - -sub _attachment_root { - my ($base) = @_; - return undef unless $base; - return $base =~ m{^https?://(?:bug)?\%bugid\%\.([a-zA-Z\.-]+)} - ? $1 - : undef; -} - -sub _attachment_host_regex { - my ($base) = @_; - return undef unless $base; - my $val = $base; - $val =~ s{^https?://}{}s; - $val =~ s{/$}{}s; - my $regex = quotemeta $val; - $regex =~ s/\\\%bugid\\\%/\\d+/g; - return qr/^$regex$/s; -} - -sub handler { - my $r = shift; - state $urlbase = Bugzilla->localconfig->{urlbase}; - state $urlbase_uri = URI->new($urlbase); - state $urlbase_host = $urlbase_uri->host; - state $urlbase_host_regex = qr/^bug(\d+)\.\Q$urlbase_host\E$/; - state $attachment_base = Bugzilla->localconfig->{attachment_base}; - state $attachment_root = _attachment_root($attachment_base); - state $attachment_host_regex = _attachment_host_regex($attachment_base); - - my $hostname = $r->hostname; - return OK if $hostname eq $urlbase_host; - - my $path = $r->uri; - return OK if $path eq '/__lbheartbeat__'; - - if ($attachment_base && $hostname eq $attachment_root) { - $r->headers_out->set(Location => $urlbase); - return REDIRECT; - } - elsif ($attachment_base && $hostname =~ $attachment_host_regex) { - if ($path =~ m{^/attachment\.cgi}s) { - return OK; - } else { - my $new_uri = URI->new($r->unparsed_uri); - $new_uri->scheme($urlbase_uri->scheme); - $new_uri->host($urlbase_host); - $r->headers_out->set(Location => $new_uri); - return REDIRECT; - } - } - elsif (my ($id) = $hostname =~ $urlbase_host_regex) { - my $new_uri = $urlbase_uri->clone; - $new_uri->path('/show_bug.cgi'); - $new_uri->query_form(id => $id); - $r->headers_out->set(Location => $new_uri); - return REDIRECT; - } - else { - $r->headers_out->set(Location => $urlbase); - return REDIRECT; - } -} - -1; \ No newline at end of file diff --git a/Bugzilla/ModPerl/StartupFix.pm b/Bugzilla/ModPerl/StartupFix.pm deleted file mode 100644 index bcc467e9f..000000000 --- a/Bugzilla/ModPerl/StartupFix.pm +++ /dev/null @@ -1,51 +0,0 @@ -# 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::ModPerl::StartupFix; -use 5.10.1; -use strict; -use warnings; - -use Filter::Util::Call; -use Apache2::ServerUtil (); - -# This module is a source filter that removes every subsequent line -# if this is the first time apache has started, -# as reported by Apache2::ServerUtil::restart_count(), which is 1 -# on the first start. - -my $FIRST_STARTUP = <<'CODE'; -warn "Bugzilla::ModPerl::StartupFix: Skipping first startup using source filter\n"; -1; -CODE - -sub import { - my ($class) = @_; - my ($ref) = {}; - filter_add( bless $ref, $class ); -} - -# this will be called for each line. -# For the first line replaced, we insert $FIRST_STARTUP. -# Every subsequent line is replaced with an empty string. -sub filter { - my ($self) = @_; - my ($status); - if ($status = filter_read() > 0) { - if (Apache2::ServerUtil::restart_count() < 2) { - if (!$self->{did_it}) { - $self->{did_it} = 1; - $_ = $FIRST_STARTUP; - } - else { - $_ = ""; - } - } - } - return $status; -} - -1; \ No newline at end of file diff --git a/Bugzilla/Quantum.pm b/Bugzilla/Quantum.pm new file mode 100644 index 000000000..d2352f6d8 --- /dev/null +++ b/Bugzilla/Quantum.pm @@ -0,0 +1,118 @@ +# 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; +use Mojo::Base 'Mojolicious'; + +# Needed for its exit() overload, must happen early in execution. +use CGI::Compile; + +use Bugzilla (); +use Bugzilla::BugMail (); +use Bugzilla::CGI (); +use Bugzilla::Constants qw(bz_locations); +use Bugzilla::Extension (); +use Bugzilla::Install::Requirements (); +use Bugzilla::Logging; +use Bugzilla::Quantum::CGI; +use Bugzilla::Quantum::SES; +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; + +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/')->to('CGI#show_bug_cgi'); + $r->any('/')->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('/extensions/BzAPI/bin/rest.cgi/*PATH_INFO')->to('CGI#bzapi_cgi'); + $r->any('/bzapi/*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') ); + }, + ); + + $r->get( + '/version.json' => sub { + my $c = shift; + $c->reply->file( $c->app->home->child('version.json') ); + }, + ); + + $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 } ); +} + +1; diff --git a/Bugzilla/Quantum/CGI.pm b/Bugzilla/Quantum/CGI.pm new file mode 100644 index 000000000..0a74f1ee5 --- /dev/null +++ b/Bugzilla/Quantum/CGI.pm @@ -0,0 +1,160 @@ +# 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::CGI; +use Mojo::Base 'Mojolicious::Controller'; + +use CGI::Compile; +use Try::Tiny; +use Taint::Util qw(untaint); +use Sys::Hostname; +use Sub::Quote 2.005000; +use Sub::Name; +use Socket qw(AF_INET inet_aton); +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); + +our $C; +my %SEEN; + +sub load_all { + 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"); + } +} + +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 { + untie *STDOUT; + $c->finish; + Bugzilla->cleanup; + CGI::initialize_globals(); + }; + }; + + 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 $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 ); +} + +sub _file_to_method { + my ($name) = @_; + $name =~ s/\./_/s; + $name =~ s/\W+/_/gs; + return $name; +} + +1; diff --git a/Bugzilla/Quantum/Plugin/BasicAuth.pm b/Bugzilla/Quantum/Plugin/BasicAuth.pm new file mode 100644 index 000000000..e17273404 --- /dev/null +++ b/Bugzilla/Quantum/Plugin/BasicAuth.pm @@ -0,0 +1,40 @@ +# 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::BasicAuth; +use 5.10.1; +use Mojo::Base qw(Mojolicious::Plugin); + +use Bugzilla::Logging; +use Carp; + +sub register { + my ( $self, $app, $conf ) = @_; + + $app->renderer->add_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; + } + ); +} + +1; \ No newline at end of file diff --git a/Bugzilla/Quantum/Plugin/BlockIP.pm b/Bugzilla/Quantum/Plugin/BlockIP.pm new file mode 100644 index 000000000..058ecbf64 --- /dev/null +++ b/Bugzilla/Quantum/Plugin/BlockIP.pm @@ -0,0 +1,43 @@ +package Bugzilla::Quantum::Plugin::BlockIP; +use 5.10.1; +use Mojo::Base 'Mojolicious::Plugin'; + +use Bugzilla::Memcached; + +use constant BLOCK_TIMEOUT => 60 * 60; + +my $MEMCACHED = Bugzilla::Memcached->new()->{memcached}; + +sub register { + my ( $self, $app, $conf ) = @_; + + $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; +} + +sub _unblock_ip { + my ( $class, $ip ) = @_; + $MEMCACHED->delete("block_ip:$ip") if $MEMCACHED; +} + +sub _before_routes { + my ($c) = @_; + return if $c->stash->{'mojo.static'}; + + my $ip = $c->tx->remote_address; + if ( $MEMCACHED && $MEMCACHED->get("block_ip:$ip") ) { + $c->block_ip($ip); + $c->res->code(429); + $c->res->message('Too Many Requests'); + $c->res->body('Too Many Requests'); + $c->finish; + } +} + +1; diff --git a/Bugzilla/Quantum/Plugin/Glue.pm b/Bugzilla/Quantum/Plugin/Glue.pm new file mode 100644 index 000000000..ded4daf15 --- /dev/null +++ b/Bugzilla/Quantum/Plugin/Glue.pm @@ -0,0 +1,101 @@ +# 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::Glue; +use 5.10.1; +use Mojo::Base 'Mojolicious::Plugin'; + +use Try::Tiny; +use Bugzilla::Constants; +use Bugzilla::Logging; +use Bugzilla::RNG (); +use JSON::MaybeXS qw(decode_json); + +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"; + } + } + } + + # 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->log( MojoX::Log::Log4perl::Tiny->new( logger => Log::Log4perl->get_logger( ref $app ) ) ); +} + +1; diff --git a/Bugzilla/Quantum/Plugin/Hostage.pm b/Bugzilla/Quantum/Plugin/Hostage.pm new file mode 100644 index 000000000..cbde7b5ee --- /dev/null +++ b/Bugzilla/Quantum/Plugin/Hostage.pm @@ -0,0 +1,86 @@ +package Bugzilla::Quantum::Plugin::Hostage; +use 5.10.1; +use Mojo::Base 'Mojolicious::Plugin'; +use Bugzilla::Logging; + +sub _attachment_root { + my ($base) = @_; + return undef unless $base; + return $base =~ m{^https?://(?:bug)?\%bugid\%\.([a-zA-Z\.-]+)} + ? $1 + : undef; +} + +sub _attachment_host_regex { + my ($base) = @_; + return undef unless $base; + my $val = $base; + $val =~ s{^https?://}{}s; + $val =~ s{/$}{}s; + my $regex = quotemeta $val; + $regex =~ s/\\\%bugid\\\%/\\d+/g; + return qr/^$regex$/s; +} + +sub register { + my ( $self, $app, $conf ) = @_; + + $app->hook( before_routes => \&_before_routes ); +} + +sub _before_routes { + my ($c) = @_; + state $urlbase = Bugzilla->localconfig->{urlbase}; + state $urlbase_uri = URI->new($urlbase); + state $urlbase_host = $urlbase_uri->host; + state $urlbase_host_regex = qr/^bug(\d+)\.\Q$urlbase_host\E$/; + state $attachment_base = Bugzilla->localconfig->{attachment_base}; + state $attachment_root = _attachment_root($attachment_base); + state $attachment_host_regex = _attachment_host_regex($attachment_base); + + my $stash = $c->stash; + my $req = $c->req; + my $url = $req->url->to_abs; + + return if $stash->{'mojo.static'}; + + my $hostname = $url->host; + return if $hostname eq $urlbase_host; + + my $path = $url->path; + return if $path eq '/__lbheartbeat__'; + + if ( $attachment_base && $hostname eq $attachment_root ) { + DEBUG("redirecting to $urlbase because $hostname is $attachment_root"); + $c->redirect_to($urlbase); + return; + } + elsif ( $attachment_base && $hostname =~ $attachment_host_regex ) { + if ( $path =~ m{^/attachment\.cgi}s ) { + return; + } + else { + my $new_uri = $url->clone; + $new_uri->scheme( $urlbase_uri->scheme ); + $new_uri->host($urlbase_host); + DEBUG("redirecting to $new_uri because $hostname matches attachment regex"); + $c->redirect_to($new_uri); + return; + } + } + elsif ( my ($id) = $hostname =~ $urlbase_host_regex ) { + my $new_uri = $urlbase_uri->clone; + $new_uri->path('/show_bug.cgi'); + $new_uri->query_form( id => $id ); + DEBUG("redirecting to $new_uri because $hostname includes bug id"); + $c->redirect_to($new_uri); + return; + } + else { + DEBUG("redirecting to $urlbase because $hostname doesn't make sense"); + $c->redirect_to($urlbase); + return; + } +} + +1; diff --git a/Bugzilla/Quantum/SES.pm b/Bugzilla/Quantum/SES.pm new file mode 100644 index 000000000..03916075d --- /dev/null +++ b/Bugzilla/Quantum/SES.pm @@ -0,0 +1,254 @@ +package Bugzilla::Quantum::SES; +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +use 5.10.1; +use Mojo::Base qw( Mojolicious::Controller ); + +use Bugzilla::Constants qw(ERROR_MODE_DIE); +use Bugzilla::Logging; +use Bugzilla::Mailer qw(MessageToMTA); +use Bugzilla::User (); +use Bugzilla::Util qw(html_quote remote_ip); +use JSON::MaybeXS qw(decode_json); +use LWP::UserAgent (); +use Try::Tiny qw(catch try); + +use Types::Standard qw( :all ); +use Type::Utils; +use Type::Params qw( compile ); + +my $Invocant = class_type { class => __PACKAGE__ }; + +sub main { + my ($self) = @_; + try { + $self->_main; + } + catch { + FATAL("Error in SES Handler: ", $_); + $self->_respond( 400 => 'Bad Request' ); + }; +} + +sub _main { + my ($self) = @_; + Bugzilla->error_mode(ERROR_MODE_DIE); + my $message = $self->_decode_json_wrapper( $self->req->body ) // return; + my $message_type = $self->req->headers->header('X-Amz-SNS-Message-Type') // '(missing)'; + + if ( $message_type eq 'SubscriptionConfirmation' ) { + $self->_confirm_subscription($message); + } + + elsif ( $message_type eq 'Notification' ) { + my $notification = $self->_decode_json_wrapper( $message->{Message} ) // return; + unless ( + # https://docs.aws.amazon.com/ses/latest/DeveloperGuide/event-publishing-retrieving-sns-contents.html + $self->_handle_notification( $notification, 'eventType' ) + + # https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html + || $self->_handle_notification( $notification, 'notificationType' ) + ) + { + WARN('Failed to find notification type'); + $self->_respond( 400 => 'Bad Request' ); + } + } + + else { + WARN("Unsupported message-type: $message_type"); + $self->_respond( 200 => 'OK' ); + } +} + +sub _confirm_subscription { + state $check = compile($Invocant, Dict[SubscribeURL => Str, slurpy Any]); + my ($self, $message) = $check->(@_); + + my $subscribe_url = $message->{SubscribeURL}; + if ( !$subscribe_url ) { + WARN('Bad SubscriptionConfirmation request: missing SubscribeURL'); + $self->_respond( 400 => 'Bad Request' ); + return; + } + + my $ua = ua(); + my $res = $ua->get( $message->{SubscribeURL} ); + if ( !$res->is_success ) { + WARN( 'Bad response from SubscribeURL: ' . $res->status_line ); + $self->_respond( 400 => 'Bad Request' ); + return; + } + + $self->_respond( 200 => 'OK' ); +} + +my $NotificationType = Enum [qw( Bounce Complaint )]; +my $TypeField = Enum [qw(eventType notificationType)]; +my $Notification = Dict [ + eventType => Optional [$NotificationType], + notificationType => Optional [$NotificationType], + slurpy Any, +]; + +sub _handle_notification { + state $check = compile($Invocant, $Notification, $TypeField ); + my ( $self, $notification, $type_field ) = $check->(@_); + + if ( !exists $notification->{$type_field} ) { + return 0; + } + my $type = $notification->{$type_field}; + + if ( $type eq 'Bounce' ) { + $self->_process_bounce($notification); + } + elsif ( $type eq 'Complaint' ) { + $self->_process_complaint($notification); + } + else { + WARN("Unsupported notification-type: $type"); + $self->_respond( 200 => 'OK' ); + } + return 1; +} + +my $BouncedRecipients = ArrayRef[ + Dict[ + emailAddress => Str, + action => Str, + diagnosticCode => Str, + slurpy Any, + ], +]; +my $BounceNotification = Dict [ + bounce => Dict [ + bouncedRecipients => $BouncedRecipients, + reportingMTA => Str, + bounceSubType => Str, + bounceType => Str, + slurpy Any, + ], + slurpy Any, +]; + +sub _process_bounce { + state $check = compile($Invocant, $BounceNotification); + my ($self, $notification) = $check->(@_); + + # disable each account that is bouncing + foreach my $recipient ( @{ $notification->{bounce}->{bouncedRecipients} } ) { + my $address = $recipient->{emailAddress}; + my $reason = sprintf '(%s) %s', $recipient->{action} // 'error', $recipient->{diagnosticCode} // 'unknown'; + + my $user = Bugzilla::User->new( { name => $address, cache => 1 } ); + if ($user) { + + # never auto-disable admin accounts + if ( $user->in_group('admin') ) { + Bugzilla->audit("ignoring bounce for admin <$address>: $reason"); + } + + else { + my $template = Bugzilla->template_inner(); + my $vars = { + mta => $notification->{bounce}->{reportingMTA} // 'unknown', + reason => $reason, + }; + my $disable_text; + $template->process( 'admin/users/bounce-disabled.txt.tmpl', $vars, \$disable_text ) + || die $template->error(); + + $user->set_disabledtext($disable_text); + $user->set_disable_mail(1); + $user->update(); + Bugzilla->audit( "bounce for <$address> disabled userid-" . $user->id . ": $reason" ); + } + } + + else { + Bugzilla->audit("bounce for <$address> has no user: $reason"); + } + } + + $self->_respond( 200 => 'OK' ); +} + +my $ComplainedRecipients = ArrayRef[Dict[ emailAddress => Str, slurpy Any ]]; +my $ComplaintNotification = Dict[ + complaint => Dict [ + complainedRecipients => $ComplainedRecipients, + complaintFeedbackType => Str, + slurpy Any, + ], + slurpy Any, +]; + +sub _process_complaint { + state $check = compile($Invocant, $ComplaintNotification); + my ($self, $notification) = $check->(@_); + my $template = Bugzilla->template_inner(); + my $json = JSON::MaybeXS->new( + pretty => 1, + utf8 => 1, + canonical => 1, + ); + + foreach my $recipient ( @{ $notification->{complaint}->{complainedRecipients} } ) { + my $reason = $notification->{complaint}->{complaintFeedbackType} // 'unknown'; + my $address = $recipient->{emailAddress}; + Bugzilla->audit("complaint for <$address> for '$reason'"); + my $vars = { + email => $address, + user => Bugzilla::User->new( { name => $address, cache => 1 } ), + reason => $reason, + notification => $json->encode($notification), + }; + my $message; + $template->process( 'email/ses-complaint.txt.tmpl', $vars, \$message ) + || die $template->error(); + MessageToMTA($message); + } + + $self->_respond( 200 => 'OK' ); +} + +sub _respond { + my ( $self, $code, $message ) = @_; + $self->render(text => "$message\n", status => $code); +} + +sub _decode_json_wrapper { + state $check = compile($Invocant, Str); + my ($self, $json) = $check->(@_); + my $result; + my $ok = try { + $result = decode_json($json); + } + catch { + WARN( 'Malformed JSON from ' . $self->tx->remote_address ); + $self->_respond( 400 => 'Bad Request' ); + return undef; + }; + return $ok ? $result : undef; +} + +sub ua { + my $ua = LWP::UserAgent->new(); + $ua->timeout(10); + $ua->protocols_allowed( [ 'http', 'https' ] ); + if ( my $proxy_url = Bugzilla->params->{'proxy_url'} ) { + $ua->proxy( [ 'http', 'https' ], $proxy_url ); + } + else { + $ua->env_proxy; + } + return $ua; +} + +1; diff --git a/Bugzilla/Quantum/Static.pm b/Bugzilla/Quantum/Static.pm new file mode 100644 index 000000000..4543d1b84 --- /dev/null +++ b/Bugzilla/Quantum/Static.pm @@ -0,0 +1,30 @@ +# 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::Static; +use Mojo::Base 'Mojolicious::Static'; +use Bugzilla::Constants qw(bz_locations); + +my $LEGACY_RE = qr{ + ^ (?:static/v[0-9]+\.[0-9]+/) ? + ( (?:extensions/[^/]+/web|(?:image|skin|j|graph)s)/.+) + $ +}xs; + +sub file { + 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); + } +} + +1; diff --git a/Bugzilla/Quantum/Stdout.pm b/Bugzilla/Quantum/Stdout.pm new file mode 100644 index 000000000..9cf19992c --- /dev/null +++ b/Bugzilla/Quantum/Stdout.pm @@ -0,0 +1,60 @@ +# 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::Stdout; +use 5.10.1; +use Moo; + +use Bugzilla::Logging; +use Encode; +use English qw(-no_match_vars); + +has 'controller' => ( + is => 'ro', + required => 1, +); + +has '_encoding' => ( + is => 'rw', + default => '', +); + +sub TIEHANDLE { ## no critic (unpack) + my $class = shift; + + return $class->new(@_); +} + +sub PRINTF { ## no critic (unpack) + my $self = shift; + $self->PRINT( sprintf @_ ); +} + +sub PRINT { ## no critic (unpack) + my $self = shift; + my $c = $self->controller; + my $bytes = join '', @_; + return unless $bytes; + if ( $self->_encoding ) { + $bytes = encode( $self->_encoding, $bytes ); + } + $c->write($bytes . ( $OUTPUT_RECORD_SEPARATOR // '' ) ); +} + +sub BINMODE { + my ( $self, $mode ) = @_; + if ($mode) { + if ( $mode eq ':bytes' or $mode eq ':raw' ) { + $self->_encoding(''); + } + elsif ( $mode eq ':utf8' ) { + $self->_encoding('utf8'); + } + } +} + +1; diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm index 299734d64..39272a538 100644 --- a/Bugzilla/Template.pm +++ b/Bugzilla/Template.pm @@ -12,6 +12,7 @@ use 5.10.1; use strict; use warnings; +use Bugzilla::Logging; use Bugzilla::Template::PreloadProvider; use Bugzilla::Bug; use Bugzilla::Constants; diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm index a8477a62d..aa524b263 100644 --- a/Bugzilla/Util.pm +++ b/Bugzilla/Util.pm @@ -29,7 +29,7 @@ use base qw(Exporter); get_text template_var disable_utf8 enable_utf8 detect_encoding email_filter round extract_nicks); - +use Bugzilla::Logging; use Bugzilla::Constants; use Bugzilla::RNG qw(irand); @@ -317,6 +317,7 @@ sub do_ssl_redirect_if_required { # If we're already running under SSL, never redirect. return if $ENV{HTTPS} && $ENV{HTTPS} eq 'on'; + DEBUG("Redirect to HTTPS because \$ENV{HTTPS}=$ENV{HTTPS}"); Bugzilla->cgi->redirect_to_https(); } diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm index 6bb73af01..5ad50e91c 100644 --- a/Bugzilla/WebService/Server/XMLRPC.pm +++ b/Bugzilla/WebService/Server/XMLRPC.pm @@ -14,11 +14,7 @@ use warnings; use Bugzilla::Logging; use XMLRPC::Transport::HTTP; use Bugzilla::WebService::Server; -if ($ENV{MOD_PERL}) { - our @ISA = qw(XMLRPC::Transport::HTTP::Apache Bugzilla::WebService::Server); -} else { - our @ISA = qw(XMLRPC::Transport::HTTP::CGI Bugzilla::WebService::Server); -} +our @ISA = qw(XMLRPC::Transport::HTTP::CGI Bugzilla::WebService::Server); use Bugzilla::WebService::Constants; use Bugzilla::Error; diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm index 29ff05448..d462c884a 100644 --- a/Bugzilla/WebService/Util.pm +++ b/Bugzilla/WebService/Util.pm @@ -23,7 +23,7 @@ use base qw(Exporter); # We have to "require", not "use" this, because otherwise it tries to # use features of Test::More during import(). -require Test::Taint; +require Test::Taint if ${^TAINT}; our @EXPORT_OK = qw( extract_flags @@ -193,8 +193,10 @@ sub taint_data { # Though this is a private function, it hasn't changed since 2004 and # should be safe to use, and prevents us from having to write it ourselves # or require another module to do it. - Test::Taint::_deeply_traverse(\&_delete_bad_keys, \@params); - Test::Taint::taint_deeply(\@params); + if (${^TAINT}) { + Test::Taint::_deeply_traverse(\&_delete_bad_keys, \@params); + Test::Taint::taint_deeply(\@params); + } } sub _delete_bad_keys { -- cgit v1.2.3-24-g4f1b From 144234c61f9d0ea53069c9182f95a0fed7526206 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Wed, 19 Sep 2018 14:16:49 -0400 Subject: no bug - bump Alien::libcmark_gfm --- Bugzilla/Markdown/GFM.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'Bugzilla') 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); -- cgit v1.2.3-24-g4f1b From 7bf615124cd3380a6f7ea0fc553ba598a3ef05a5 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Thu, 20 Sep 2018 09:54:28 -0400 Subject: Bug 1492850 - Remove places where headers are printed There's one place where some (unused?) debug code prints out headers without using Bugzilla->cgi, and testagent.cgi which does the same. The first thing is removed and testagent.cgi is also removed, with its route handled by a simple route. --- Bugzilla/Quantum.pm | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'Bugzilla') diff --git a/Bugzilla/Quantum.pm b/Bugzilla/Quantum.pm index d2352f6d8..8af06c477 100644 --- a/Bugzilla/Quantum.pm +++ b/Bugzilla/Quantum.pm @@ -63,6 +63,10 @@ sub startup { $r->any('/')->to('CGI#index_cgi'); $r->any('/bug/')->to('CGI#show_bug_cgi'); $r->any('/')->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 => '' } ); -- cgit v1.2.3-24-g4f1b From 8047b0395f5175da934f3aaf09b15a15d83da755 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Mon, 24 Sep 2018 15:54:18 -0400 Subject: Bug 1492926 - Handle DBIx::Connectors more appropriately This is a bigger change than I anticipated, because the way we cached DBIx::Connector objects was bad. Now we cache the Bugzilla::DB instances in connect_main() and connect_shadow(). This is for maintaining a 1:1 mapping of Bugzilla::DB objects and DBIx::Connector objects. This is important because we want be able to inspect Bugzilla::DB->bz_in_transactions() from the 'connected' event. Note that we weaken the lexical variable $self in _build_connector() because it is referenced by the callback passed to DBI. Without this there would be a memory cycle and stuff would never be freed. (tested my understanding in this gist: https://gist.github.com/dylanwh/646574a027f7b7d92cb7586676da7468) --- Bugzilla/DB.pm | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) (limited to 'Bugzilla') diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm index cd5954219..142c241bf 100644 --- a/Bugzilla/DB.pm +++ b/Bugzilla/DB.pm @@ -12,13 +12,13 @@ use Moo; use DBI; use DBIx::Connector; -our %Connector; has 'connector' => ( is => 'lazy', handles => [ qw( dbh ) ], ); +use Bugzilla::Logging; use Bugzilla::Constants; use Bugzilla::Install::Requirements; use Bugzilla::Install::Util qw(install_string); @@ -31,7 +31,9 @@ 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); has [qw(dsn user pass attrs)] => ( is => 'ro', @@ -44,7 +46,7 @@ has [qw(dsn user pass attrs)] => ( # time we need a DBI handle to ensure the connection is alive. { my @DBI_METHODS = qw( - begin_work column_info commit disconnect do errstr get_info last_insert_id ping prepare + 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 ); @@ -130,6 +132,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'); @@ -147,13 +155,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 { @@ -293,7 +304,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); }; @@ -1304,13 +1314,18 @@ sub _build_connector { } } my $class = ref $self; - if ($class->can('on_dbi_connected')) { - $attributes->{Callbacks} = { - connected => sub { $class->on_dbi_connected(@_); return }, - } - } + 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{"$user.$dsn"} //= DBIx::Connector->new($dsn, $user, $pass, $attributes); + return DBIx::Connector->new($dsn, $user, $pass, $attributes); } ##################################################################### -- cgit v1.2.3-24-g4f1b From 65aed407b07a5e8ef19ced43f958c14c046e6ed8 Mon Sep 17 00:00:00 2001 From: Kohei Yoshino Date: Sun, 23 Sep 2018 11:43:53 -0400 Subject: Bug 1490595 - Bugzilla update check should use https --- Bugzilla/Constants.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Bugzilla') diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index 185a30390..525705ce1 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -210,7 +210,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, -- cgit v1.2.3-24-g4f1b From deec4ab75d6478f51d6c72a230343ab955116a6b Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Tue, 25 Sep 2018 17:30:28 -0400 Subject: Bug 1494065 - Add a basic test using Test::Mojo --- Bugzilla/Quantum.pm | 2 +- Bugzilla/Test/Util.pm | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) (limited to 'Bugzilla') diff --git a/Bugzilla/Quantum.pm b/Bugzilla/Quantum.pm index 8af06c477..c64e57e13 100644 --- a/Bugzilla/Quantum.pm +++ b/Bugzilla/Quantum.pm @@ -34,7 +34,7 @@ sub startup { DEBUG('Starting up'); $self->plugin('Bugzilla::Quantum::Plugin::Glue'); - $self->plugin('Bugzilla::Quantum::Plugin::Hostage'); + $self->plugin('Bugzilla::Quantum::Plugin::Hostage') unless $ENV{BUGZILLA_DISABLE_HOSTAGE}; $self->plugin('Bugzilla::Quantum::Plugin::BlockIP'); $self->plugin('Bugzilla::Quantum::Plugin::BasicAuth'); diff --git a/Bugzilla/Test/Util.pm b/Bugzilla/Test/Util.pm index 02c842658..8124c25ee 100644 --- a/Bugzilla/Test/Util.pm +++ b/Bugzilla/Test/Util.pm @@ -12,9 +12,10 @@ use strict; use warnings; use base qw(Exporter); -our @EXPORT = qw(create_user); +our @EXPORT = qw(create_user issue_api_key); use Bugzilla::User; +use Bugzilla::User::APIKey; sub create_user { my ($login, $password, %extra) = @_; @@ -29,4 +30,21 @@ 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); + } +} + 1; -- cgit v1.2.3-24-g4f1b From 09dd4d3192073ba9ecc7feb8a5dfcd213affd525 Mon Sep 17 00:00:00 2001 From: Kohei Yoshino Date: Mon, 24 Sep 2018 17:27:12 -0400 Subject: Bug 1493500 - Remove all trailing whitespaces from all files --- Bugzilla/User.pm | 2 +- Bugzilla/WebService/Bug.pm | 8 ++++---- Bugzilla/WebService/Bugzilla.pm | 4 ++-- Bugzilla/WebService/README | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) (limited to 'Bugzilla') 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..d247b8ffb 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 }; 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/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 -- cgit v1.2.3-24-g4f1b From b0d4aab23e9d89f608e79265fcfbefb88166ecc1 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Thu, 27 Sep 2018 22:01:33 -0400 Subject: no bug - use bugzilla as the default template engine --- Bugzilla/Quantum/Plugin/Glue.pm | 1 + 1 file changed, 1 insertion(+) (limited to 'Bugzilla') diff --git a/Bugzilla/Quantum/Plugin/Glue.pm b/Bugzilla/Quantum/Plugin/Glue.pm index ded4daf15..b9cf9268f 100644 --- a/Bugzilla/Quantum/Plugin/Glue.pm +++ b/Bugzilla/Quantum/Plugin/Glue.pm @@ -94,6 +94,7 @@ sub register { or die $template->error; } ); + $app->renderer->default_handler('bugzilla'); $app->log( MojoX::Log::Log4perl::Tiny->new( logger => Log::Log4perl->get_logger( ref $app ) ) ); } -- cgit v1.2.3-24-g4f1b From ed452b9533b7626250ffb0bd1d0258eaa3f853f9 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Fri, 28 Sep 2018 10:37:18 -0400 Subject: no bug - use more generous timeouts this makes all the mojo timeouts larger -- except for 'clients' which should be smaller because we're so synchronous. It also puts them into environmental variables so ops can tweak them. Note some of the code has moved to the main application class to make future people less likely to not notice these values. --- Bugzilla/Quantum.pm | 28 ++++++++++++++++++++++++++++ Bugzilla/Quantum/Plugin/Glue.pm | 22 ---------------------- 2 files changed, 28 insertions(+), 22 deletions(-) (limited to 'Bugzilla') diff --git a/Bugzilla/Quantum.pm b/Bugzilla/Quantum.pm index c64e57e13..3d392c47b 100644 --- a/Bugzilla/Quantum.pm +++ b/Bugzilla/Quantum.pm @@ -38,6 +38,34 @@ sub startup { $self->plugin('Bugzilla::Quantum::Plugin::BlockIP'); $self->plugin('Bugzilla::Quantum::Plugin::BasicAuth'); + # 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(); + try { Bugzilla->dbh->ping }; + } + ); + Bugzilla::Extension->load_all(); if ( $self->mode ne 'development' ) { Bugzilla->preload_features(); diff --git a/Bugzilla/Quantum/Plugin/Glue.pm b/Bugzilla/Quantum/Plugin/Glue.pm index b9cf9268f..a38a12657 100644 --- a/Bugzilla/Quantum/Plugin/Glue.pm +++ b/Bugzilla/Quantum/Plugin/Glue.pm @@ -31,28 +31,6 @@ sub register { } } - # 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) = @_; -- cgit v1.2.3-24-g4f1b From 0cfd7cbbd50ac464360c876551cb592ea1a6bf3a Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Mon, 1 Oct 2018 00:58:26 -0400 Subject: fix error and bump version (#772) --- Bugzilla/Quantum.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Bugzilla') diff --git a/Bugzilla/Quantum.pm b/Bugzilla/Quantum.pm index 3d392c47b..28fbd4bf6 100644 --- a/Bugzilla/Quantum.pm +++ b/Bugzilla/Quantum.pm @@ -62,7 +62,7 @@ sub startup { sub { Bugzilla::RNG::srand(); srand(); - try { Bugzilla->dbh->ping }; + eval { Bugzilla->dbh->ping }; } ); -- cgit v1.2.3-24-g4f1b From dd5c0861db83e58ae7f507b4ecfc564d96792cb8 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Mon, 1 Oct 2018 12:03:41 -0400 Subject: Bug 1495071 - Mojolicious Cleanup There are some things that should've been in the first patch but were missed: 1. Calling $c->finish in the finally block should not happen if an exception has been raised. 2. Bugzilla->cleanup() should be called at the same time the mojolicious stash is cleared. 3. Code referencing the shutdownhtml should be removed 4. The conditionals that ran code in Bugzilla.pm when it was not run under mod_perl should instead check where the Bugzilla.pm module was loaded from. 5. Revert the default template from #770 6. Also removed some stuff that manipulates the PATH and signals, which we shouldn't do --- Bugzilla/CGI.pm | 6 ---- Bugzilla/Quantum.pm | 50 +++++++++++----------------- Bugzilla/Quantum/CGI.pm | 13 ++++++-- Bugzilla/Quantum/Plugin/BasicAuth.pm | 40 ---------------------- Bugzilla/Quantum/Plugin/Glue.pm | 3 +- Bugzilla/Quantum/Plugin/Helpers.pm | 64 ++++++++++++++++++++++++++++++++++++ 6 files changed, 97 insertions(+), 79 deletions(-) delete mode 100644 Bugzilla/Quantum/Plugin/BasicAuth.pm create mode 100644 Bugzilla/Quantum/Plugin/Helpers.pm (limited to 'Bugzilla') diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm index e76b1c609..4be384b67 100644 --- a/Bugzilla/CGI.pm +++ b/Bugzilla/CGI.pm @@ -97,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/Quantum.pm b/Bugzilla/Quantum.pm index 28fbd4bf6..e9e7713e2 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 (); @@ -26,6 +28,7 @@ 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 }; @@ -36,7 +39,7 @@ sub startup { $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::BasicAuth'); + $self->plugin('Bugzilla::Quantum::Plugin::Helpers'); # hypnotoad is weird and doesn't look for MOJO_LISTEN itself. $self->config( @@ -81,13 +84,20 @@ sub startup { } ); } + Bugzilla::WebService::Server::REST->preload; + + $self->setup_routes; + + Bugzilla::Hook::process( 'app_startup', { app => $self } ); +} + +sub setup_routes { + my ($self) = @_; my $r = $self->routes; Bugzilla::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/')->to('CGI#show_bug_cgi'); $r->any('/')->to('CGI#show_bug_cgi'); @@ -102,36 +112,18 @@ sub startup { $r->any('/extensions/BzAPI/bin/rest.cgi/*PATH_INFO')->to('CGI#bzapi_cgi'); $r->any('/bzapi/*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') ); - }, - ); + $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->get( - '/version.json' => sub { - my $c = shift; - $c->reply->file( $c->app->home->child('version.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('/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( @@ -143,8 +135,6 @@ sub startup { } ); $ses_auth->any('/index.cgi')->to('SES#main'); - - Bugzilla::Hook::process( 'app_startup', { app => $self } ); } 1; diff --git a/Bugzilla/Quantum/CGI.pm b/Bugzilla/Quantum/CGI.pm index 0a74f1ee5..945a87d5b 100644 --- a/Bugzilla/Quantum/CGI.pm +++ b/Bugzilla/Quantum/CGI.pm @@ -58,16 +58,20 @@ sub load_one { 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; try { Bugzilla->init_page(); $inner->(); } catch { - die $_ unless ref $_ eq 'ARRAY' && $_->[0] eq "EXIT\n"; + die $_ unless _is_exit($_); } finally { + my $error = shift; untie *STDOUT; - $c->finish; + $c->finish if !$error || _is_exit($error); Bugzilla->cleanup; CGI::initialize_globals(); }; @@ -157,4 +161,9 @@ sub _file_to_method { return $name; } +sub _is_exit { + my ($error) = @_; + return ref $error eq 'ARRAY' && $error->[0] eq "EXIT\n"; +} + 1; diff --git a/Bugzilla/Quantum/Plugin/BasicAuth.pm b/Bugzilla/Quantum/Plugin/BasicAuth.pm deleted file mode 100644 index e17273404..000000000 --- a/Bugzilla/Quantum/Plugin/BasicAuth.pm +++ /dev/null @@ -1,40 +0,0 @@ -# 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::BasicAuth; -use 5.10.1; -use Mojo::Base qw(Mojolicious::Plugin); - -use Bugzilla::Logging; -use Carp; - -sub register { - my ( $self, $app, $conf ) = @_; - - $app->renderer->add_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; - } - ); -} - -1; \ No newline at end of file diff --git a/Bugzilla/Quantum/Plugin/Glue.pm b/Bugzilla/Quantum/Plugin/Glue.pm index a38a12657..e9d0056d0 100644 --- a/Bugzilla/Quantum/Plugin/Glue.pm +++ b/Bugzilla/Quantum/Plugin/Glue.pm @@ -14,6 +14,7 @@ use Bugzilla::Constants; use Bugzilla::Logging; use Bugzilla::RNG (); use JSON::MaybeXS qw(decode_json); +use Scope::Guard; sub register { my ( $self, $app, $conf ) = @_; @@ -42,6 +43,7 @@ sub register { } } Log::Log4perl::MDC->put( request_id => $c->req->request_id ); + $c->stash->{cleanup_guard} = Scope::Guard->new( \&Bugzilla::cleanup ); } ); @@ -72,7 +74,6 @@ sub register { or die $template->error; } ); - $app->renderer->default_handler('bugzilla'); $app->log( MojoX::Log::Log4perl::Tiny->new( logger => Log::Log4perl->get_logger( ref $app ) ) ); } diff --git a/Bugzilla/Quantum/Plugin/Helpers.pm b/Bugzilla/Quantum/Plugin/Helpers.pm new file mode 100644 index 000000000..0aedca338 --- /dev/null +++ b/Bugzilla/Quantum/Plugin/Helpers.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::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; -- cgit v1.2.3-24-g4f1b From 62412db14081dd66cd5b2701b598b5af9eb31528 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Tue, 2 Oct 2018 14:22:05 -0400 Subject: add helpers for handling logins and error handling --- Bugzilla/Constants.pm | 4 ++ Bugzilla/Error.pm | 12 ++++- Bugzilla/Error/Base.pm | 21 ++++++++ Bugzilla/Error/Code.pm | 14 +++++ Bugzilla/Error/User.pm | 13 +++++ Bugzilla/Quantum.pm | 2 + Bugzilla/Quantum/CGI.pm | 3 +- Bugzilla/Quantum/Home.pm | 26 ++++++++++ Bugzilla/Quantum/Plugin/Glue.pm | 111 ++++++++++++++++++++++++++++++++++------ 9 files changed, 188 insertions(+), 18 deletions(-) create mode 100644 Bugzilla/Error/Base.pm create mode 100644 Bugzilla/Error/Code.pm create mode 100644 Bugzilla/Error/User.pm create mode 100644 Bugzilla/Quantum/Home.pm (limited to 'Bugzilla') diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index 525705ce1..d0b74b5e3 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 @@ -488,6 +490,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. @@ -497,6 +500,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/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/Quantum.pm b/Bugzilla/Quantum.pm index e9e7713e2..f454a78c5 100644 --- a/Bugzilla/Quantum.pm +++ b/Bugzilla/Quantum.pm @@ -22,6 +22,7 @@ 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 ); @@ -98,6 +99,7 @@ sub setup_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/')->to('CGI#show_bug_cgi'); $r->any('/')->to('CGI#show_bug_cgi'); diff --git a/Bugzilla/Quantum/CGI.pm b/Bugzilla/Quantum/CGI.pm index 945a87d5b..317c189cc 100644 --- a/Bugzilla/Quantum/CGI.pm +++ b/Bugzilla/Quantum/CGI.pm @@ -19,7 +19,7 @@ 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; @@ -61,6 +61,7 @@ sub load_one { # the finally block calls cleanup. $c->stash->{cleanup_guard}->dismiss; + Bugzilla->usage_mode(USAGE_MODE_BROWSER); try { Bugzilla->init_page(); $inner->(); diff --git a/Bugzilla/Quantum/Home.pm b/Bugzilla/Quantum/Home.pm new file mode 100644 index 000000000..b3f1ec1d1 --- /dev/null +++ b/Bugzilla/Quantum/Home.pm @@ -0,0 +1,26 @@ +# 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/Glue.pm b/Bugzilla/Quantum/Plugin/Glue.pm index e9d0056d0..8f4144589 100644 --- a/Bugzilla/Quantum/Plugin/Glue.pm +++ b/Bugzilla/Quantum/Plugin/Glue.pm @@ -13,7 +13,10 @@ 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 { @@ -44,34 +47,110 @@ sub register { } 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 $vars = delete $c->stash->{vars}; + my %params; # Helpers - my %helper; - foreach my $method ( grep {m/^\w+\z/} keys %{ $renderer->helpers } ) { - my $sub = $renderer->helpers->{$method}; - $helper{$method} = sub { $c->$sub(@_) }; + foreach my $method (grep { m/^\w+\z/ } keys %{$renderer->helpers}) { + my $sub = $renderer->helpers->{$method}; + $params{$method} = sub { $c->$sub(@_) }; } - $vars->{helper} = \%helper; + # Stash values + $params{$_} = $c->stash->{$_} for grep { m/^\w+\z/ } keys %{$c->stash}; - # The controller - $vars->{c} = $c; - my $name = $options->{template}; - unless ( $name =~ /\./ ) { - $name = sprintf '%s.%s.tmpl', $options->{template}, $options->{format}; - } + $params{self} = $params{c} = $c; + + my $name = sprintf '%s.%s.tmpl', $options->{template}, $options->{format}; my $template = Bugzilla->template; - $template->process( $name, $vars, $output ) - or die $template->error; + $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); + } } ); -- cgit v1.2.3-24-g4f1b From b340e1d0568ea33d15ff59ff48560e78966121e4 Mon Sep 17 00:00:00 2001 From: Kohei Yoshino Date: Tue, 2 Oct 2018 14:24:06 -0400 Subject: Bug 1263502 - Add duplicates to /rest/bug/id --- Bugzilla/WebService/Bug.pm | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'Bugzilla') diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm index d247b8ffb..9003f3480 100644 --- a/Bugzilla/WebService/Bug.pm +++ b/Bugzilla/WebService/Bug.pm @@ -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 }; @@ -2594,6 +2597,10 @@ C of Cs. The ids of bugs that this bug "depends on". C 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 + +C of Cs. The ids of bugs that are marked as duplicate of this bug. + =item C C The number of hours that it was estimated that this bug would @@ -2911,6 +2918,8 @@ and all custom fields. =item The C item was added to the C return value in Bugzilla B<4.4>. +=item The C array was added in Bugzilla B<6.0>. + =back =back -- cgit v1.2.3-24-g4f1b From 286637cf16f1b933e8d4e14a4be4ca7123866374 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Tue, 2 Oct 2018 16:22:19 -0400 Subject: Bug 1495869 - Crash graph not found after mojo migration --- Bugzilla/Template.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Bugzilla') diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm index 39272a538..c337b5af8 100644 --- a/Bugzilla/Template.pm +++ b/Bugzilla/Template.pm @@ -1029,7 +1029,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; }, -- cgit v1.2.3-24-g4f1b From df00fc4c827311b0849b243dbb2d650a2c1957cd Mon Sep 17 00:00:00 2001 From: dklawren Date: Wed, 3 Oct 2018 11:23:00 -0400 Subject: Bug 1495906 - After mojo update /latest/configuration API call no longer works and gives page not found --- Bugzilla/Quantum.pm | 1 + 1 file changed, 1 insertion(+) (limited to 'Bugzilla') diff --git a/Bugzilla/Quantum.pm b/Bugzilla/Quantum.pm index f454a78c5..014663bd0 100644 --- a/Bugzilla/Quantum.pm +++ b/Bugzilla/Quantum.pm @@ -112,6 +112,7 @@ sub setup_routes { $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__'); -- cgit v1.2.3-24-g4f1b From f81ef54f3224d875c40076636282e2b7387a7fd4 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Wed, 3 Oct 2018 16:08:55 -0400 Subject: no bug - reformat all new quantum files using new standard --- Bugzilla/Quantum.pm | 199 ++++++++++---------- Bugzilla/Quantum/CGI.pm | 245 +++++++++++++------------ Bugzilla/Quantum/Home.pm | 18 +- Bugzilla/Quantum/Plugin/BlockIP.pm | 38 ++-- Bugzilla/Quantum/Plugin/Glue.pm | 234 +++++++++++------------ Bugzilla/Quantum/Plugin/Helpers.pm | 82 +++++---- Bugzilla/Quantum/Plugin/Hostage.pm | 117 ++++++------ Bugzilla/Quantum/SES.pm | 367 ++++++++++++++++++------------------- Bugzilla/Quantum/Static.pm | 16 +- Bugzilla/Quantum/Stdout.pm | 50 +++-- 10 files changed, 687 insertions(+), 679 deletions(-) (limited to 'Bugzilla') diff --git a/Bugzilla/Quantum.pm b/Bugzilla/Quantum.pm index 014663bd0..4fddb8da9 100644 --- a/Bugzilla/Quantum.pm +++ b/Bugzilla/Quantum.pm @@ -34,110 +34,113 @@ 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') 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' ], - }, + 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; - # 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; - - $self->setup_routes; + $self->setup_routes; - 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/')->to('CGI#show_bug_cgi'); - $r->any('/')->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'); + 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/')->to('CGI#show_bug_cgi'); + $r->any('/')->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 317c189cc..beb849687 100644 --- a/Bugzilla/Quantum/CGI.pm +++ b/Bugzilla/Quantum/CGI.pm @@ -25,146 +25,151 @@ 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) - - # 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(); - }; + 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 ($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 $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"; + 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 index b3f1ec1d1..48d5e47bd 100644 --- a/Bugzilla/Quantum/Home.pm +++ b/Bugzilla/Quantum/Home.pm @@ -13,14 +13,16 @@ 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($_); - }; + 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 8f4144589..f04b9c025 100644 --- a/Bugzilla/Quantum/Plugin/Glue.pm +++ b/Bugzilla/Quantum/Plugin/Glue.pm @@ -20,141 +20,145 @@ 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"; + } + } + } + + $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->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->secrets( [ Bugzilla->localconfig->{side_wide_secret} ] ); + $app->renderer->add_handler( + 'bugzilla' => sub { + my ($renderer, $c, $output, $options) = @_; - $app->renderer->add_handler( - 'bugzilla' => sub { - my ( $renderer, $c, $output, $options ) = @_; + my %params; - 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}; + # Helpers + foreach my $method (grep {m/^\w+\z/} keys %{$renderer->helpers}) { + my $sub = $renderer->helpers->{$method}; + $params{$method} = sub { $c->$sub(@_) }; + } - $params{self} = $params{c} = $c; + # Stash values + $params{$_} = $c->stash->{$_} for grep {m/^\w+\z/} keys %{$c->stash}; - 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; + $params{self} = $params{c} = $c; - return Bugzilla->user if Bugzilla->user->id; + 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; - $type = LOGIN_REQUIRED if $c->param('GoAheadAndLogIn') || Bugzilla->params->{requirelogin}; + return Bugzilla->user if Bugzilla->user->id; - # 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; - } + $type = LOGIN_REQUIRED + if $c->param('GoAheadAndLogIn') || Bugzilla->params->{requirelogin}; - my $login_cookie = $c->cookie("Bugzilla_logincookie"); - my $user_id = $c->cookie("Bugzilla_login"); - my $ip_addr = $c->tx->remote_address; + # 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; + } - return $c->bugzilla->login_redirect_if_required($type) unless ( $login_cookie && $user_id ); + my $login_cookie = $c->cookie("Bugzilla_logincookie"); + my $user_id = $c->cookie("Bugzilla_login"); + my $ip_addr = $c->tx->remote_address; - my $db_cookie = Bugzilla->dbh->selectrow_array( - q{ + 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); - } - } - ); + }, 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 index 0aedca338..72dd96cf9 100644 --- a/Bugzilla/Quantum/Plugin/Helpers.pm +++ b/Bugzilla/Quantum/Plugin/Helpers.pm @@ -12,53 +12,55 @@ use Bugzilla::Logging; use Carp; sub register { - my ( $self, $app, $conf ) = @_; + 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 =~ /^([^:]+):(.*)/; + $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 ($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; - } + 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 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) ); - }) + 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) = @_; + ); + } + ); + $app->routes->add_shortcut( + page => sub { + my ($r, $path, $id) = @_; - return $r->any($path)->to('CGI#page_cgi' => { id => $id }); - } - ); + return $r->any($path)->to('CGI#page_cgi' => {id => $id}); + } + ); } 1; diff --git a/Bugzilla/Quantum/Plugin/Hostage.pm b/Bugzilla/Quantum/Plugin/Hostage.pm index cbde7b5ee..63fad2be2 100644 --- a/Bugzilla/Quantum/Plugin/Hostage.pm +++ b/Bugzilla/Quantum/Plugin/Hostage.pm @@ -4,83 +4,82 @@ use Mojo::Base 'Mojolicious::Plugin'; use Bugzilla::Logging; sub _attachment_root { - my ($base) = @_; - return undef unless $base; - return $base =~ m{^https?://(?:bug)?\%bugid\%\.([a-zA-Z\.-]+)} - ? $1 - : undef; + my ($base) = @_; + return undef unless $base; + return $base =~ m{^https?://(?:bug)?\%bugid\%\.([a-zA-Z\.-]+)} ? $1 : undef; } sub _attachment_host_regex { - my ($base) = @_; - return undef unless $base; - my $val = $base; - $val =~ s{^https?://}{}s; - $val =~ s{/$}{}s; - my $regex = quotemeta $val; - $regex =~ s/\\\%bugid\\\%/\\d+/g; - return qr/^$regex$/s; + my ($base) = @_; + return undef unless $base; + my $val = $base; + $val =~ s{^https?://}{}s; + $val =~ s{/$}{}s; + my $regex = quotemeta $val; + $regex =~ s/\\\%bugid\\\%/\\d+/g; + return qr/^$regex$/s; } sub register { - my ( $self, $app, $conf ) = @_; + my ($self, $app, $conf) = @_; - $app->hook( before_routes => \&_before_routes ); + $app->hook(before_routes => \&_before_routes); } sub _before_routes { - my ($c) = @_; - state $urlbase = Bugzilla->localconfig->{urlbase}; - state $urlbase_uri = URI->new($urlbase); - state $urlbase_host = $urlbase_uri->host; - state $urlbase_host_regex = qr/^bug(\d+)\.\Q$urlbase_host\E$/; - state $attachment_base = Bugzilla->localconfig->{attachment_base}; - state $attachment_root = _attachment_root($attachment_base); - state $attachment_host_regex = _attachment_host_regex($attachment_base); + my ($c) = @_; + state $urlbase = Bugzilla->localconfig->{urlbase}; + state $urlbase_uri = URI->new($urlbase); + state $urlbase_host = $urlbase_uri->host; + state $urlbase_host_regex = qr/^bug(\d+)\.\Q$urlbase_host\E$/; + state $attachment_base = Bugzilla->localconfig->{attachment_base}; + state $attachment_root = _attachment_root($attachment_base); + state $attachment_host_regex = _attachment_host_regex($attachment_base); - my $stash = $c->stash; - my $req = $c->req; - my $url = $req->url->to_abs; + my $stash = $c->stash; + my $req = $c->req; + my $url = $req->url->to_abs; - return if $stash->{'mojo.static'}; + return if $stash->{'mojo.static'}; - my $hostname = $url->host; - return if $hostname eq $urlbase_host; + my $hostname = $url->host; + return if $hostname eq $urlbase_host; - my $path = $url->path; - return if $path eq '/__lbheartbeat__'; + my $path = $url->path; + return if $path eq '/__lbheartbeat__'; - if ( $attachment_base && $hostname eq $attachment_root ) { - DEBUG("redirecting to $urlbase because $hostname is $attachment_root"); - $c->redirect_to($urlbase); - return; - } - elsif ( $attachment_base && $hostname =~ $attachment_host_regex ) { - if ( $path =~ m{^/attachment\.cgi}s ) { - return; - } - else { - my $new_uri = $url->clone; - $new_uri->scheme( $urlbase_uri->scheme ); - $new_uri->host($urlbase_host); - DEBUG("redirecting to $new_uri because $hostname matches attachment regex"); - $c->redirect_to($new_uri); - return; - } - } - elsif ( my ($id) = $hostname =~ $urlbase_host_regex ) { - my $new_uri = $urlbase_uri->clone; - $new_uri->path('/show_bug.cgi'); - $new_uri->query_form( id => $id ); - DEBUG("redirecting to $new_uri because $hostname includes bug id"); - $c->redirect_to($new_uri); - return; + if ($attachment_base && $hostname eq $attachment_root) { + DEBUG("redirecting to $urlbase because $hostname is $attachment_root"); + $c->redirect_to($urlbase); + return; + } + elsif ($attachment_base && $hostname =~ $attachment_host_regex) { + if ($path =~ m{^/attachment\.cgi}s) { + return; } else { - DEBUG("redirecting to $urlbase because $hostname doesn't make sense"); - $c->redirect_to($urlbase); - return; + my $new_uri = $url->clone; + $new_uri->scheme($urlbase_uri->scheme); + $new_uri->host($urlbase_host); + DEBUG( + "redirecting to $new_uri because $hostname matches attachment regex"); + $c->redirect_to($new_uri); + return; } + } + elsif (my ($id) = $hostname =~ $urlbase_host_regex) { + my $new_uri = $urlbase_uri->clone; + $new_uri->path('/show_bug.cgi'); + $new_uri->query_form(id => $id); + DEBUG("redirecting to $new_uri because $hostname includes bug id"); + $c->redirect_to($new_uri); + return; + } + else { + DEBUG("redirecting to $urlbase because $hostname doesn't make sense"); + $c->redirect_to($urlbase); + return; + } } 1; diff --git a/Bugzilla/Quantum/SES.pm b/Bugzilla/Quantum/SES.pm index 03916075d..750da4e77 100644 --- a/Bugzilla/Quantum/SES.pm +++ b/Bugzilla/Quantum/SES.pm @@ -1,4 +1,5 @@ package Bugzilla::Quantum::SES; + # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. @@ -22,233 +23,231 @@ use Types::Standard qw( :all ); use Type::Utils; use Type::Params qw( compile ); -my $Invocant = class_type { class => __PACKAGE__ }; +my $Invocant = class_type {class => __PACKAGE__}; sub main { - my ($self) = @_; - try { - $self->_main; - } - catch { - FATAL("Error in SES Handler: ", $_); - $self->_respond( 400 => 'Bad Request' ); - }; + my ($self) = @_; + try { + $self->_main; + } + catch { + FATAL("Error in SES Handler: ", $_); + $self->_respond(400 => 'Bad Request'); + }; } sub _main { - my ($self) = @_; - Bugzilla->error_mode(ERROR_MODE_DIE); - my $message = $self->_decode_json_wrapper( $self->req->body ) // return; - my $message_type = $self->req->headers->header('X-Amz-SNS-Message-Type') // '(missing)'; - - if ( $message_type eq 'SubscriptionConfirmation' ) { - $self->_confirm_subscription($message); + my ($self) = @_; + Bugzilla->error_mode(ERROR_MODE_DIE); + my $message = $self->_decode_json_wrapper($self->req->body) // return; + my $message_type = $self->req->headers->header('X-Amz-SNS-Message-Type') + // '(missing)'; + + if ($message_type eq 'SubscriptionConfirmation') { + $self->_confirm_subscription($message); + } + + elsif ($message_type eq 'Notification') { + my $notification = $self->_decode_json_wrapper($message->{Message}) + // return; + unless ( +# https://docs.aws.amazon.com/ses/latest/DeveloperGuide/event-publishing-retrieving-sns-contents.html + $self->_handle_notification($notification, 'eventType') + +# https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html + || $self->_handle_notification($notification, 'notificationType') + ) + { + WARN('Failed to find notification type'); + $self->_respond(400 => 'Bad Request'); } + } - elsif ( $message_type eq 'Notification' ) { - my $notification = $self->_decode_json_wrapper( $message->{Message} ) // return; - unless ( - # https://docs.aws.amazon.com/ses/latest/DeveloperGuide/event-publishing-retrieving-sns-contents.html - $self->_handle_notification( $notification, 'eventType' ) - - # https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html - || $self->_handle_notification( $notification, 'notificationType' ) - ) - { - WARN('Failed to find notification type'); - $self->_respond( 400 => 'Bad Request' ); - } - } - - else { - WARN("Unsupported message-type: $message_type"); - $self->_respond( 200 => 'OK' ); - } + else { + WARN("Unsupported message-type: $message_type"); + $self->_respond(200 => 'OK'); + } } sub _confirm_subscription { - state $check = compile($Invocant, Dict[SubscribeURL => Str, slurpy Any]); - my ($self, $message) = $check->(@_); - - my $subscribe_url = $message->{SubscribeURL}; - if ( !$subscribe_url ) { - WARN('Bad SubscriptionConfirmation request: missing SubscribeURL'); - $self->_respond( 400 => 'Bad Request' ); - return; - } - - my $ua = ua(); - my $res = $ua->get( $message->{SubscribeURL} ); - if ( !$res->is_success ) { - WARN( 'Bad response from SubscribeURL: ' . $res->status_line ); - $self->_respond( 400 => 'Bad Request' ); - return; - } - - $self->_respond( 200 => 'OK' ); + state $check = compile($Invocant, Dict [SubscribeURL => Str, slurpy Any]); + my ($self, $message) = $check->(@_); + + my $subscribe_url = $message->{SubscribeURL}; + if (!$subscribe_url) { + WARN('Bad SubscriptionConfirmation request: missing SubscribeURL'); + $self->_respond(400 => 'Bad Request'); + return; + } + + my $ua = ua(); + my $res = $ua->get($message->{SubscribeURL}); + if (!$res->is_success) { + WARN('Bad response from SubscribeURL: ' . $res->status_line); + $self->_respond(400 => 'Bad Request'); + return; + } + + $self->_respond(200 => 'OK'); } my $NotificationType = Enum [qw( Bounce Complaint )]; my $TypeField = Enum [qw(eventType notificationType)]; -my $Notification = Dict [ - eventType => Optional [$NotificationType], - notificationType => Optional [$NotificationType], - slurpy Any, +my $Notification = Dict [ + eventType => Optional [$NotificationType], + notificationType => Optional [$NotificationType], + slurpy Any, ]; sub _handle_notification { - state $check = compile($Invocant, $Notification, $TypeField ); - my ( $self, $notification, $type_field ) = $check->(@_); - - if ( !exists $notification->{$type_field} ) { - return 0; - } - my $type = $notification->{$type_field}; - - if ( $type eq 'Bounce' ) { - $self->_process_bounce($notification); - } - elsif ( $type eq 'Complaint' ) { - $self->_process_complaint($notification); - } - else { - WARN("Unsupported notification-type: $type"); - $self->_respond( 200 => 'OK' ); - } - return 1; + state $check = compile($Invocant, $Notification, $TypeField); + my ($self, $notification, $type_field) = $check->(@_); + + if (!exists $notification->{$type_field}) { + return 0; + } + my $type = $notification->{$type_field}; + + if ($type eq 'Bounce') { + $self->_process_bounce($notification); + } + elsif ($type eq 'Complaint') { + $self->_process_complaint($notification); + } + else { + WARN("Unsupported notification-type: $type"); + $self->_respond(200 => 'OK'); + } + return 1; } -my $BouncedRecipients = ArrayRef[ - Dict[ - emailAddress => Str, - action => Str, - diagnosticCode => Str, - slurpy Any, - ], +my $BouncedRecipients = ArrayRef [ + Dict [emailAddress => Str, action => Str, diagnosticCode => Str, slurpy Any,], ]; my $BounceNotification = Dict [ - bounce => Dict [ - bouncedRecipients => $BouncedRecipients, - reportingMTA => Str, - bounceSubType => Str, - bounceType => Str, - slurpy Any, - ], + bounce => Dict [ + bouncedRecipients => $BouncedRecipients, + reportingMTA => Str, + bounceSubType => Str, + bounceType => Str, slurpy Any, + ], + slurpy Any, ]; sub _process_bounce { - state $check = compile($Invocant, $BounceNotification); - my ($self, $notification) = $check->(@_); - - # disable each account that is bouncing - foreach my $recipient ( @{ $notification->{bounce}->{bouncedRecipients} } ) { - my $address = $recipient->{emailAddress}; - my $reason = sprintf '(%s) %s', $recipient->{action} // 'error', $recipient->{diagnosticCode} // 'unknown'; - - my $user = Bugzilla::User->new( { name => $address, cache => 1 } ); - if ($user) { - - # never auto-disable admin accounts - if ( $user->in_group('admin') ) { - Bugzilla->audit("ignoring bounce for admin <$address>: $reason"); - } - - else { - my $template = Bugzilla->template_inner(); - my $vars = { - mta => $notification->{bounce}->{reportingMTA} // 'unknown', - reason => $reason, - }; - my $disable_text; - $template->process( 'admin/users/bounce-disabled.txt.tmpl', $vars, \$disable_text ) - || die $template->error(); - - $user->set_disabledtext($disable_text); - $user->set_disable_mail(1); - $user->update(); - Bugzilla->audit( "bounce for <$address> disabled userid-" . $user->id . ": $reason" ); - } - } - - else { - Bugzilla->audit("bounce for <$address> has no user: $reason"); - } + state $check = compile($Invocant, $BounceNotification); + my ($self, $notification) = $check->(@_); + + # disable each account that is bouncing + foreach my $recipient (@{$notification->{bounce}->{bouncedRecipients}}) { + my $address = $recipient->{emailAddress}; + my $reason = sprintf '(%s) %s', $recipient->{action} // 'error', + $recipient->{diagnosticCode} // 'unknown'; + + my $user = Bugzilla::User->new({name => $address, cache => 1}); + if ($user) { + + # never auto-disable admin accounts + if ($user->in_group('admin')) { + Bugzilla->audit("ignoring bounce for admin <$address>: $reason"); + } + + else { + my $template = Bugzilla->template_inner(); + my $vars = { + mta => $notification->{bounce}->{reportingMTA} // 'unknown', + reason => $reason, + }; + my $disable_text; + $template->process('admin/users/bounce-disabled.txt.tmpl', + $vars, \$disable_text) + || die $template->error(); + + $user->set_disabledtext($disable_text); + $user->set_disable_mail(1); + $user->update(); + Bugzilla->audit( + "bounce for <$address> disabled userid-" . $user->id . ": $reason"); + } + } + + else { + Bugzilla->audit("bounce for <$address> has no user: $reason"); } + } - $self->_respond( 200 => 'OK' ); + $self->_respond(200 => 'OK'); } -my $ComplainedRecipients = ArrayRef[Dict[ emailAddress => Str, slurpy Any ]]; -my $ComplaintNotification = Dict[ - complaint => Dict [ - complainedRecipients => $ComplainedRecipients, - complaintFeedbackType => Str, - slurpy Any, - ], +my $ComplainedRecipients = ArrayRef [Dict [emailAddress => Str, slurpy Any]]; +my $ComplaintNotification = Dict [ + complaint => Dict [ + complainedRecipients => $ComplainedRecipients, + complaintFeedbackType => Str, slurpy Any, + ], + slurpy Any, ]; sub _process_complaint { - state $check = compile($Invocant, $ComplaintNotification); - my ($self, $notification) = $check->(@_); - my $template = Bugzilla->template_inner(); - my $json = JSON::MaybeXS->new( - pretty => 1, - utf8 => 1, - canonical => 1, - ); - - foreach my $recipient ( @{ $notification->{complaint}->{complainedRecipients} } ) { - my $reason = $notification->{complaint}->{complaintFeedbackType} // 'unknown'; - my $address = $recipient->{emailAddress}; - Bugzilla->audit("complaint for <$address> for '$reason'"); - my $vars = { - email => $address, - user => Bugzilla::User->new( { name => $address, cache => 1 } ), - reason => $reason, - notification => $json->encode($notification), - }; - my $message; - $template->process( 'email/ses-complaint.txt.tmpl', $vars, \$message ) - || die $template->error(); - MessageToMTA($message); - } + state $check = compile($Invocant, $ComplaintNotification); + my ($self, $notification) = $check->(@_); + my $template = Bugzilla->template_inner(); + my $json = JSON::MaybeXS->new(pretty => 1, utf8 => 1, canonical => 1,); + + foreach my $recipient (@{$notification->{complaint}->{complainedRecipients}}) + { + my $reason = $notification->{complaint}->{complaintFeedbackType} + // 'unknown'; + my $address = $recipient->{emailAddress}; + Bugzilla->audit("complaint for <$address> for '$reason'"); + my $vars = { + email => $address, + user => Bugzilla::User->new({name => $address, cache => 1}), + reason => $reason, + notification => $json->encode($notification), + }; + my $message; + $template->process('email/ses-complaint.txt.tmpl', $vars, \$message) + || die $template->error(); + MessageToMTA($message); + } - $self->_respond( 200 => 'OK' ); + $self->_respond(200 => 'OK'); } sub _respond { - my ( $self, $code, $message ) = @_; - $self->render(text => "$message\n", status => $code); + my ($self, $code, $message) = @_; + $self->render(text => "$message\n", status => $code); } sub _decode_json_wrapper { - state $check = compile($Invocant, Str); - my ($self, $json) = $check->(@_); - my $result; - my $ok = try { - $result = decode_json($json); - } - catch { - WARN( 'Malformed JSON from ' . $self->tx->remote_address ); - $self->_respond( 400 => 'Bad Request' ); - return undef; - }; - return $ok ? $result : undef; + state $check = compile($Invocant, Str); + my ($self, $json) = $check->(@_); + my $result; + my $ok = try { + $result = decode_json($json); + } + catch { + WARN('Malformed JSON from ' . $self->tx->remote_address); + $self->_respond(400 => 'Bad Request'); + return undef; + }; + return $ok ? $result : undef; } sub ua { - my $ua = LWP::UserAgent->new(); - $ua->timeout(10); - $ua->protocols_allowed( [ 'http', 'https' ] ); - if ( my $proxy_url = Bugzilla->params->{'proxy_url'} ) { - $ua->proxy( [ 'http', 'https' ], $proxy_url ); - } - else { - $ua->env_proxy; - } - return $ua; + my $ua = LWP::UserAgent->new(); + $ua->timeout(10); + $ua->protocols_allowed(['http', 'https']); + if (my $proxy_url = Bugzilla->params->{'proxy_url'}) { + $ua->proxy(['http', 'https'], $proxy_url); + } + else { + $ua->env_proxy; + } + return $ua; } 1; diff --git a/Bugzilla/Quantum/Static.pm b/Bugzilla/Quantum/Static.pm index 4543d1b84..6ac803e96 100644 --- a/Bugzilla/Quantum/Static.pm +++ b/Bugzilla/Quantum/Static.pm @@ -16,15 +16,15 @@ my $LEGACY_RE = qr{ }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; -- cgit v1.2.3-24-g4f1b From b12dbd21abedcfab84b249df52c5e5a039e3d028 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Wed, 3 Oct 2018 17:52:23 -0400 Subject: no bug - make libcmark-gfm optional (#792) --- Bugzilla/Install/Requirements.pm | 1 - 1 file changed, 1 deletion(-) (limited to 'Bugzilla') 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'] ); -- cgit v1.2.3-24-g4f1b From e3fb2fbabb83a12117ca8db405dacd1781c028cd Mon Sep 17 00:00:00 2001 From: Kohei Yoshino Date: Wed, 3 Oct 2018 22:39:57 -0400 Subject: Bug 1489120 - Add a rest API to get triage owners for each product/component pair (#797) --- Bugzilla/WebService/Product.pm | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'Bugzilla') diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm index 6ca3fee90..0b58c6e1c 100644 --- a/Bugzilla/WebService/Product.pm +++ b/Bugzilla/WebService/Product.pm @@ -215,6 +215,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 => @@ -548,6 +550,11 @@ default. C The login name of the user who will be set as the QA Contact for new bugs by default. +=item C + +C The login name of the user who is named as the Triage Owner of the +component. + =item C C Components, when displayed in a list, are sorted first by this integer -- cgit v1.2.3-24-g4f1b From d32521c8b2ca82469540caff9a6ce5a9ca3c4f7b Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Thu, 4 Oct 2018 09:34:13 -0400 Subject: Bug 1495901 - SES type checking error --- Bugzilla/Quantum/SES.pm | 101 ++++++++++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 42 deletions(-) (limited to 'Bugzilla') diff --git a/Bugzilla/Quantum/SES.pm b/Bugzilla/Quantum/SES.pm index 750da4e77..2d19de240 100644 --- a/Bugzilla/Quantum/SES.pm +++ b/Bugzilla/Quantum/SES.pm @@ -19,11 +19,60 @@ use JSON::MaybeXS qw(decode_json); use LWP::UserAgent (); use Try::Tiny qw(catch try); -use Types::Standard qw( :all ); -use Type::Utils; -use Type::Params qw( compile ); - -my $Invocant = class_type {class => __PACKAGE__}; +use Type::Library -base, -declare => qw( + Self + Notification NotificationType TypeField + BounceNotification BouncedRecipients + ComplaintNotification ComplainedRecipients +); +use Type::Utils -all; +use Types::Standard -all; +use Type::Params qw(compile); + +class_type Self, {class => __PACKAGE__}; + +declare ComplainedRecipients, + as ArrayRef [Dict [emailAddress => Str, slurpy Any]]; +declare ComplaintNotification, + as Dict [ + complaint => Dict [ + complainedRecipients => ComplainedRecipients, + complaintFeedbackType => Str, + slurpy Any, + ], + slurpy Any, + ]; + +declare BouncedRecipients, + as ArrayRef [ + Dict [ + emailAddress => Str, + action => Optional [Str], + diagnosticCode => Optional [Str], + status => Optional [Str], + slurpy Any, + ], + ]; +declare BounceNotification, + as Dict [ + bounce => Dict [ + bouncedRecipients => BouncedRecipients, + reportingMTA => Str, + bounceSubType => Str, + bounceType => Str, + slurpy Any, + ], + slurpy Any, + ]; + +declare NotificationType, as Enum [qw( Bounce Complaint )]; +declare TypeField, as Enum [qw(eventType notificationType)]; +declare Notification, + as Dict [ + eventType => Optional [NotificationType], + notificationType => Optional [NotificationType], + slurpy Any, + ]; sub main { my ($self) = @_; @@ -70,7 +119,7 @@ sub _main { } sub _confirm_subscription { - state $check = compile($Invocant, Dict [SubscribeURL => Str, slurpy Any]); + state $check = compile(Self, Dict [SubscribeURL => Str, slurpy Any]); my ($self, $message) = $check->(@_); my $subscribe_url = $message->{SubscribeURL}; @@ -91,16 +140,8 @@ sub _confirm_subscription { $self->_respond(200 => 'OK'); } -my $NotificationType = Enum [qw( Bounce Complaint )]; -my $TypeField = Enum [qw(eventType notificationType)]; -my $Notification = Dict [ - eventType => Optional [$NotificationType], - notificationType => Optional [$NotificationType], - slurpy Any, -]; - sub _handle_notification { - state $check = compile($Invocant, $Notification, $TypeField); + state $check = compile(Self, Notification, TypeField); my ($self, $notification, $type_field) = $check->(@_); if (!exists $notification->{$type_field}) { @@ -121,22 +162,8 @@ sub _handle_notification { return 1; } -my $BouncedRecipients = ArrayRef [ - Dict [emailAddress => Str, action => Str, diagnosticCode => Str, slurpy Any,], -]; -my $BounceNotification = Dict [ - bounce => Dict [ - bouncedRecipients => $BouncedRecipients, - reportingMTA => Str, - bounceSubType => Str, - bounceType => Str, - slurpy Any, - ], - slurpy Any, -]; - sub _process_bounce { - state $check = compile($Invocant, $BounceNotification); + state $check = compile(Self, BounceNotification); my ($self, $notification) = $check->(@_); # disable each account that is bouncing @@ -180,18 +207,8 @@ sub _process_bounce { $self->_respond(200 => 'OK'); } -my $ComplainedRecipients = ArrayRef [Dict [emailAddress => Str, slurpy Any]]; -my $ComplaintNotification = Dict [ - complaint => Dict [ - complainedRecipients => $ComplainedRecipients, - complaintFeedbackType => Str, - slurpy Any, - ], - slurpy Any, -]; - sub _process_complaint { - state $check = compile($Invocant, $ComplaintNotification); + state $check = compile(Self, ComplaintNotification); my ($self, $notification) = $check->(@_); my $template = Bugzilla->template_inner(); my $json = JSON::MaybeXS->new(pretty => 1, utf8 => 1, canonical => 1,); @@ -223,7 +240,7 @@ sub _respond { } sub _decode_json_wrapper { - state $check = compile($Invocant, Str); + state $check = compile(Self, Str); my ($self, $json) = $check->(@_); my $result; my $ok = try { -- cgit v1.2.3-24-g4f1b From f564933fdd8a06d44c4d5cc81a5c3e49e7be8928 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Fri, 5 Oct 2018 10:34:02 -0400 Subject: Bug 1496570 - Bugzilla doesn't attempt to decode form-urlencoded data without a content-type header --- Bugzilla/Quantum/CGI.pm | 2 ++ 1 file changed, 2 insertions(+) (limited to 'Bugzilla') diff --git a/Bugzilla/Quantum/CGI.pm b/Bugzilla/Quantum/CGI.pm index beb849687..79fbcfde6 100644 --- a/Bugzilla/Quantum/CGI.pm +++ b/Bugzilla/Quantum/CGI.pm @@ -91,6 +91,8 @@ sub _ENV { = $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; -- cgit v1.2.3-24-g4f1b From 75dbfe1dc03748957f07eca5ac583bedc6fdba76 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Tue, 9 Oct 2018 17:01:07 -0400 Subject: Bug 623384 - Use Module::Runtime instead of eval { require } or eval "use" --- Bugzilla/BugUrl.pm | 8 ++++---- Bugzilla/Config.pm | 3 ++- Bugzilla/DB.pm | 3 ++- Bugzilla/DB/Schema.pm | 11 ++++++++--- Bugzilla/WebService/Server.pm | 12 ++++++++++-- 5 files changed, 26 insertions(+), 11 deletions(-) (limited to 'Bugzilla') 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/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/DB.pm b/Bugzilla/DB.pm index 142c241bf..87110aaaa 100644 --- a/Bugzilla/DB.pm +++ b/Bugzilla/DB.pm @@ -34,6 +34,7 @@ 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', @@ -174,7 +175,7 @@ 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: " . $@); 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/WebService/Server.pm b/Bugzilla/WebService/Server.pm index a76c4c48c..e02788911 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(); -- cgit v1.2.3-24-g4f1b From 37d767c50d5ae69b13c47b71ba16b93c6b450730 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Tue, 9 Oct 2018 17:06:18 -0400 Subject: Bug 1497343 - Add some rudimentary type checking to Bugzilla::WebServe::Util::validate() --- Bugzilla/WebService/Bug.pm | 3 ++- Bugzilla/WebService/Util.pm | 24 ++++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) (limited to 'Bugzilla') diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm index 9003f3480..61a95e07d 100644 --- a/Bugzilla/WebService/Bug.pm +++ b/Bugzilla/WebService/Bug.pm @@ -1462,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 }; 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); -- cgit v1.2.3-24-g4f1b From 871fc7dd332dadd24a7e6e1db3c7f5e8ef93b00e Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Thu, 11 Oct 2018 15:28:18 -0400 Subject: Bug 1498206 - Replace LWP::UserAgent with Mojo::UserAgent in phabbugz extension --- Bugzilla/Test/Util.pm | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) (limited to 'Bugzilla') diff --git a/Bugzilla/Test/Util.pm b/Bugzilla/Test/Util.pm index 8124c25ee..9fbc151f7 100644 --- a/Bugzilla/Test/Util.pm +++ b/Bugzilla/Test/Util.pm @@ -12,10 +12,12 @@ use strict; use warnings; use base qw(Exporter); -our @EXPORT = qw(create_user issue_api_key); +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) = @_; @@ -47,4 +49,21 @@ sub issue_api_key { } } +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; -- cgit v1.2.3-24-g4f1b From 706d114f14beac2f7c68b4c3a3fc7cb58691aced Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Thu, 11 Oct 2018 22:47:24 -0400 Subject: Bug 1497487 - Backport bug 767623 to BMO: Use HMAC to generate tokens and sensitive graph filenames --- Bugzilla/Token.pm | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) (limited to 'Bugzilla') 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. -- cgit v1.2.3-24-g4f1b From 5688d0e712b85bc892ce405a1b79e3571f6d6d0f Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Sat, 13 Oct 2018 00:32:57 -0400 Subject: Bug 1495741 - memory issues: Avoid copying stuff in the webservice layer so much --- Bugzilla/WebService/JSON.pm | 64 +++++++++++++++++++++++++++++++++++ Bugzilla/WebService/JSON/Box.pm | 43 +++++++++++++++++++++++ Bugzilla/WebService/Server/JSONRPC.pm | 14 ++++++-- Bugzilla/WebService/Server/REST.pm | 1 + 4 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 Bugzilla/WebService/JSON.pm create mode 100644 Bugzilla/WebService/JSON/Box.pm (limited to 'Bugzilla') 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/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()); -- cgit v1.2.3-24-g4f1b From 5f5d1a83c44a7521622686e8e0a88a744e92c335 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Sat, 13 Oct 2018 00:33:18 -0400 Subject: Bug 1495741 - memory issues: disable etag generation for /rest/product --- Bugzilla/WebService/Product.pm | 2 ++ Bugzilla/WebService/Server.pm | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) (limited to 'Bugzilla') diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm index 0b58c6e1c..8ee6ff43f 100644 --- a/Bugzilla/WebService/Product.pm +++ b/Bugzilla/WebService/Product.pm @@ -68,6 +68,8 @@ 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'] }); diff --git a/Bugzilla/WebService/Server.pm b/Bugzilla/WebService/Server.pm index e02788911..c4bd3e605 100644 --- a/Bugzilla/WebService/Server.pm +++ b/Bugzilla/WebService/Server.pm @@ -81,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; -- cgit v1.2.3-24-g4f1b From 6657fa9f5210d5b5a9b14c0cba6882bd56232054 Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Sat, 13 Oct 2018 00:33:48 -0400 Subject: Bug 1495741 - memory issues: use local flag cache --- Bugzilla/WebService/Product.pm | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'Bugzilla') diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm index 8ee6ff43f..cdd8a0a92 100644 --- a/Bugzilla/WebService/Product.pm +++ b/Bugzilla/WebService/Product.pm @@ -64,6 +64,7 @@ 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; @@ -136,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 }; @@ -229,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'}}], }; } @@ -242,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 => @@ -266,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 { -- cgit v1.2.3-24-g4f1b