| package Kalamar; | 
 | use Mojo::Base 'Mojolicious'; | 
 | use Mojo::ByteStream 'b'; | 
 | use Mojo::URL; | 
 | use Mojo::File; | 
 | use Mojo::JSON qw/decode_json encode_json/; | 
 | use Mojo::Util qw/url_escape deprecated slugify/; | 
 | use List::Util qw!none uniq!; | 
 |  | 
 | # Minor version - may be patched from package.json | 
 | our $VERSION = '0.60'; | 
 |  | 
 | # Supported version of Backend API | 
 | our $API_VERSION = '1.0'; | 
 |  | 
 | # TODO: The FAQ-Page has a contact form for new questions | 
 | # TODO: Embed query serialization | 
 | # TODO: Embed collection statistics | 
 | # TODO: Implement tab opener for matches and the tutorial | 
 | # TODO: Implement a "projects" system | 
 |  | 
 | # Start the application and register all routes and plugins | 
 | sub startup { | 
 |   my $self = shift; | 
 |  | 
 |   # Set version based on package file | 
 |   # This may introduce a SemVer patch number | 
 |   my $pkg_path = $self->home->child('package.json'); | 
 |   if ($ENV{KALAMAR_VERSION}) { | 
 |     $Kalamar::VERSION = $ENV{KALAMAR_VERSION}; | 
 |   } | 
 |   elsif (-e $pkg_path->to_abs) { | 
 |     my $pkg = $pkg_path->slurp; | 
 |     $Kalamar::VERSION = decode_json($pkg)->{version}; | 
 |   }; | 
 |  | 
 |   # Lift maximum template cache | 
 |   $self->renderer->cache->max_keys(200); | 
 |  | 
 |   # Add additional plugin path | 
 |   push(@{$self->plugins->namespaces}, __PACKAGE__ . '::Plugin'); | 
 |  | 
 |   # Add additional commands | 
 |   push(@{$self->commands->namespaces}, __PACKAGE__ . '::Command'); | 
 |  | 
 |   # Set secrets for signed cookies | 
 |   my $secret_file = $self->home->rel_file('kalamar.secret.json'); | 
 |  | 
 |   # Support old secrets file | 
 |   # This is deprecated 2021-03-05 | 
 |   if (-e (my $old_secret = $self->home->child('kalamar.secret'))) { | 
 |  | 
 |     # Load file and split lines for multiple secrets | 
 |     my $secrets = [b($old_secret->slurp)->split("\n")]; | 
 |  | 
 |     $self->secrets($secrets); | 
 |  | 
 |     for (@$secrets) { | 
 |       if (length($secrets) > 22) { | 
 |         $self->log->warn( | 
 |           'Unable to automatically switch to Autosecrets, as secret is too long (> 22 chars)' | 
 |         ); | 
 |         goto CONF; | 
 |       }; | 
 |     } | 
 |  | 
 |     eval { | 
 |       $secret_file->spew(encode_json(@$secrets)); | 
 |       $secret_file->chmod(0600); | 
 |       if (-w $secret_file) { | 
 |         $self->log->warn( | 
 |           "Please delete $old_secret file " . | 
 |             "- $secret_file was created instead" | 
 |           ); | 
 |       } | 
 |     }; | 
 |     if ($@) { | 
 |       $self->log->error("Please make $secret_file accessible"); | 
 |     }; | 
 |   } | 
 |  | 
 |   # File not found ... | 
 |   # Kalamar needs secrets in a file to be easily deployable | 
 |   # and publishable at the same time. | 
 |   else { | 
 |     $self->plugin(AutoSecrets => { | 
 |       path => $secret_file | 
 |     }); | 
 |   }; | 
 |  | 
 |  CONF: | 
 |  | 
 |   # Configuration framework | 
 |   $self->plugin('Config'); | 
 |  | 
 |   $self->log->info('Mode is ' . $self->mode); | 
 |  | 
 |   # Get configuration | 
 |   my $conf = $self->config('Kalamar'); | 
 |   unless ($conf) { | 
 |     $self->config(Kalamar => {}); | 
 |     $conf = $self->config('Kalamar'); | 
 |   }; | 
 |  | 
 |   # Set log file. All precedng logs where send to stderr | 
 |   if ($conf->{log_file}) { | 
 |     Mojo::File->new(Mojo::File->new($conf->{log_file})->dirname)->make_path; | 
 |     $self->log(Mojo::Log->new( | 
 |       path => $conf->{log_file}, | 
 |     )); | 
 |   }; | 
 |  | 
 |  | 
 |   # Check for API endpoint and set the endpoint accordingly | 
 |   if ($conf->{api}) { | 
 |  | 
 |     # The api endpoint should be defined as a separated path | 
 |     # and version string | 
 |     $self->log->warn( | 
 |       'Kalamar.api is no longer supported in configurations '. | 
 |         'in favor of Kalamar.api_path' | 
 |       ); | 
 |   }; | 
 |  | 
 |   $self->sessions->cookie_name('kalamar'); | 
 |  | 
 |   # Require HTTPS | 
 |   if ($conf->{https_only}) { | 
 |  | 
 |     # ... for cookie transport | 
 |     $self->sessions->secure(1); | 
 |  | 
 |     # Temporary for session riding | 
 |     $self->sessions->samesite('None'); | 
 |  | 
 |     # For all pages | 
 |     $self->hook( | 
 |       before_dispatch => sub { | 
 |         shift->res->headers->header('Strict-Transport-Security' => 'max-age=3600; includeSubDomains'); | 
 |       } | 
 |     ); | 
 |   }; | 
 |  | 
 |   # Run the app from a subdirectory | 
 |   if ($conf->{proxy_prefix}) { | 
 |  | 
 |     for ($self->sessions) { | 
 |       $_->cookie_path($conf->{proxy_prefix}); | 
 |       $_->cookie_name('kalamar-' . slugify($conf->{proxy_prefix})); | 
 |     }; | 
 |  | 
 |     # Set prefix in stash | 
 |     $self->defaults(prefix => $conf->{proxy_prefix}); | 
 |  | 
 |     # Create base path | 
 |     $self->hook( | 
 |       before_dispatch => sub { | 
 |         shift->req->url->base->path($conf->{proxy_prefix} . '/'); | 
 |       }); | 
 |   }; | 
 |  | 
 |   $self->hook( | 
 |     before_dispatch => sub { | 
 |       my $h = shift->res->headers; | 
 |       $h->header('X-Content-Type-Options' => 'nosniff'); | 
 |       $h->header('X-XSS-Protection' => '1; mode=block'); | 
 |       $h->header( | 
 |         'Access-Control-Allow-Methods' => | 
 |           $h->header('Access-Control-Allow-Methods') // 'GET, POST, OPTIONS' | 
 |         ); | 
 |     } | 
 |   ); | 
 |  | 
 |   $conf->{proxy_host} //= 1; | 
 |  | 
 |   # Take proxy host | 
 |   if ($conf->{proxy_host}) { | 
 |     $self->hook( | 
 |       before_dispatch => sub { | 
 |         my $c = shift; | 
 |         my $h = $c->req->headers; | 
 |         if (my $host = $h->header('X-Forwarded-Host')) { | 
 |  | 
 |           my $proto = $h->header('X-Forwarded-Proto') // ($conf->{https_only} ? 'https' : undef); | 
 |  | 
 |           foreach ($c->req->url->base) { | 
 |             $_->host($host); | 
 |             $_->scheme($proto); | 
 |             $_->port(undef); | 
 |           }; | 
 |         }; | 
 |       } | 
 |     ); | 
 |   }; | 
 |  | 
 |   # API is not yet set - define the default Kustvakt api endpoint | 
 |   $conf->{api_path} //= $ENV{KALAMAR_API} || 'https://korap.ids-mannheim.de/api/'; | 
 |   $conf->{api_version} //= $API_VERSION; | 
 |  | 
 |   # Add development path | 
 |   if ($self->mode eq 'development') { | 
 |     push @{$self->static->paths}, 'dev'; | 
 |   }; | 
 |  | 
 |   # Set proxy timeouts | 
 |   if ($conf->{proxy_inactivity_timeout}) { | 
 |     $self->ua->inactivity_timeout($conf->{proxy_inactivity_timeout}); | 
 |   }; | 
 |   if ($conf->{proxy_connect_timeout}) { | 
 |     $self->ua->connect_timeout($conf->{proxy_connect_timeout}); | 
 |   }; | 
 |  | 
 |   # Client notifications | 
 |   $self->plugin(Notifications => { | 
 |     'Kalamar::Plugin::Notifications' => 1, | 
 |     JSON => 1, | 
 |     HTML => 1 | 
 |   }); | 
 |  | 
 |   # Establish content security policy | 
 |   # This needs to be defined prior to Kalamar::Plugin::Piwik! | 
 |   $self->plugin(CSP => { | 
 |     'default-src' => 'self', | 
 |     'style-src'   => ['self','unsafe-inline'], | 
 |     # Hash for korap-overview.svg script | 
 |     'script-src'  => ['self','sha256-VGXK99kFz+zmAQ0kxgleFrBWZgybFAPOl3GQtS7FQkI='], | 
 |     'connect-src' => 'self', | 
 |     'frame-src'   => '*', | 
 |     'frame-ancestors' => 'self', | 
 |     'media-src'   => 'none', | 
 |     'object-src'  => 'self', | 
 |     'font-src'    => 'self', | 
 |     'img-src'     => ['self', 'data:'], | 
 |     -with_nonce => 1 | 
 |   }); | 
 |  | 
 |   # Localization framework | 
 |   $self->plugin(Localize => { | 
 |     dict => { | 
 |       Q => { | 
 |         _ => sub { shift->config('Kalamar')->{'examplecorpus'} }, | 
 |       } | 
 |     }, | 
 |     resources => [ | 
 |       'kalamar.dict', | 
 |       'kalamar.queries.dict', | 
 |       'loc/kalamar.ro.dict', | 
 |       'loc/kalamar.hu.dict' | 
 |     ] | 
 |   }); | 
 |  | 
 |   # Pagination widget | 
 |   $self->plugin('TagHelpers::Pagination' => { | 
 |     prev      => '<span><span><</span></span>', | 
 |     next      => '<span><span>></span></span>', | 
 |     ellipsis  => '<a class="ellipsis inactive"><span><span>...</span></span></a>', | 
 |     separator => '', | 
 |     current   => '<span>{current}</span>', | 
 |     page      => '<span>{page}</span>' | 
 |   }); | 
 |  | 
 |   # Obfuscate email addresses | 
 |   $self->plugin('TagHelpers::MailToChiffre' => { | 
 |     method_name => 'PArok', | 
 |     pattern_rotate => 673, | 
 |     no_inline => 1 | 
 |   }); | 
 |  | 
 |   # Load plugins | 
 |   foreach ( | 
 |     'KalamarHelpers',            # Specific Helpers for Kalamar | 
 |     'KalamarPages',              # Page Helpers for Kalamar | 
 |     'KalamarErrors',             # Specific Errors for Kalamar | 
 |     'KalamarUser',               # Specific Helpers for Kalamar Users | 
 |     'ClientIP',                  # Get client IP from X-Forwarded-For | 
 |     'ClosedRedirect',            # Redirect with OpenRedirect protection | 
 |     'TagHelpers::ContentBlock',  # Flexible content blocks | 
 |   ) { | 
 |     $self->plugin($_); | 
 |   }; | 
 |  | 
 |   my $serializer = 'JSON'; | 
 |  | 
 |   if (my $chi = $self->config('CHI')) { | 
 |     if ($chi->{default}) { | 
 |       $chi->{default}->{serializer} = $serializer; | 
 |     }; | 
 |     if ($chi->{user}) { | 
 |       $chi->{user}->{serializer} = $serializer; | 
 |     }; | 
 |   }; | 
 |  | 
 |   # Global caching mechanism | 
 |   $self->plugin('CHI' => { | 
 |     default => { | 
 |       driver => 'Memory', | 
 |       global => 1, | 
 |       serializer => $serializer | 
 |     }, | 
 |     user => { | 
 |       driver => 'Memory', | 
 |       global => 1, | 
 |       serializer => $serializer | 
 |     } | 
 |   }); | 
 |  | 
 |   # Configure mail exception | 
 |   if ($self->config('MailException')) { | 
 |     $self->plugin('MailException' => $self->config('MailException')); | 
 |   }; | 
 |  | 
 |   # Load plugins defined in environment variables | 
 |   if ($ENV{'KALAMAR_PLUGINS'}) { | 
 |     $conf->{'plugins'} //= []; | 
 |     push @{$conf->{'plugins'}}, split(/\s*,\s*/, $ENV{'KALAMAR_PLUGINS'} // ''); | 
 |   }; | 
 |  | 
 |   # Load further plugins, | 
 |   # that can override core functions, | 
 |   # therefore order may be of importance | 
 |   if (exists $conf->{'plugins'}) { | 
 |     foreach (uniq @{$conf->{'plugins'}}) { | 
 |       $self->plugin('Kalamar::Plugin::' . $_); | 
 |     }; | 
 |   }; | 
 |  | 
 |   # Set defaults per config | 
 |   $self->defaults( | 
 |     items_per_page => 25, | 
 |     context => '40-t,40-t', # Before: 'base/s:p'/'paragraph' | 
 |     alignment => 'left' | 
 |   ); | 
 |  | 
 |   if (exists $conf->{defaults}) { | 
 |     my $def = $conf->{defaults}; | 
 |     foreach (qw!items_per_page context alignment!) { | 
 |       $self->defaults($_ => $def->{$_}) if $def->{$_}; | 
 |     }; | 
 |   }; | 
 |  | 
 |   # Configure documentation navigation | 
 |   my $doc_navi = Mojo::File->new($self->home->child('templates','doc','navigation.json'))->slurp; | 
 |   $doc_navi = $doc_navi ? decode_json($doc_navi) : []; | 
 |  | 
 |   # TODO: | 
 |   #   Use navi->add() | 
 |   if ($conf->{navi_ext}) { | 
 |     push @$doc_navi, @{$conf->{navi_ext}}; | 
 |   }; | 
 |  | 
 |   # TODO: | 
 |   #   Remove navi entry | 
 |   $self->config(doc_navi => $doc_navi); | 
 |  | 
 |   $self->navi->set(doc => $doc_navi); | 
 |  | 
 |   $self->log->info('API expected at ' . $self->korap->api); | 
 |  | 
 |   # Establish routes with authentification | 
 |   my $r = $self->routes; | 
 |  | 
 |   # Set footer value | 
 |   $self->content_block(footer => { | 
 |     inline => '<%= embedded_link_to "doc", "V ' . $Kalamar::VERSION . '", "korap", "kalamar" %>', | 
 |     position => 100 | 
 |   }); | 
 |  | 
 |   # Add nonce script | 
 |   $self->content_block(nonce_js => { | 
 |     inline => <<'NONCE_JS' | 
 |       // Remove the no-js class from the body | 
 |       document.body.classList.remove('no-js'); | 
 | NONCE_JS | 
 |   }); | 
 |  | 
 |   # Base query route | 
 |   $r->get('/')->to('search#query')->name('index'); | 
 |  | 
 |   # Documentation routes | 
 |   $r->get('/doc')->to('documentation#page', page => 'ql')->name('doc_start'); | 
 |   $r->get('/doc/:scope/:page')->to('documentation#page', scope => undef)->name('doc'); | 
 |  | 
 |   # Settings routes | 
 |   if ($self->navi->exists('settings')) { | 
 |     $r->get('/settings')->to( | 
 |       cb => sub { | 
 |         my $c = shift; | 
 |         $c->res->headers->header('X-Robots' => 'noindex'); | 
 |         return $c->render('settings'); | 
 |       } | 
 |     )->name('settings_start'); | 
 |     $r->get('/settings/:scope/:page')->to( | 
 |       scope => undef, | 
 |       page => undef | 
 |     )->name('settings'); | 
 |   }; | 
 |  | 
 |   # Contact route | 
 |   $r->get('/contact')->to('documentation#contact'); | 
 |   $r->get('/contact')->mail_to_chiffre('documentation#contact'); | 
 |  | 
 |   # API proxy route | 
 |   $r->any('/api/v#apiv' => [apiv => ['1.0']])->name('proxy')->to('Proxy#pass'); | 
 |   $r->any('/api/v#apiv/*api_path' => [apiv => ['1.0']])->to('Proxy#pass'); | 
 |  | 
 |   # Match route | 
 |   # Corpus route | 
 |   my $corpus = $r->get('/corpus')->to('search#corpus_info')->name('corpus'); | 
 |   my $doc    = $r->any('/corpus/#corpus_id/#doc_id'); | 
 |   my $text   = $doc->get('/#text_id')->to('search#text_info')->name('text'); | 
 |   my $match  = $doc->get('/#text_id/#match_id')->to('search#match_info')->name('match'); | 
 | }; | 
 |  | 
 |  | 
 | 1; | 
 |  | 
 |  | 
 | __END__ | 
 |  | 
 | =pod | 
 |  | 
 | =encoding utf8 | 
 |  | 
 | =head1 NAME | 
 |  | 
 | Kalamar | 
 |  | 
 |  | 
 | =head1 DESCRIPTION | 
 |  | 
 | L<Kalamar> is a L<Mojolicious|http://mojolicio.us/> based user interface | 
 | frontend for the L<KorAP Corpus Analysis Platform|https://korap.ids-mannheim.de/>. | 
 |  | 
 | B<See the README for further information!> | 
 |  | 
 | =head2 COPYRIGHT AND LICENSE | 
 |  | 
 | Copyright (C) 2015-2024, L<IDS Mannheim|https://www.ids-mannheim.de/> | 
 | Author: L<Nils Diewald|https://www.nils-diewald.de/> | 
 |  | 
 | Kalamar is developed as part of the L<KorAP|http://korap.ids-mannheim.de/> | 
 | Corpus Analysis Platform at the | 
 | L<Leibniz Institute for the German Language (IDS)|https://www.ids-mannheim.de/>, | 
 | member of the | 
 | L<Leibniz-Gemeinschaft|http://www.leibniz-gemeinschaft.de> | 
 | and supported by the L<KobRA|http://www.kobra.tu-dortmund.de> project, | 
 | funded by the | 
 | L<Federal Ministry of Education and Research (BMBF)|http://www.bmbf.de/en/>. | 
 |  | 
 | Kalamar is free software published under the | 
 | L<BSD-2 License|https://opensource.org/licenses/BSD-2-Clause>. | 
 |  | 
 | =cut |