From cc79413a506a3493094f186f24bf27a89d8cd4ad Mon Sep 17 00:00:00 2001 From: Julian Maurice Date: Thu, 12 Apr 2018 20:42:13 +0200 Subject: [PATCH] Bug 20582: Turn Koha into a Mojolicious application This patch is a proof-of-concept of Koha as a Mojolicious application This has several benefits: - Run `morbo bin/koha` and you're set : fully functional Koha without apache2/nginx (URL rewrites and static files are handled by the app) - apache2/nginx configuration is much simpler (an example of nginx configuration is included in the patch) - starman and plack middlewares can still be used for debug or gzip compression for instance (see bin/koha.psgi) - All that is needed to run koha is in the source code and not hidden in an obscure directory (I'm looking at you, debian/templates/plack.psgi) - Using Test::Mojo we can test the whole application, as we do with the REST API (which is a Mojolicious application too) - It's another step in the direction of dropping CGI support - It also opens a way for converting CGI scripts into Mojolicious controllers and actions (even if that's not possible at the moment because of the authentication code) - It requires zero changes to existing files, so it should apply nicely forever (no rebase needed \o/) A few downsides too: - It is not complete : some things from debian/templates/plack.psgi are missing, like the L1 cache flush or Koha::Middleware::SetEnv. But that shouldn't be too hard to add. - Already mentioned above, but the actual state of get_template_and_user and checkauth completely forbids the use of Mojolicious controllers. We really need to rewrite C4::Auth - Not sure if it's a downside or not, but as OPAC and Intranet are on the same host:port, if you authenticate on OPAC you will be authenticated on Intranet too. This can probably be changed if it's really a problem. - ... see FIXME in the code Other remarks: - It uses the same mechanism as Plack::App::CGIBin to deal with CGI scripts, so it should be equivalent in terms of performance How to test ? - Run `morbo bin/koha`, then go to: - http://localhost:3000/ - Redirects to OPAC main page - Then navigate in the OPAC, searching for bugs - http://localhost:3000/intranet - Redirects to intranet main page - Then navigate in the Intranet, still searching for bugs - http://localhost:3000/api/v1/... - The REST API - Test the web installer - Read the code (and the comments), it's not very long Signed-off-by: Josef Moravec --- Koha/App/Koha.pm | 56 ++++++++++++++++++++++++++++++++ Koha/App/Plugin/CGIBinKoha.pm | 75 +++++++++++++++++++++++++++++++++++++++++++ Koha/App/Plugin/RESTV1.pm | 15 +++++++++ bin/koha | 11 +++++++ bin/koha.psgi | 13 ++++++++ etc/nginx.conf | 15 +++++++++ 6 files changed, 185 insertions(+) create mode 100644 Koha/App/Koha.pm create mode 100644 Koha/App/Plugin/CGIBinKoha.pm create mode 100644 Koha/App/Plugin/RESTV1.pm create mode 100755 bin/koha create mode 100755 bin/koha.psgi create mode 100644 etc/nginx.conf diff --git a/Koha/App/Koha.pm b/Koha/App/Koha.pm new file mode 100644 index 0000000000..c3e11d4e91 --- /dev/null +++ b/Koha/App/Koha.pm @@ -0,0 +1,56 @@ +package Koha::App::Koha; + +use Modern::Perl; + +use Mojo::Base 'Mojolicious'; + +sub startup { + my ($self) = @_; + + push @{$self->plugins->namespaces}, 'Koha::App::Plugin'; + push @{$self->static->paths}, $self->home->rel_file('koha-tmpl'); + + # Create route for all CGI scripts, need to be loaded first because of + # CGI::Compile + $self->plugin('CGIBinKoha'); + + # Create routes for API + # FIXME This generates routes like this: /api/api/v1/... + $self->plugin('RESTV1'); + + $self->hook(before_dispatch => \&_before_dispatch); + + # Everything after this comment is only needed to use Mojolicious + # controllers. + # We can't use them for anything serious right now because of the + # authentication code that needs a full rewrite + + $self->plugin('TemplateToolkit', template => {PLUGIN_BASE => 'Koha::Template::Plugin'}); + $self->renderer->default_handler('tt2'); + + $self->routes->namespaces(['Koha::Controller']); + + my $r = $self->routes; + + $r->any('/')->to(cb => sub { shift->redirect_to('/cgi-bin/koha/opac-main.pl') }); + $r->any('/intranet')->to(cb => sub { shift->redirect_to('/cgi-bin/koha/mainpage.pl') }); +} + +sub _before_dispatch { + my $c = shift; + + # URL rewriting in Koha! No Apache/Nginx conf needed + my $url = $c->req->url->path->to_string; + + # Remove Koha version from URL + $url =~ s/_\d{2}\.\d{7}\.(js|css)/.$1/; + + # See FIXME above + if ($url =~ m|^/api/v|) { + $url = '/api' . $url; + } + + $c->req->url->parse($url); +} + +1; diff --git a/Koha/App/Plugin/CGIBinKoha.pm b/Koha/App/Plugin/CGIBinKoha.pm new file mode 100644 index 0000000000..e3edeffdf4 --- /dev/null +++ b/Koha/App/Plugin/CGIBinKoha.pm @@ -0,0 +1,75 @@ +package Koha::App::Plugin::CGIBinKoha; + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Plugin'; + +use CGI::Compile; +use CGI::Emulate::PSGI; +use IO::Scalar; + +sub register { + my ($self, $app) = @_; + + $app->routes->any('/cgi-bin/koha/*name' => sub { + my ($c) = @_; + + my $script = $c->stash('name'); + + # Special case for installer which can takes a long time + $c->inactivity_timeout(300) if $script eq 'installer/install.pl'; + + # Remove trailing slash, if any (e.g. .../svc/config/systempreferences/) + $script =~ s|/$||; + + # Guess if it's an OPAC script, or an Intranet script + # FIXME: errors scripts (400.pl, 401.pl, ...) exist in both intranet and + # opac. The intranet ones will never be executed + # Same for svc/club/cancel_enrollment, svc/club/enroll and svc/report + if (-e $c->app->home->rel_file("opac/$script")) { + $script = "opac/$script"; + } + + unless (-e $c->app->home->rel_file($script)) { + return $c->reply->not_found; + } + + my $sub = CGI::Compile->compile($script); + my $app = CGI::Emulate::PSGI->handler($sub); + my $response = $app->($self->_psgi_env($c)); + + $c->res->code($response->[0]); + $c->res->headers->from_hash({ @{ $response->[1] } }); + $c->res->body(join('', @{$response->[2]})); + $c->rendered; + })->name('cgi'); +} + +sub _psgi_env { + my ($self, $c) = @_; + + my $body = $c->req->body; + my $env = { + 'psgi.input' => IO::Scalar->new(\$body), + 'psgi.errors' => *STDERR, + REQUEST_METHOD => $c->req->method, + QUERY_STRING => $c->req->url->query->to_string, + SERVER_NAME => $c->req->url->to_abs->host, + SERVER_PORT => $c->req->url->to_abs->port, + SERVER_PROTOCOL => 'HTTP/1.1', + CONTENT_LENGTH => $c->req->headers->content_length, + CONTENT_TYPE => $c->req->headers->content_type, + REMOTE_ADDR => $c->tx->remote_address, + SCRIPT_NAME => $c->req->url->path->to_string, + }; + for my $name (@{ $c->req->headers->names }) { + my $value = $c->req->headers->header($name); + $name =~ s/-/_/g; + $name = 'HTTP_' . uc($name); + $env->{$name} = $value; + } + + return $env; +} + +1; diff --git a/Koha/App/Plugin/RESTV1.pm b/Koha/App/Plugin/RESTV1.pm new file mode 100644 index 0000000000..6f59d044a8 --- /dev/null +++ b/Koha/App/Plugin/RESTV1.pm @@ -0,0 +1,15 @@ +package Koha::App::Plugin::RESTV1; + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Plugin'; + +use Koha::REST::V1; + +sub register { + my ($self, $app) = @_; + + $app->routes->any('/api')->detour(app => Koha::REST::V1->new); +} + +1; diff --git a/bin/koha b/bin/koha new file mode 100755 index 0000000000..2239025a99 --- /dev/null +++ b/bin/koha @@ -0,0 +1,11 @@ +#!/usr/bin/env perl + +use Modern::Perl; + +# No need for PERL5LIB +use FindBin; +use lib "$FindBin::Bin/.."; + +use Mojolicious::Commands; + +Mojolicious::Commands->start_app('Koha::App::Koha'); diff --git a/bin/koha.psgi b/bin/koha.psgi new file mode 100755 index 0000000000..7857de3455 --- /dev/null +++ b/bin/koha.psgi @@ -0,0 +1,13 @@ +#!/usr/bin/env starman + +use Modern::Perl; + +use FindBin; +use Mojo::Server::PSGI; +use Plack::Builder; + +builder { + my $server = Mojo::Server::PSGI->new; + $server->load_app("$FindBin::Bin/koha"); + $server->to_psgi_app; +}; diff --git a/etc/nginx.conf b/etc/nginx.conf new file mode 100644 index 0000000000..da2184b1de --- /dev/null +++ b/etc/nginx.conf @@ -0,0 +1,15 @@ +upstream koha { + server 127.0.0.1:3000; +} + +server { + listen 80; + listen [::]:80; + + server_name koha-dev; # CHANGEME + + location / { + include proxy_params; + proxy_pass http://koha; + } +} -- 2.11.0