lemonldap-ng/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/Notifications/XML.pm

435 lines
15 KiB
Perl

package Lemonldap::NG::Portal::Lib::Notifications::XML;
use strict;
use Mouse;
use XML::LibXML;
use XML::LibXSLT;
use POSIX qw(strftime);
our $VERSION = '2.0.12';
# Lemonldap::NG::Portal::Main::Plugin provides addAuthRoute() and
# addUnauthRoute() methods in addition of Lemonldap::NG::Common::Module.
extends 'Lemonldap::NG::Portal::Main::Plugin';
# PROPERTIES
# XML parser
has parser => (
is => 'rw',
builder => sub {
return XML::LibXML->new( load_ext_dtd => 0, expand_entities => 0 );
}
);
# XSLT document
has stylesheet => (
is => 'rw',
lazy => 1,
builder => sub {
my $self = $_[0];
my $xslt = XML::LibXSLT->new();
my $styleFile =
( $self->conf->{notificationXSLTfile}
and -e $self->conf->{notificationXSLTfile} )
? $self->conf->{notificationXSLTfile}
: $self->conf->{templateDir} . '/common/notification.xsl';
unless ( -e $styleFile ) {
$self->{logger}->error("$styleFile not found, aborting");
die "$styleFile not found";
}
return $xslt->parse_stylesheet( $self->parser->parse_file($styleFile) );
}
);
# Underlying notifications storage object (File, DBI, LDAP,...)
has notifObject => ( is => 'rw' );
# Notification server accessors
has imported => ( is => 'rw', default => 0 );
has server => ( is => 'rw' );
# INITIALIZATION
sub init {
return 1;
}
# Search for notifications and if any, returns HTML fragment.
sub checkForNotifications {
my ( $self, $req ) = @_;
# Look for pending notifications in database
my $uid = $req->sessionInfo->{ $self->notifObject->notifField };
my ( $notifs, $forUser ) = $self->notifObject->getNotifications($uid);
my $form;
unless ($notifs) {
$self->logger->info("No notification found");
return 0;
}
# Transform notifications
my $i = 0; # Files count
my $now = strftime "%Y-%m-%d", localtime;
foreach my $file ( values %$notifs ) {
my $xml = $self->parser->parse_string($file);
my $j = 0; # Notifications count
LOOP: foreach my $notif (
eval {
$xml->documentElement->getElementsByTagName('notification');
}
)
{
# Get the reference
my $reference = $notif->getAttribute('reference');
$self->logger->debug("Get reference $reference");
# Check it in session
if ( exists $req->{sessionInfo}->{"notification_$reference"} ) {
# The notification was already accepted
$self->logger->debug(
"Notification $reference was already accepted");
# Remove it from XML
$notif->unbindNode();
next LOOP;
}
# Check date
my $date = $notif->getAttribute('date');
$self->logger->debug("Get date: $date");
unless ( $date and $date =~ /\b\d{4}-\d{2}-\d{2}\b/ ) {
$self->logger->error('Malformed date');
$notif->unbindNode();
next LOOP;
}
unless ( $date le $now ) {
$self->logger->debug('Notification date not reached');
# Remove it from XML
$notif->unbindNode();
next LOOP;
}
# Check condition if any
if ( my $condition = $notif->getAttribute('condition') ) {
$self->logger->debug("Get condition $condition");
$condition = $self->p->HANDLER->substitute($condition);
unless ( $condition = $self->p->HANDLER->buildSub($condition) )
{
$self->logger->error( 'Notification condition error: '
. $self->p->HANDLER->tsv->{jail}->error );
# Remove it from XML
$notif->unbindNode();
next LOOP;
}
unless ( $condition->( $req, $req->sessionInfo ) ) {
$self->logger->debug(
'Notification condition not authorized');
# Remove it from XML
$notif->unbindNode();
next LOOP;
}
}
$j++;
}
# Go to next file if no notification found
next unless $j;
$i++;
# Transform XML into HTML
my $results = $self->stylesheet->transform( $xml, start => $i );
$form .= $self->stylesheet->output_string($results);
}
if ($@) {
$self->userLogger->warn(
"Bad XML file: a notification for $uid was not done ($@)");
return 0;
}
# Stop here if nothing to display
return 0 unless $i;
$self->userLogger->info("$i pending notification(s) found for $uid");
# Returns HTML fragment
return $form;
}
# Search for accepted notification and if any, returns HTML fragment.
sub viewNotification {
my ( $self, $req, $ref, $epoch ) = @_;
# Look for pending notifications in database
my $uid = $req->userData->{ $self->notifObject->notifField };
my ( $notifs, $forUser ) =
$self->notifObject->getAcceptedNotifs( $uid, $ref );
my $form;
unless ($notifs) {
$self->logger->info("No accepted notification found");
return 0;
}
# Transform notifications
my $i = 0; # Files count
foreach my $file ( values %$notifs ) {
my $xml = $self->parser->parse_string($file);
my $j = 0; # Notifications count
LOOP: foreach my $notif (
eval {
$xml->documentElement->getElementsByTagName('notification');
}
)
{
# Get the reference
my $reference = $notif->getAttribute('reference');
$self->logger->debug("Get reference $reference");
# Check it in session
unless (exists $req->{userData}->{"notification_$reference"}
and $req->{userData}->{"notification_$reference"} eq $epoch
and $reference eq $ref )
{
# The notification is not already accepted
$self->logger->debug(
"Notification $reference was already accepted");
# Remove it from XML
$notif->unbindNode();
next LOOP;
}
$j++;
}
# Go to next file if no notification found
next unless $j;
$i++;
# Transform XML into HTML
my $results = $self->stylesheet->transform( $xml, start => $i );
$form .= $self->stylesheet->output_string($results);
}
if ($@) {
$self->userLogger->warn(
"Bad XML file: a notification for $uid was not done ($@)");
return 0;
}
# Stop here if nothing to display
return 0 unless $i;
$self->userLogger->info("$i accepted notification(s) found for $uid");
# Returns HTML fragment
return $form;
}
sub getNotifBack {
my ( $self, $req, $name ) = @_;
# Search for Lemonldap::NG cookie (ciphered)
my $id;
unless ( $id = $req->cookies->{ $self->{conf}->{cookieName} } ) {
return $self->p->sendError( $req, 'No cookie found', 401 );
}
if ( $req->param('cancel') ) {
$self->logger->debug('Cancel called -> remove ciphered cookie');
$req->addCookie(
$self->p->cookie(
name => $self->conf->{cookieName},
value => 0,
domain => $self->conf->{domain},
secure => $self->conf->{securedCookie},
expires => 'Wed, 21 Oct 2015 00:00:00 GMT'
)
);
$req->mustRedirect(1);
return $self->p->do( $req, [] );
}
# Look if all notifications have been accepted. If not, redirect to
# portal
# Try to decrypt Lemonldap::NG ciphered cookie
$id = $self->p->HANDLER->tsv->{cipher}->decrypt($id)
or return $self->sendError( $req, 'Unable to decrypt', 500 );
# Check that session exists
$req->userData( $self->p->HANDLER->retrieveSession( $req, $id ) )
or return $self->sendError( $req, 'Unknown session', 401 );
# Restore data
$self->p->importHandlerData($req);
my $uid = $req->sessionInfo->{ $self->notifObject->notifField };
# ALL notifications are returned here => Need to check active ones only
my ( $notifs, $forUser ) =
eval { $self->notifObject->getNotifications($uid) };
return $self->p->sendError( $req, $@, 500 ) if ($@);
if ($notifs) {
# Get accepted notifications
my ( $refs, $checks ) = ( {}, {} );
my $prms = $req->parameters;
foreach ( keys %$prms ) {
my $v = $prms->{$_};
if (s/^reference//) {
$refs->{$v} = $_;
}
elsif ( s/^check// and /^(\d+x\d+)x(\d+)$/ and $v eq 'accepted' ) {
push @{ $checks->{$1} }, $2;
}
}
my $result = 1;
my $now = strftime "%Y-%m-%d", localtime;
foreach my $fileName ( keys %$notifs ) {
my $file = $notifs->{$fileName};
my $fileResult = 1;
my $xml = $self->parser->parse_string($file);
# Get pending notifications and verify that they have been accepted
LOOP:
foreach my $notif (
$xml->documentElement->getElementsByTagName('notification') )
{
my $reference = $notif->getAttribute('reference');
# Check date
my $date = $notif->getAttribute('date');
$self->logger->debug("Get date: $date");
unless ( $date and $date =~ /\b\d{4}-\d{2}-\d{2}\b/ ) {
$self->logger->error('Malformed date');
next LOOP;
}
unless ( $date le $now ) {
$self->logger->debug('Notification date not reached');
$fileResult = 0; # Do not delete notification
next LOOP;
}
# Check condition if any
if ( my $condition = $notif->getAttribute('condition') ) {
$self->logger->debug("Get condition $condition");
$condition = $self->p->HANDLER->substitute($condition);
unless ( $condition =
$self->p->HANDLER->buildSub($condition) )
{
$self->logger->error( 'Notification condition error: '
. $self->p->HANDLER->tsv->{jail}->error );
next LOOP;
}
unless ( $condition->( $req, $req->sessionInfo ) ) {
$self->logger->debug(
'Notification condition not authorized');
$fileResult = 0; # Do not delete notification
next LOOP;
}
}
# Check if this pending notification has been seen
if ( my $refId = $refs->{$reference} ) {
# Verity that checkboxes have been checked
my @toCheck = $notif->getElementsByTagName('check');
if ( my $toCheckCount = @toCheck ) {
unless ($checks->{$refId}
and $toCheckCount == @{ $checks->{$refId} } )
{
$self->userLogger->notice(
"$uid has not accepted notification $reference"
);
$result = $fileResult = 0;
next;
}
}
}
else {
# Current pending notification has not been found in
# request
$result = $fileResult = 0;
$self->logger->debug(
'Current pending notification has not been found');
next;
}
# Register acceptation
$self->userLogger->notice(
"$uid has accepted notification $reference");
$self->p->updatePersistentSession( $req,
{ "notification_$reference" => time() } );
$self->logger->debug(
"Notification $reference registered in persistent session");
}
# Notifications accepted for this file, delete it unless it's a wildcard
if ( $fileResult and exists $forUser->{$fileName} ) {
$self->logger->debug("Notification file deleted");
$self->notifObject->delete($fileName);
}
}
unless ($result) {
# One pending notification has been found and not accepted,
# restart process to display pending notifications
# TODO: is it a good idea to launch all 'endAuth' subs ?
$self->logger->debug(
'Pending notification has been found and not accepted');
return $self->p->do( $req, [ @{ $self->p->endAuth } ] );
}
# All pending notifications have been accepted, restore cookies and
# launch 'controlUrl' to restore "urldc" using do()
$self->logger->debug('All pending notifications have been accepted');
$self->p->rebuildCookies($req);
return $self->p->do( $req, [ 'controlUrl', @{ $self->p->endAuth } ] );
}
else {
# No notifications checked here, this entry point must not be called.
# Redirecting to portal
$self->logger->debug('No notifications checked');
$req->mustRedirect(1);
return $self->p->do( $req, [] );
}
}
sub notificationServer {
my ( $self, $req ) = @_;
unless ( $self->imported ) {
eval {
require Lemonldap::NG::Common::PSGI::SOAPServer;
require Lemonldap::NG::Common::PSGI::SOAPService;
};
if ($@) {
return $self->p->sendError( $req, $@, 500 );
}
$self->server( Lemonldap::NG::Common::PSGI::SOAPServer->new );
$self->imported(1);
}
unless ( $req->env->{HTTP_SOAPACTION} ) {
return $self->p->sendError( $req, 'SOAP requests only', 400 );
}
return $self->server->dispatch_to(
Lemonldap::NG::Common::PSGI::SOAPService->new(
$self, $req, 'newNotification',
)
)->handle($req);
}
sub newNotification {
my ( $self, $req, $xml ) = @_;
return $self->notifObject->newNotification( $xml,
$self->conf->{notificationDefaultCond} );
}
1;