meon-Web/lib/meon/Web/Controller/Root.pm
package meon::Web::Controller::Root;
use Moose;
use namespace::autoclean;
use 5.010;
use Path::Class 'file', 'dir';
use meon::Web::SPc;
use meon::Web::Config;
use meon::Web::Util;
use meon::Web::env;
use XML::LibXML 1.70;
use URI::Escape 'uri_escape';
use IO::Any;
use Class::Load 'load_class';
use File::MimeInfo 'mimetype';
use Scalar::Util 'blessed';
use DateTime::Format::HTTP;
use Imager;
use URI::Escape 'uri_escape';
use List::MoreUtils 'none';
use WWW::Mechanize;
use JSON::XS 'decode_json';
use Data::asXML 0.07;
use meon::Web::Form::Login;
use meon::Web::Form::Delete;
use meon::Web::Member;
use meon::Web::TimelineEntry;
BEGIN { extends 'Catalyst::Controller' }
__PACKAGE__->config(namespace => '');
sub auto : Private {
my ( $self, $c ) = @_;
meon::Web::env->clear;
meon::Web::env->stash($c->stash);
meon::Web::env->session($c->session);
my $uri = $c->req->uri;
my $hostname = $uri->host;
meon::Web::env->hostname($hostname);
my $hostname_dir_name = meon::Web::env->hostname_dir_name;
$c->detach('/status_not_found', ['no such domain '.$hostname.' configured'])
unless $hostname_dir_name;
my $hostname_dir = $c->stash->{hostname_dir} = meon::Web::env->hostname_dir;
my $template_file = file($hostname_dir, 'template', 'xsl', 'default.xsl')->stringify;
$c->stash->{template} = XML::LibXML->load_xml(location => $template_file);
$c->default_auth_store->folder(meon::Web::env->profiles_dir);
meon::Web::env->user($c->user);
# set cookie domain
my $cookie_domain = $hostname;
my $config_cookie_domain = meon::Web::env->hostname_config->{'main'}{'cookie-domain'};
if ($config_cookie_domain && (substr($hostname,0-length($config_cookie_domain)) eq $config_cookie_domain)) {
$cookie_domain = $config_cookie_domain;
}
$c->_session_plugin_config->{cookie_domain} = $cookie_domain;
$c->change_session_expires( 30*24*60*60 )
if $c->session->{remember_login};
return 1;
}
sub static : Path('/static') {
my ($self, $c) = @_;
my $static_file = file(@{$c->static_include_path}, $c->req->path);
$c->detach('/status_not_found', [($c->debug ? $static_file : '')])
unless -e $static_file;
my $mime_type = mimetype($static_file->stringify);
$c->res->content_type($mime_type);
$c->res->body(IO::Any->read([$static_file]));
}
sub default :Path {
my ( $self, $c ) = @_;
$c->forward('resolve_xml', []);
}
sub resolve_xml : Private {
my ( $self, $c ) = @_;
my $hostname_dir = $c->stash->{hostname_dir};
my $include_dir = meon::Web::env->include_dir;
my $path =
delete($c->session->{post_redirect_path})
|| $c->stash->{path}
|| $c->req->uri;
$path = URI->new($path)
unless blessed($path);
# replace …/index by …/ in url
if ($path =~ m{/index$}) {
my $new_uri = $c->req->uri;
$new_uri->path(substr($path->path,0,-5));
$c->res->redirect($new_uri->absolute);
$c->detach;
}
meon::Web::env->current_path(file($path->path));
my $xml_file = file(meon::Web::env->content_dir, $path->path_segments);
$xml_file .= 'index' if ($xml_file =~ m{/$});
$xml_file .= '.xml';
# add trailing slash and redirect when uri points to a folder
if ((! -f $xml_file) && (-d substr($xml_file,0,-4))) {
my $new_uri = $c->req->uri;
$new_uri->path($path->path.'/');
$c->res->redirect($new_uri->absolute);
$c->detach;
}
if ((! -f $xml_file) && (-f substr($xml_file,0,-4))) {
my $static_file = file(substr($xml_file,0,-4));
my $mtime = $static_file->stat->mtime;
if (!$c->req->param('t')) {
$c->res->redirect($c->req->uri_with({t => $mtime})->absolute);
$c->detach;
}
my $max_age = 365*24*60*60;
$c->res->header('Cache-Control' => 'max-age='.$max_age.', private');
$c->res->header(
'Expires' => DateTime::Format::HTTP->format_datetime(
DateTime->now->add(seconds => $max_age)
)
);
$c->res->header(
'Last-Modified' => DateTime::Format::HTTP->format_datetime(
DateTime->from_epoch(epoch => $mtime)
)
);
my $mime_type = mimetype($static_file->basename);
$c->res->content_type($mime_type);
$c->res->body($static_file->open('r'));
$c->detach;
}
meon::Web::env->xml_file($xml_file);
unless (-e $xml_file) {
my $not_found_handler = meon::Web::env->hostname_config->{'main'}{'not-found-handler'};
if ($not_found_handler) {
load_class($not_found_handler);
my $content_dir = meon::Web::env->content_dir;
my $relative_path = $xml_file;
$relative_path =~ s/^$content_dir//;
die 'forbidden' if $path eq $relative_path;
$not_found_handler->check($content_dir, $relative_path);
}
$c->detach('/status_not_found', [($c->debug ? $path.' '.$xml_file : $path)])
if (!eval { meon::Web::env->xml });
}
$xml_file = file($xml_file);
$c->stash->{xml_file} = $xml_file;
my $dom = meon::Web::env->xml;
my $xpc = meon::Web::Util->xpc;
$c->model('ResponseXML')->dom($dom);
$c->model('ResponseXML')->push_new_element('current-path')->appendText($c->req->uri->path);
$c->model('ResponseXML')->push_new_element('current-uri')->appendText($c->req->uri->absolute);
$c->model('ResponseXML')->push_new_element('static-mtime')->appendText(meon::Web::env->static_dir_mtime);
$c->model('ResponseXML')->push_new_element('run-env')->appendText(Run::Env->current);
# user
if ($c->user_exists) {
my $user_el = $c->model('ResponseXML')->create_element('user');
my $user_el_username = $c->model('ResponseXML')->create_element('username');
$user_el_username->appendText($c->user->username);
$user_el->appendChild($user_el_username);
my $member = $c->member;
my $full_name_el = $c->model('ResponseXML')->create_element('full-name');
$full_name_el->appendText($member->get_member_meta('full-name'));
$user_el->appendChild($full_name_el);
my $profile_el = $c->model('ResponseXML')->create_element('profile');
if (my $member_profile = $member->get_member_meta_element('member-profile')) {
$profile_el->appendChild($member_profile);
$user_el->appendChild($profile_el);
}
$c->model('ResponseXML')->append_xml($user_el);
my @user_roles = $c->user->roles;
if (my $backend_user_data = $c->session->{backend_user_data}) {
my $bu_data_el = $c->model('ResponseXML')->create_element('backend-user-data');
$c->model('ResponseXML')->append_xml($bu_data_el);
my $dxml = Data::asXML->new(
pretty => Run::Env->dev,
namespace => 1,
);
$bu_data_el->appendChild(
$dxml->encode($backend_user_data)
);
if ($backend_user_data->{web_roles}) {
my @backed_roles = map {
'backend-'.$_
} eval {@{$backend_user_data->{web_roles}}};
push(@user_roles, @backed_roles);
}
}
my $roles_el = $c->model('ResponseXML')->create_element('roles');
foreach my $role (@user_roles) {
$roles_el->appendChild(
$c->model('ResponseXML')->create_element($role)
);
}
$user_el->appendChild($roles_el);
my @access_roles = map { $_->textContent } $xpc->findnodes('/w:page/w:meta/w:access/w:role',$dom);
foreach my $role (@access_roles) {
$c->detach('/status_forbidden', []) if (none {$_ eq $role} @user_roles);
}
}
else {
if ($xpc->findnodes('/w:page/w:meta/w:members-only',$dom)) {
$c->detach('/login', []);
}
}
# redirect
my ($redirect) = $xpc->findnodes('/w:page/w:meta/w:redirect', $dom);
if ($redirect) {
$redirect = $redirect->textContent;
my $redirect_uri = $c->traverse_uri($redirect);
$redirect_uri = $redirect_uri->absolute
if $redirect_uri->can('absolute');
$c->res->redirect($redirect_uri);
$c->detach;
}
# includes
my (@include_elements) =
$xpc->findnodes('/w:page//w:include',$dom);
foreach my $include_el (@include_elements) {
my $include_path = $include_el->getAttribute('path');
unless ($include_path) {
$include_el->appendText('path attribute missing');
next;
}
my $include_rel = dir(meon::Web::Util->path_fixup($include_path));
my $file = file($include_dir, $include_rel)->absolute;
next unless -f $file;
$file = $file->resolve;
$c->detach('/status_forbidden', [])
unless $include_dir->contains($file);
my $include_xml = eval { XML::LibXML->load_xml(location => $file) };
my (@include_filter_elements) =
$xpc->findnodes('//w:apply-filter',$include_xml);
foreach my $include_filter_el (@include_filter_elements) {
my $filter_ident = $include_filter_el->getAttribute('ident');
die 'no filter name specified'
unless $filter_ident;
my $filter_class = 'meon::Web::Filter::'.$filter_ident;
load_class($filter_class);
my $status = $filter_class->new(
dom => $include_xml,
include_node => $include_el,
user => $c->user,
)->apply;
if (my $err_msg = $status->{error}) {
if (($status->{status} // 0) == 404) {
$c->detach('/status_not_found', [$err_msg]);
}
else {
die $err_msg;
}
}
$include_filter_el->parentNode->removeChild($include_filter_el);
}
if ($include_xml) {
$include_el->replaceNode($include_xml->documentElement());
}
else {
die 'failed to load include '.$@;
}
}
# forms
if (my ($form_el) = $xpc->findnodes('/w:page/w:meta/w:form',$dom)) {
my $skip_form = 0;
if ($xpc->findnodes('w:owner-only',$form_el)) {
$skip_form = 1;
if ($c->user_exists) {
my $member = $c->member;
my $member_folder = $member->dir;
$skip_form = 0
if $member_folder->contains($xml_file);
}
}
unless ($skip_form) {
my $back_link = delete $c->req->params->{_back_link};
if (defined($back_link)) {
$c->model('ResponseXML')->push_new_element('back-link')->appendText($back_link);
$c->stash->{back_link} = $back_link;
}
my ($form_class) = 'meon::Web::Form::'.$xpc->findnodes('/w:page/w:meta/w:form/w:process', $dom);
load_class($form_class);
my $form = $form_class->new(c => $c);
my $params = $c->req->body_parameters;
foreach my $field ($form->fields) {
next if $field->type ne 'Upload';
my $field_name = $field->name;
$params->{$field_name} = $c->req->upload($field_name)
if $c->req->params->{$field_name};
}
$form->process(params=>$params);
$form->submitted
if $form->is_valid && $form->can('submitted') && ($c->req->method eq 'POST');
$c->model('ResponseXML')->add_xhtml_form(
$form->render
);
if (my $form_input_errors = delete $c->session->{form_input_errors}) {
foreach my $input_name (keys %$form_input_errors) {
my ($input) = $xpc->findnodes(
'.//x:input[@name="'.$input_name.'"]'
.'|.//x:select[@name="'.$input_name.'"]'
.'|.//x:textarea[@name="'.$input_name.'"]'
,$c->model('ResponseXML')->dom
);
next unless $input;
$input->setAttribute('class' => $input->hasAttribute('class') ? $input->getAttribute('class').' error' : 'error');
my $span = $input->parentNode->addNewChild($input->namespaceURI, 'span');
$span->setAttribute('class' => 'help-inline');
$span->appendText($form_input_errors->{$input_name});
my $error_class = 'error';
my $div = $input->parentNode;
if ($div->getAttribute('class') // '' eq 'form-group') {
$error_class = 'has-error';
}
else {
$div->parentNode;
}
$div->setAttribute(
'class'
=> ($div->hasAttribute('class') ? $div->getAttribute('class').' '.$error_class : $error_class)
);
}
}
}
}
# folder listing
my (@folder_elements) =
$xpc->findnodes('/w:page/w:content//w:dir-listing',$dom);
foreach my $folder_el (@folder_elements) {
my $folder_name = $folder_el->getAttribute('path');
my $reverse = $folder_el->getAttribute('reverse');
unless ($folder_name) {
$folder_el->appendText('path attribute missing');
next;
}
my $folder_rel = dir(meon::Web::Util->path_fixup($folder_name));
my $folder = dir($xml_file->dir, $folder_rel)->absolute;
next unless -d $folder;
$folder = $folder->resolve;
$c->detach('/status_forbidden', [])
unless $hostname_dir->contains($folder);
my @folders = sort(grep { $_->is_dir } $folder->children(no_hidden => 1));
@folders = reverse @folders if $reverse;
my @files = sort(grep { not $_->is_dir } $folder->children(no_hidden => 1));
@files = reverse @files if $reverse;
foreach my $file (@folders) {
$file = $file->basename;
my $file_el = $c->model('ResponseXML')->create_element('folder');
$file_el->setAttribute('href' => join('/', map { uri_escape($_) } $folder_rel->dir_list, $file));
$file_el->appendText($file);
$folder_el->appendChild($file_el);
}
foreach my $file (@files) {
$file = $file->basename;
my $file_el = $c->model('ResponseXML')->create_element('file');
$file_el->setAttribute('href' => join('/', map { uri_escape($_) } $folder_rel->dir_list, $file));
$file_el->appendText($file);
$folder_el->appendChild($file_el);
}
}
# gallery listing
my (@galleries) = $xpc->findnodes('/w:page/w:content//w:gallery',$dom);
foreach my $gallery (@galleries) {
my $gallery_path = $gallery->getAttribute('href');
my $max_width = $gallery->getAttribute('thumb-width');
my $max_height = $gallery->getAttribute('thumb-height');
my $folder_rel = dir(meon::Web::Util->path_fixup($gallery_path));
my $folder = dir($xml_file->dir, $folder_rel)->absolute;
die 'no pictures in '.$folder unless -d $folder;
$folder = $folder->resolve;
$c->detach('/status_forbidden', [])
unless $hostname_dir->contains($folder);
my @files = sort(grep { not $_->is_dir } $folder->children(no_hidden => 1));
foreach my $file (@files) {
$file = $file->basename;
next if $file =~ m/\.xml$/;
my $thumb_file = file(map { uri_escape($_) } $folder_rel->dir_list, 'thumb', $file);
my $img_file = file(map { uri_escape($_) } $folder_rel->dir_list, $file);
my $file_el = $c->model('ResponseXML')->create_element('img');
$file_el->setAttribute('src' => $img_file);
$file_el->setAttribute('src-thumb' => $thumb_file);
$file_el->setAttribute('title' => $file);
$file_el->setAttribute('alt' => $file);
$gallery->appendChild($file_el);
# create thumbnail image
$thumb_file = file($xml_file->dir, $thumb_file);
unless (-e $thumb_file) {
$thumb_file->dir->mkpath
unless -e $thumb_file->dir;
my $img = Imager->new(file => file($xml_file->dir, $img_file))
or die Imager->errstr();
if ($img->getwidth > $max_width) {
$img = $img->scale(xpixels => $max_width)
|| die 'failed to scale image - '.$img->errstr;
}
if ($img->getheight > $max_height) {
$img = $img->scale(ypixels => $max_height)
|| die 'failed to scale image - '.$img->errstr;
}
$img->write(file => $thumb_file->stringify) || die 'failed to save image - '.$img->errstr;
}
}
}
# generate timeline
my ($timeline_el) = $xpc->findnodes('/w:page/w:content//w:timeline', $dom);
if ($timeline_el) {
my $timeline_class = $timeline_el->getAttribute('class') // 'folder';
my @entries_files;
foreach my $href_entry ($xpc->findnodes('w:timeline-entry[@href]', $timeline_el)) {
my $href = $href_entry->getAttribute('href');
$timeline_el->removeChild($href_entry);
my $path = file(meon::Web::Util->full_path_fixup($href).'.xml');
push(@entries_files,$path)
if -e $path;
}
@entries_files = $xml_file->dir->children(no_hidden => 1)
if $timeline_class eq 'folder';
my @entries =
sort { $b->created <=> $a->created }
grep { eval { $_->element } }
map { meon::Web::TimelineEntry->new(file => $_) }
grep { $_->basename ne $xml_file->basename }
grep { !$_->is_dir }
@entries_files
;
foreach my $entry (@entries) {
my $entry_el = $entry->element;
my $intro = $entry->intro;
my $href = $entry->file->resolve;
return unless $href;
$href = substr($href,0,-4);
$href = substr($href,length($c->stash->{hostname_dir}.'/content'));
$entry_el->setAttribute('href' => $href);
if (defined($intro)) {
my $intro_snipped_el = $c->model('ResponseXML')->create_element('intro-snipped');
$entry_el->appendChild($intro_snipped_el);
$intro_snipped_el->appendText(length($intro) > 78 ? substr($intro,0,78).'…' : $intro);
}
$timeline_el->appendChild($entry_el);
}
if (my $older = $self->_older_entries($c)) {
my $older_el = $c->model('ResponseXML')->create_element('older');
$timeline_el->appendChild($older_el);
$older_el->setAttribute('href' => $older);
}
if (my $newer = $self->_newer_entries($c)) {
my $newer_el = $c->model('ResponseXML')->create_element('newer');
$timeline_el->appendChild($newer_el);
$newer_el->setAttribute('href' => $newer);
}
}
# generate members list
my ($members_list_el) = $xpc->findnodes('/w:page/w:content//w:members-list', $dom);
if ($members_list_el) {
my %members_by_section;
my $active_only = $members_list_el->getAttribute('active-only');
foreach my $member (sort { $a->section cmp $b->section } meon::Web::env->all_members) {
next if ($active_only && !$member->is_active);
$member->shred_password;
my $sec = $member->section;
$members_by_section{$sec} //= [];
push(@{$members_by_section{$sec}}, $member);
}
foreach my $sec (sort keys %members_by_section) {
my $sec_el = $c->model('ResponseXML')->create_element('section');
$sec_el->setAttribute('name' => $sec);
$members_list_el->appendChild($sec_el);
foreach my $member (@{$members_by_section{$sec}}) {
my $meta = $member->member_meta;
$sec_el->appendChild($meta);
my $username = $member->username;
my $username_el = $c->model('ResponseXML')->create_element('username');
$username_el->appendText($username);
$meta->appendChild($username_el);
my $status = $member->user->status;
my $status_el = $c->model('ResponseXML')->create_element('status');
$status_el->appendText($status);
$meta->appendChild($status_el);
}
}
}
# generate exists
my (@exists) = (
$xpc->findnodes('//w:exists', $dom),
$xpc->findnodes('//w:exists', $c->stash->{template}),
);
foreach my $exist_el (@exists) {
my $href = $exist_el->getAttribute('href');
my $path = meon::Web::Util->full_path_fixup($href);
$exist_el->appendText(-e $path ? 1 : 0);
}
# handle different templates
my ($template_node) = $xpc->findnodes('/w:page/w:meta/w:template', $dom);
if ($template_node) {
my $template_name = $template_node->textContent;
my $template_file = file($hostname_dir, 'template', 'xsl', $template_name.'.xsl')->stringify;
$c->detach('/status_not_found', ['no such template '.$template_name])
unless -f $template_file;
$c->stash->{template} = XML::LibXML->load_xml(location => $template_file);
}
}
sub _older_entries {
my ( $self, $c ) = @_;
my $dir = $c->stash->{xml_file}->dir;
my $cur_dir = $dir->basename;
$dir = $dir->parent;
while ($cur_dir =~ m/^\d+$/) {
my @min_folders =
sort
grep { $_ < $cur_dir }
grep { m/^\d+$/ }
map { $_->basename }
grep { $_->is_dir }
$dir->children(no_hidden => 1)
;
if (@min_folders) {
# find the last folder of this folder
while (@min_folders) {
$dir = $dir->subdir(pop(@min_folders));
@min_folders =
sort
grep { m/^\d+$/ }
map { $_->basename }
grep { $_->is_dir }
$dir->children(no_hidden => 1)
;
}
return '/'.$dir->relative(meon::Web::env->content_dir).'/';
}
$cur_dir = $dir->basename;
$dir = $dir->parent;
}
}
sub _newer_entries {
my ( $self, $c ) = @_;
my $dir = $c->stash->{xml_file}->dir;
my $cur_dir = $dir->basename;
$dir = $dir->parent;
while ($cur_dir =~ m/^\d+$/) {
my @max_folders =
sort
grep { $_ > $cur_dir }
grep { m/^\d+$/ }
map { $_->basename }
grep { $_->is_dir }
$dir->children(no_hidden => 1)
;
if (@max_folders) {
# find the first folder of this folder
while (@max_folders) {
$dir = $dir->subdir(shift(@max_folders));
@max_folders =
sort
grep { m/^\d+$/ }
map { $_->basename }
grep { $_->is_dir }
$dir->children(no_hidden => 1)
;
}
return '/'.$dir->relative(meon::Web::env->content_dir).'/';
}
$cur_dir = $dir->basename;
$dir = $dir->parent;
}
}
sub status_forbidden : Private {
my ( $self, $c, $message ) = @_;
$c->res->status(403);
my $xml_file = file(meon::Web::env->content_dir, '403.xml');
if (-e $xml_file) {
$c->session->{post_redirect_path} = '/403';
$self->resolve_xml($c);
$c->model('ResponseXML')->push_new_element('error-message')->appendText($message)
if $message;
}
else {
$message = '403 - Forbidden: '.$c->req->uri."\n".($message // '');
$c->res->content_type('text/plain');
$c->res->body($message);
}
}
sub status_not_found : Private {
my ( $self, $c, $message ) = @_;
$c->res->status(404);
my $xml_file = file(meon::Web::env->content_dir, '404.xml');
if (-e $xml_file) {
$c->session->{post_redirect_path} = '/404';
$self->resolve_xml($c);
$c->model('ResponseXML')->push_new_element('error-message')->appendText($message)
if $message;
}
else {
$message = '404 - Page not found: '.$c->req->uri."\n".($message // '');
$c->res->content_type('text/plain');
$c->res->body($message);
}
}
sub logout : Local {
my ( $self, $c ) = @_;
my $username = eval { $c->user->username };
$c->delete_session;
$c->log->info('logout user '.$username)
if $username;
return $c->res->redirect($c->uri_for('/'));
}
sub login : Local {
my ( $self, $c ) = @_;
return $c->res->redirect($c->uri_for('/'))
if $c->user_exists;
my $members_folder = $c->default_auth_store->folder;
my $ext_auth_username = $c->session->{external_auth_username};
if (
meon::Web::env->hostname_config->{'auth'}{'external'}
&& $ext_auth_username
) {
my $member = meon::Web::Member->new(
members_folder => $members_folder,
username => $ext_auth_username,
);
if ($member->exists) {
$c->set_authenticated($c->find_user({ username => $ext_auth_username }));
$c->log->info('user '.$ext_auth_username.' authenticated via external authentication');
$c->change_session_id;
delete $c->session->{external_auth_username};
return $c->res->redirect($c->req->uri->absolute);
}
my $registration_link = meon::Web::env->hostname_config->{'auth'}{'registration'};
$c->stash->{path} = $c->traverse_uri($registration_link);
$c->detach('resolve_xml', []);
}
my $token = $c->req->param('auth-token');
my $username = $c->req->param('username');
my $password = $c->req->param('password');
my $back_to = $c->req->param('back-to');
$c->session->{remember_login} = $c->req->param('remember_login');
if ($c->action eq 'logout') {
return $c->res->redirect($c->uri_for('/'));
}
if ($c->user_exists && !$token) {
$back_to ||= '/';
return $c->res->redirect($c->uri_for($back_to));
}
my $login_form = meon::Web::Form::Login->new();
# token authentication
if ($token) {
my $member;
if (($token eq 'admin') && $c->user_exists) {
my @roles = $c->user->roles;
if (any {$_ eq 'admin'} @roles) {
$member = meon::Web::Member->new(
members_folder => $members_folder,
username => $username,
);
}
}
else {
$member = meon::Web::Member->find_by_token(
members_folder => $members_folder,
token => $token,
);
if ($member && !$member->is_active) {
$member = undef;
$login_form->add_form_error('Account not activated or expired.');
}
}
if ($member) {
my $username = $member->username;
$c->set_authenticated($c->find_user({ username => $username }));
$c->log->info('user '.$username.' authenticated via token');
$c->change_session_id;
$c->session->{old_pw_not_required} = 1;
return $c->res->redirect(
$c->req->uri_with({
'auth-token' => undef,
'username' => undef,
})->absolute
);
}
else {
$login_form->add_form_error('Invalid authentication token.');
}
}
else {
$login_form->process(params=>$c->req->params);
if (meon::Web::env->hostname_config->{'auth'}{'external'}) {
if ($username && $password && $login_form->is_valid) {
my $auth_url = meon::Web::env->hostname_config->{'auth'}{'url'};
my $username_field = meon::Web::env->hostname_config->{'auth'}{'username'};
my $password_field = meon::Web::env->hostname_config->{'auth'}{'password'};
my $content_match = meon::Web::env->hostname_config->{'auth'}{'content-match'};
my $mech = WWW::Mechanize->new();
if ($content_match) {
eval {
$mech->get( $auth_url );
$mech->submit_form(
with_fields => {
$username_field => $username,
$password_field => $password,
}
);
die 'external auth failed - status '.$mech->status
unless $mech->status == 200;
$mech->get( $auth_url );
if ($content_match) {
die 'external auth failed - content does not match m/'.$content_match.'/xms ('.$mech->uri.')'
unless $mech->content =~ m/$content_match/xms;
}
};
}
else {
eval {
my $res = $mech->post( $auth_url, {
$username_field => $username,
$password_field => $password,
});
die 'external auth failed - status '.$mech->status
unless $mech->status == 200;
if ($res->header('Content-Type') =~ m{application/json}) {
my $data = eval { decode_json($res->content) };
if ($data && $data->{user_data}) {
$c->session->{backend_user_data} = $data->{user_data};
}
}
};
}
if ($@) {
$login_form->field('password')->add_error('authentication failed, please check your password or try again later');
$c->log->error($@);
$c->res->status(403);
}
else {
$c->session->{external_auth_username} = $username;
if (my $user = $c->find_user({ username => $username })) {
$c->set_authenticated($user);
delete $c->session->{external_auth_username};
}
$c->log->info('user '.$username.' authenticated via external authentication [2]');
return $c->res->redirect(
$c->req->uri_with({username => undef, password => undef})->absolute
);
}
}
}
else {
if ($username =~ m/\@/) {
my $member = meon::Web::Member->find_by_email(
members_folder => $members_folder,
email => $username,
);
$username = $member->user->username
if $member;
}
if ($username && $password && $login_form->is_valid) {
if (
$c->authenticate({
username => $username,
password => $password,
})
) {
$c->log->info('user '.$username.' authenticated');
$c->change_session_id;
return $c->res->redirect($c->req->uri);
}
else {
$c->log->info('login of user '.$username.' fail');
$login_form->field('password')->add_error('authentication failed');
$c->res->status(403);
}
}
}
}
$c->stash->{path} = URI->new('/login');
$c->forward('resolve_xml', []);
$c->model('ResponseXML')->add_xhtml_form(
$login_form->render
);
}
sub exception : Path('/exception-test') {
die 'here';
}
sub end : ActionClass('RenderView') {
my ($self, $c) = @_;
my @errors = @{ $c->error };
if (@errors) {
$c->response->status(500);
my $message = join("\n", @errors);
$message ||= 'No output';
my $xml_file = file(meon::Web::env->content_dir, '500.xml');
if (-e $xml_file) {
eval {
$c->session->{post_redirect_path} = '/500';
$c->forward('resolve_xml', []);
$c->model('ResponseXML')->push_new_element('error-message')->appendText($message)
if $message;
};
if ($@) {
$c->log->error($@);
return;
}
}
else {
$message = '500 - Internal server error: '.$c->req->uri."\n".($message // '');
$c->res->content_type('text/plain');
$c->res->body($message);
}
}
while (my $error = shift(@{$c->error})) {
$c->log->error($error);
}
}
__PACKAGE__->meta->make_immutable;
1;