Commit 8bbc156c authored by Frédéric Buclin's avatar Frédéric Buclin

Bug 1201113: Support to run Bugzilla as a PSGI application

r=dylan
parent c94abb04
# Don't allow people to retrieve non-cgi executable files or our private data
<FilesMatch (\.pm|\.pl|\.tmpl|localconfig.*|cpanfile)$>
<FilesMatch (\.pm|\.pl|\.psgi|\.tmpl|localconfig.*|cpanfile)$>
<IfModule mod_version.c>
<IfVersion < 2.4>
Deny from all
......
......@@ -13,8 +13,11 @@ use warnings;
# We want any compile errors to get to the browser, if possible.
BEGIN {
# This makes sure we're in a CGI.
if ($ENV{SERVER_SOFTWARE} && !$ENV{MOD_PERL}) {
# This makes sure we're in a CGI. mod_perl doesn't support Carp
# and Plack reports errors elsewhere.
# We cannot call i_am_persistent() from here as its module is
# not loaded yet.
if ($ENV{SERVER_SOFTWARE} && !($ENV{MOD_PERL} || $ENV{BZ_PLACK})) {
require CGI::Carp;
CGI::Carp->import('fatalsToBrowser');
}
......@@ -32,7 +35,7 @@ use Bugzilla::Field;
use Bugzilla::Flag;
use Bugzilla::Install::Localconfig qw(read_localconfig);
use Bugzilla::Install::Requirements qw(OPTIONAL_MODULES have_vers);
use Bugzilla::Install::Util qw(init_console include_languages);
use Bugzilla::Install::Util qw(init_console include_languages i_am_persistent);
use Bugzilla::Memcached;
use Bugzilla::Template;
use Bugzilla::Token;
......@@ -149,11 +152,16 @@ sub init_page {
{
exit;
}
# Plack requires to exit differently.
return -1 if $ENV{BZ_PLACK};
_shutdown();
}
}
sub _shutdown {
# For security reasons, log out users when Bugzilla is down.
# Bugzilla->login() is required to catch the logincookie, if any.
my $user;
eval { $user = Bugzilla->login(LOGIN_OPTIONAL); };
my $user = eval { Bugzilla->login(LOGIN_OPTIONAL); };
if ($@) {
# The DB is not accessible. Use the default user object.
$user = Bugzilla->user;
......@@ -162,30 +170,28 @@ sub init_page {
my $userid = $user->id;
Bugzilla->logout();
my $template = Bugzilla->template;
my $vars = {};
$vars->{'message'} = 'shutdown';
$vars->{'userid'} = $userid;
# Generate and return a message about the downtime, appropriately
# for if we're a command-line script or a CGI script.
my $extension;
if (i_am_cgi() && (!Bugzilla->cgi->param('ctype')
|| Bugzilla->cgi->param('ctype') eq 'html')) {
$extension = 'html';
}
else {
$extension = 'txt';
}
my $cgi = Bugzilla->cgi;
my $extension = 'txt';
if (i_am_cgi()) {
# Set the HTTP status to 503 when Bugzilla is down to avoid pages
# being indexed by search engines.
print Bugzilla->cgi->header(-status => 503,
print $cgi->header(-status => 503,
-retry_after => SHUTDOWNHTML_RETRY_AFTER);
if (!$cgi->param('ctype') || $cgi->param('ctype') eq 'html') {
$extension = 'html';
}
}
my $template = Bugzilla->template;
my $vars = { message => 'shutdown', userid => $userid };
$template->process("global/message.$extension.tmpl", $vars)
|| ThrowTemplateError($template->error);
or ThrowTemplateError($template->error);
exit;
}
}
#####################################################################
......@@ -714,11 +720,13 @@ sub _cleanup {
}
sub END {
# Bugzilla.pm cannot compile in mod_perl.pl if this runs.
_cleanup() unless $ENV{MOD_PERL};
# This is managed in mod_perl.pl and app.psgi when running
# in a persistent environment.
_cleanup() unless i_am_persistent();
}
init_page() if !$ENV{MOD_PERL};
# Also managed in mod_perl.pl and app.psgi.
init_page() unless i_am_persistent();
1;
......
......@@ -66,7 +66,7 @@ sub new {
# else we will be redirected outside Bugzilla.
my $script_name = $self->script_name;
$path_info =~ s/^\Q$script_name\E//;
if ($path_info) {
if ($script_name && $path_info) {
print $self->redirect($self->url(-path => 0, -query => 1));
}
}
......@@ -283,7 +283,7 @@ sub close_standby_message {
print $self->multipart_end();
print $self->multipart_start(-type => $contenttype);
}
else {
elsif (!$self->{_header_done}) {
print $self->header($contenttype);
}
}
......@@ -356,6 +356,7 @@ sub header {
Bugzilla::Hook::process('cgi_headers',
{ cgi => $self, headers => \%headers }
);
$self->{_header_done} = 1;
return $self->SUPER::header(%headers) || "";
}
......
......@@ -16,6 +16,7 @@ use autodie qw(:default);
use Bugzilla::Constants;
use Bugzilla::Hook;
use Bugzilla::Install::Util qw(i_am_persistent);
use Bugzilla::Util qw(trick_taint);
use JSON::XS;
......@@ -324,8 +325,10 @@ sub read_param_file {
# might not have thrown an error. Bugzilla::CGI->new
# hasn't even been called yet, so we manually use CGI::Carp here
# so that the user sees the error.
unless (i_am_persistent()) {
require CGI::Carp;
CGI::Carp->import('fatalsToBrowser');
}
die "The $file file does not exist."
. ' You probably need to run checksetup.pl.',
}
......
......@@ -28,8 +28,9 @@ use Date::Format;
sub _in_eval {
my $in_eval = 0;
for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) {
last if $sub =~ /^ModPerl/;
$in_eval = 1 if $sub =~ /^\(eval\)/;
last if $sub =~ /^(?:ModPerl|Plack|CGI::Compile)/;
# An eval followed by CGI::Compile is not a "real" eval.
$in_eval = 1 if $sub =~ /^\(eval\)/ && (caller($stack + 1))[3] !~ /^CGI::Compile/;
}
return $in_eval;
}
......
......@@ -167,6 +167,7 @@ sub FILESYSTEM {
'install-module.pl' => { perms => OWNER_EXECUTE },
'clean-bug-user-last-visit.pl' => { perms => WS_EXECUTE },
'app.psgi' => { perms => CGI_READ },
'Bugzilla.pm' => { perms => CGI_READ },
"$localconfig*" => { perms => CGI_READ },
'bugzilla.dtd' => { perms => WS_SERVE },
......
......@@ -313,6 +313,26 @@ sub OPTIONAL_MODULES {
feature => ['jsonrpc'],
},
{
package => 'Plack',
module => 'Plack',
# 1.0031 contains a security fix which would affect us.
# It also fixes warnings thrown in Perl 5.20 and newer.
version => 1.0031,
feature => ['psgi'],
},
{
package => 'CGI-Compile',
module => 'CGI::Compile',
version => 0,
feature => ['psgi'],
},
{
package => 'CGI-Emulate-PSGI',
module => 'CGI::Emulate::PSGI',
version => 0,
feature => ['psgi'],
},
{
package => 'Test-Taint',
module => 'Test::Taint',
# 1.06 no longer throws warnings with Perl 5.10+.
......@@ -474,6 +494,7 @@ use constant FEATURE_FILES => (
'Bugzilla/WebService.pm', 'Bugzilla/WebService/*.pm'],
rest => ['Bugzilla/WebService/Server/REST.pm', 'rest.cgi',
'Bugzilla/WebService/Server/REST/Resources/*.pm'],
psgi => ['app.psgi'],
moving => ['importxml.pl'],
auth_ldap => ['Bugzilla/Auth/Verify/LDAP.pm'],
auth_radius => ['Bugzilla/Auth/Verify/RADIUS.pm'],
......
......@@ -34,6 +34,7 @@ our @EXPORT_OK = qw(
extension_requirement_packages
extension_template_directory
extension_web_directory
i_am_persistent
indicate_progress
install_string
include_languages
......@@ -83,6 +84,10 @@ sub get_version_and_os {
os_ver => $os_details[3] };
}
sub i_am_persistent {
return ($ENV{MOD_PERL} || $ENV{BZ_PLACK}) ? 1 : 0;
}
sub _extension_paths {
my $dir = bz_locations()->{'extensionsdir'};
my @extension_items = glob("$dir/*");
......@@ -711,6 +716,11 @@ binary, if the binary is in the C<PATH>.
Returns a hash containing information about what version of Bugzilla we're
running, what perl version we're using, and what OS we're running on.
=item C<i_am_persistent>
Returns true if Bugzilla is running in a persistent environment, such as
mod_perl or PSGI. Returns false if running in mod_cgi mode.
=item C<get_console_locale>
Returns the language to use based on the LC_CTYPE value returned by the OS.
......
......@@ -17,7 +17,7 @@ use Bugzilla::WebService::Constants;
use Bugzilla::Hook;
use Bugzilla::Install::Requirements;
use Bugzilla::Install::Util qw(install_string template_include_path
include_languages);
include_languages i_am_persistent);
use Bugzilla::Classification;
use Bugzilla::Keyword;
use Bugzilla::Util;
......@@ -740,7 +740,7 @@ sub create {
# if a packager has modified bz_locations() to contain absolute
# paths.
ABSOLUTE => 1,
RELATIVE => $ENV{MOD_PERL} ? 0 : 1,
RELATIVE => i_am_persistent() ? 0 : 1,
COMPILE_DIR => bz_locations()->{'template_cache'},
......
#!/usr/bin/perl
# 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 strict;
use warnings;
use File::Basename;
use lib dirname(__FILE__);
use Bugzilla::Constants ();
use lib Bugzilla::Constants::bz_locations()->{ext_libpath};
use Plack;
use Plack::Builder;
use Plack::App::URLMap;
use Plack::App::WrapCGI;
use Plack::Response;
use constant STATIC => qw(
data/assets
data/webdot
docs
extensions/[^/]+/web
graphs
images
js
skins
);
builder {
my $static_paths = join('|', STATIC);
enable 'Static',
path => qr{^/($static_paths)/},
root => Bugzilla::Constants::bz_locations->{cgi_path};
$ENV{BZ_PLACK} = 'Plack/' . Plack->VERSION;
my $map = Plack::App::URLMap->new;
my @cgis = glob('*.cgi');
my $shutdown_app = Plack::App::WrapCGI->new(script => 'shutdown.cgi')->to_app;
foreach my $cgi_script (@cgis) {
my $app = eval { Plack::App::WrapCGI->new(script => $cgi_script)->to_app };
# Some CGI scripts won't compile if not all optional Perl modules are
# installed. That's expected.
if ($@) {
warn "Cannot compile $cgi_script. Skipping!\n";
next;
}
my $wrapper = sub {
my $ret = Bugzilla::init_page();
my $res = ($ret eq '-1' && $cgi_script ne 'editparams.cgi') ? $shutdown_app->(@_) : $app->(@_);
Bugzilla::_cleanup();
return $res;
};
my $base_name = basename($cgi_script);
$map->map('/' => $wrapper) if $cgi_script eq 'index.cgi';
$map->map('/rest' => $wrapper) if $cgi_script eq 'rest.cgi';
$map->map("/$base_name" => $wrapper);
}
my $app = $map->to_app;
};
......@@ -38,7 +38,6 @@ sub LoadTemplate {
$action =~ /(\w+)/;
$action = $1;
print $cgi->header();
$template->process("admin/classifications/$action.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
exit;
......
......@@ -136,7 +136,6 @@ unless ($action) {
my @groups = Bugzilla::Group->get_all;
$vars->{'groups'} = \@groups;
print $cgi->header();
$template->process("admin/groups/list.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
exit;
......@@ -157,10 +156,8 @@ if ($action eq 'changeform') {
$vars->{'group'} = $group;
$vars->{'token'} = issue_session_token('edit_group');
print $cgi->header();
$template->process("admin/groups/edit.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
exit;
}
......@@ -172,10 +169,9 @@ if ($action eq 'changeform') {
if ($action eq 'add') {
$vars->{'token'} = issue_session_token('add_group');
print $cgi->header();
$template->process("admin/groups/create.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
exit;
}
......@@ -204,7 +200,6 @@ if ($action eq 'new') {
get_current_and_available($group, $vars);
$vars->{'token'} = issue_session_token('edit_group');
print $cgi->header();
$template->process("admin/groups/edit.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
exit;
......@@ -228,10 +223,8 @@ if ($action eq 'del') {
$vars->{'group'} = $group;
$vars->{'token'} = issue_session_token('delete_group');
print $cgi->header();
$template->process("admin/groups/delete.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
exit;
}
......@@ -255,7 +248,6 @@ if ($action eq 'delete') {
$vars->{'message'} = 'group_deleted';
$vars->{'groups'} = [Bugzilla::Group->get_all];
print $cgi->header();
$template->process("admin/groups/list.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
exit;
......@@ -277,7 +269,6 @@ if ($action eq 'postchanges') {
$vars->{'changes'} = $changes;
$vars->{'token'} = issue_session_token('edit_group');
print $cgi->header();
$template->process("admin/groups/edit.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
exit;
......@@ -288,6 +279,7 @@ if ($action eq 'confirm_remove') {
$vars->{'group'} = $group;
$vars->{'regexp'} = CheckGroupRegexp($cgi->param('regexp'));
$vars->{'token'} = issue_session_token('remove_group_members');
$template->process('admin/groups/confirm-remove.html.tmpl', $vars)
|| ThrowTemplateError($template->error());
exit;
......@@ -326,10 +318,8 @@ if ($action eq 'remove_regexp') {
$vars->{'group'} = $group->name;
$vars->{'groups'} = [Bugzilla::Group->get_all];
print $cgi->header();
$template->process("admin/groups/list.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
exit;
}
......
......@@ -24,10 +24,6 @@ my $dbh = Bugzilla->dbh;
my $template = Bugzilla->template;
my $vars = {};
#
# Preliminary checks:
#
my $user = Bugzilla->login(LOGIN_REQUIRED);
print $cgi->header();
......@@ -47,22 +43,16 @@ $vars->{'action'} = $action;
if ($action eq "") {
$vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
print $cgi->header();
$template->process("admin/keywords/list.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
exit;
}
if ($action eq 'add') {
$vars->{'token'} = issue_session_token('add_keyword');
print $cgi->header();
$template->process("admin/keywords/create.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
exit;
}
......@@ -79,8 +69,6 @@ if ($action eq 'new') {
delete_token($token);
print $cgi->header();
$vars->{'message'} = 'keyword_created';
$vars->{'name'} = $keyword->name;
$vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
......@@ -90,7 +78,6 @@ if ($action eq 'new') {
exit;
}
#
# action='edit' -> present the edit keywords from
#
......@@ -104,13 +91,11 @@ if ($action eq 'edit') {
$vars->{'keyword'} = $keyword;
$vars->{'token'} = issue_session_token('edit_keyword');
print $cgi->header();
$template->process("admin/keywords/edit.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
exit;
}
#
# action='update' -> update the keyword
#
......@@ -129,8 +114,6 @@ if ($action eq 'update') {
delete_token($token);
print $cgi->header();
$vars->{'message'} = 'keyword_updated';
$vars->{'keyword'} = $keyword;
$vars->{'changes'} = $changes;
......@@ -148,7 +131,6 @@ if ($action eq 'del') {
$vars->{'keyword'} = $keyword;
$vars->{'token'} = issue_session_token('delete_keyword');
print $cgi->header();
$template->process("admin/keywords/confirm-delete.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
exit;
......@@ -163,8 +145,6 @@ if ($action eq 'delete') {
delete_token($token);
print $cgi->header();
$vars->{'message'} = 'keyword_deleted';
$vars->{'keyword'} = $keyword;
$vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
......
#!/usr/bin/perl -T
# 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 strict;
use warnings;
use lib qw(. lib);
use Bugzilla;
Bugzilla::_shutdown();
......@@ -40,7 +40,7 @@ foreach my $file (@testitems) {
ok(1,"$file does not have a shebang");
} else {
my $flags;
if (!defined $ext || $ext eq "pl") {
if (!defined $ext || $ext eq 'pl' || $ext eq 'psgi') {
# standalone programs aren't taint checked yet
if (grep { $file eq $_ } @require_taint) {
$flags = 'T';
......
......@@ -34,7 +34,7 @@ sub isTestingFile {
my ($file) = @_;
my $exclude;
if ($file =~ /\.cgi$|\.pl$|\.pm$/) {
if ($file =~ /\.psgi$|\.cgi$|\.pl$|\.pm$/) {
return 1;
}
my $additional;
......
......@@ -103,6 +103,7 @@ END
feature_mod_perl => 'mod_perl',
feature_moving => 'Move Bugs Between Installations',
feature_patch_viewer => 'Patch Viewer',
feature_psgi => 'PSGI Support',
feature_rest => 'REST Interface',
feature_smtp_auth => 'SMTP Authentication',
feature_smtp_ssl => 'SSL Support for SMTP',
......
......@@ -15,5 +15,6 @@ use strict;
use warnings;
say "content-type:text/plain\n";
say "OK " . ($::ENV{MOD_PERL} || "mod_cgi");
exit;
print 'OK ';
say $ENV{BZ_PLACK} || $ENV{MOD_PERL} || 'mod_cgi';
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment