summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.htaccess2
-rw-r--r--Bugzilla.pm86
-rw-r--r--Bugzilla/CGI.pm5
-rw-r--r--Bugzilla/Config.pm21
-rw-r--r--Bugzilla/Error.pm5
-rw-r--r--Bugzilla/Install/Filesystem.pm1
-rw-r--r--Bugzilla/Install/Requirements.pm21
-rw-r--r--Bugzilla/Install/Util.pm10
-rw-r--r--Bugzilla/Template.pm4
-rw-r--r--app.psgi70
-rwxr-xr-xeditclassifications.cgi1
-rwxr-xr-xeditgroups.cgi18
-rwxr-xr-xeditkeywords.cgi20
-rwxr-xr-xshutdown.cgi17
-rw-r--r--t/002goodperl.t2
-rw-r--r--t/Support/Files.pm2
-rw-r--r--template/en/default/setup/strings.txt.pl1
-rwxr-xr-xtestagent.cgi5
18 files changed, 197 insertions, 94 deletions
diff --git a/.htaccess b/.htaccess
index 973b396d4..f9eeb541c 100644
--- a/.htaccess
+++ b/.htaccess
@@ -1,5 +1,5 @@
# 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
diff --git a/Bugzilla.pm b/Bugzilla.pm
index c6d7ae39b..16075b2d1 100644
--- a/Bugzilla.pm
+++ b/Bugzilla.pm
@@ -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,43 +152,46 @@ sub init_page {
{
exit;
}
+ # Plack requires to exit differently.
+ return -1 if $ENV{BZ_PLACK};
+ _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); };
- if ($@) {
- # The DB is not accessible. Use the default user object.
- $user = Bugzilla->user;
- $user->{settings} = {};
- }
- 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')) {
+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 { Bugzilla->login(LOGIN_OPTIONAL); };
+ if ($@) {
+ # The DB is not accessible. Use the default user object.
+ $user = Bugzilla->user;
+ $user->{settings} = {};
+ }
+ my $userid = $user->id;
+ Bugzilla->logout();
+
+ # Generate and return a message about the downtime, appropriately
+ # for if we're a command-line script or a CGI script.
+ 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 $cgi->header(-status => 503,
+ -retry_after => SHUTDOWNHTML_RETRY_AFTER);
+
+ if (!$cgi->param('ctype') || $cgi->param('ctype') eq 'html') {
$extension = 'html';
}
- else {
- $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,
- -retry_after => SHUTDOWNHTML_RETRY_AFTER);
- }
- $template->process("global/message.$extension.tmpl", $vars)
- || ThrowTemplateError($template->error);
- exit;
}
+
+ my $template = Bugzilla->template;
+ my $vars = { message => 'shutdown', userid => $userid };
+
+ $template->process("global/message.$extension.tmpl", $vars)
+ 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;
diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm
index fcca0ec6a..25ee0acbe 100644
--- a/Bugzilla/CGI.pm
+++ b/Bugzilla/CGI.pm
@@ -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) || "";
}
diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm
index 5dfe2e37d..d47577212 100644
--- a/Bugzilla/Config.pm
+++ b/Bugzilla/Config.pm
@@ -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;
@@ -319,15 +320,17 @@ sub read_param_file {
}
}
elsif ($ENV{'SERVER_SOFTWARE'}) {
- # We're in a CGI, but the params file doesn't exist. We can't
- # Template Toolkit, or even install_string, since checksetup
- # 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.
- require CGI::Carp;
- CGI::Carp->import('fatalsToBrowser');
- die "The $file file does not exist."
- . ' You probably need to run checksetup.pl.',
+ # We're in a CGI, but the params file doesn't exist. We can't
+ # Template Toolkit, or even install_string, since checksetup
+ # 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.',
}
return \%params;
}
diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm
index e730022db..ee40ccf8b 100644
--- a/Bugzilla/Error.pm
+++ b/Bugzilla/Error.pm
@@ -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;
}
diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm
index 5f5677460..e17285b2f 100644
--- a/Bugzilla/Install/Filesystem.pm
+++ b/Bugzilla/Install/Filesystem.pm
@@ -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 },
diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm
index 9a03968d7..a48778c1b 100644
--- a/Bugzilla/Install/Requirements.pm
+++ b/Bugzilla/Install/Requirements.pm
@@ -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'],
diff --git a/Bugzilla/Install/Util.pm b/Bugzilla/Install/Util.pm
index c05037061..82752b961 100644
--- a/Bugzilla/Install/Util.pm
+++ b/Bugzilla/Install/Util.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.
diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm
index 04abe8200..9398ca4b5 100644
--- a/Bugzilla/Template.pm
+++ b/Bugzilla/Template.pm
@@ -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'},
diff --git a/app.psgi b/app.psgi
new file mode 100644
index 000000000..c04359fb1
--- /dev/null
+++ b/app.psgi
@@ -0,0 +1,70 @@
+#!/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;
+};
diff --git a/editclassifications.cgi b/editclassifications.cgi
index ea4b139da..f839cfa03 100755
--- a/editclassifications.cgi
+++ b/editclassifications.cgi
@@ -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;
diff --git a/editgroups.cgi b/editgroups.cgi
index 35989b954..f2c915556 100755
--- a/editgroups.cgi
+++ b/editgroups.cgi
@@ -135,8 +135,7 @@ sub get_current_and_available {
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;
@@ -155,12 +154,10 @@ if ($action eq 'changeform') {
get_current_and_available($group, $vars);
$vars->{'group'} = $group;
- $vars->{'token'} = issue_session_token('edit_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;
}
diff --git a/editkeywords.cgi b/editkeywords.cgi
index ab079e540..da7513f6f 100755
--- a/editkeywords.cgi
+++ b/editkeywords.cgi
@@ -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();
diff --git a/shutdown.cgi b/shutdown.cgi
new file mode 100755
index 000000000..7b33ec7c4
--- /dev/null
+++ b/shutdown.cgi
@@ -0,0 +1,17 @@
+#!/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();
diff --git a/t/002goodperl.t b/t/002goodperl.t
index d1858361f..cfc9fb9e9 100644
--- a/t/002goodperl.t
+++ b/t/002goodperl.t
@@ -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';
diff --git a/t/Support/Files.pm b/t/Support/Files.pm
index f3fae58fc..f1c88e858 100644
--- a/t/Support/Files.pm
+++ b/t/Support/Files.pm
@@ -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;
diff --git a/template/en/default/setup/strings.txt.pl b/template/en/default/setup/strings.txt.pl
index d1bb873ca..4409d9ff3 100644
--- a/template/en/default/setup/strings.txt.pl
+++ b/template/en/default/setup/strings.txt.pl
@@ -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',
diff --git a/testagent.cgi b/testagent.cgi
index d9d5afd1a..dfb1ff228 100755
--- a/testagent.cgi
+++ b/testagent.cgi
@@ -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';