Commit 61fd463f authored by Xavier Guimard's avatar Xavier Guimard

Split notifications (XML vs JSON) [#868]

parent f6665c2d
......@@ -2,19 +2,21 @@ package Lemonldap::NG::Common::Notifications;
use strict;
use Mouse;
use XML::LibXML;
our $VERSION = '2.0.0';
extends 'Lemonldap::NG::Common::Module';
# XML parser. TODO: replace this by JSON
has parser => (
is => 'rw',
builder => sub {
return XML::LibXML->new();
sub import {
if ( $_[1] eq 'XML' ) {
extends 'Lemonldap::NG::Common::Notifications::XML',
'Lemonldap::NG::Common::Module';
}
);
else {
extends 'Lemonldap::NG::Common::Notifications::JSON',
'Lemonldap::NG::Common::Module';
}
}
has notifField => (
is => 'rw',
......@@ -40,111 +42,4 @@ sub getNotifications {
}
}
# Check XML datas and insert new notifications.
# @param $xml XML string containing notification
# @return number of notifications done
sub newNotification {
my ( $self, $xml ) = @_;
eval { $xml = $self->parser->parse_string($xml); };
if ($@) {
$self->lmLog( "Unable to read XML file : $@", 'error' );
return 0;
}
my @notifs;
my ( $version, $encoding ) = ( $xml->version(), $xml->encoding() );
foreach
my $notif ( $xml->documentElement->getElementsByTagName('notification') )
{
my @datas = ();
# Mandatory information
foreach (qw(date uid reference)) {
my $tmp;
unless ( $tmp = $notif->getAttribute($_) ) {
$self->lmLog( "Attribute $_ is missing", 'error' );
return 0;
}
push @datas, $tmp;
}
# Other information
foreach (qw(condition)) {
my $tmp;
if ( $tmp = $notif->getAttribute($_) ) {
push @datas, $tmp;
}
else { push @datas, ""; }
}
my $result = XML::LibXML::Document->new( $version, $encoding );
my $root = XML::LibXML::Element->new('root');
$root->appendChild($notif);
$result->setDocumentElement($root);
push @notifs, [ @datas, $result ];
}
my $tmp = $self->{type};
my $count;
foreach (@notifs) {
$count++;
my ( $r, $err ) = $self->newNotif(@$_);
die "$err" unless ($r);
}
return $count;
}
## Delete notifications for the connected user
## @param $uid of the user
## @param $myref notification's reference
## @return number of deleted notifications
sub deleteNotification {
my ( $self, $uid, $myref ) = @_;
my @data;
# Check input parameters
unless ( $uid and $myref ) {
$self->lmLog(
"SOAP service deleteNotification called without all parameters",
'error' );
return 0;
}
$self->lmLog(
"SOAP service deleteNotification called for uid $uid and reference $myref",
'debug'
);
# Get notifications
my $user = $self->get($uid);
# Return 0 if no files were found
return 0 unless ($user);
# Counting
my $count = 0;
foreach my $ref ( keys %$user ) {
my $xml = $self->parser->parse_string( $user->{$ref} );
# Browse notification in file
foreach my $notif (
$xml->documentElement->getElementsByTagName('notification') )
{
# Get notification's data
if ( $notif->getAttribute('reference') eq $myref ) {
push @data, $ref;
}
# Delete the notification (really)
foreach (@data) {
if ( $self->purge( $_, 1 ) ) {
$self->lmLog( "Notification $_ was removed.", 'debug' );
$count++;
}
}
}
}
return $count;
}
1;
......@@ -14,6 +14,10 @@ use Encode;
our $VERSION = '2.0.0';
extends 'Lemonldap::NG::Common::Notifications';
sub import {
shift;
return Lemonldap::NG::Common::Notifications->import(@_)
}
has dbiTable => (
is => 'ro',
......@@ -28,7 +32,7 @@ has dbiChain => (
has dbiUser => (
is => 'ro',
default => sub {
$_[0]->p->lmLog( 'Warning: "dbiUser" parameter is not set', 'warn' );
$_[0]->{p}->lmLog( 'Warning: "dbiUser" parameter is not set', 'warn' );
return '';
}
);
......
......@@ -12,6 +12,11 @@ use MIME::Base64;
our $VERSION = '2.0.0';
extends 'Lemonldap::NG::Common::Notifications';
sub import {
shift;
return Lemonldap::NG::Common::Notifications->import(@_)
}
has dirName => ( is => 'ro', required => 1 );
......
......@@ -16,6 +16,10 @@ use utf8;
our $VERSION = '2.0.0';
extends 'Lemonldap::NG::Common::Notifications';
sub import {
shift;
return Lemonldap::NG::Common::Notifications->import(@_)
}
has ldapServer => (
is => 'ro',
......
package Lemonldap::NG::Common::Notifications::XML;
use strict;
use Mouse;
use XML::LibXML;
# XML parser
has parser => (
is => 'rw',
builder => sub {
return XML::LibXML->new();
}
);
# Check XML datas and insert new notifications.
# @param $xml XML string containing notification
# @return number of notifications done
sub newNotification {
my ( $self, $xml ) = @_;
eval { $xml = $self->parser->parse_string($xml); };
if ($@) {
$self->lmLog( "Unable to read XML file : $@", 'error' );
return 0;
}
my @notifs;
my ( $version, $encoding ) = ( $xml->version(), $xml->encoding() );
foreach
my $notif ( $xml->documentElement->getElementsByTagName('notification') )
{
my @datas = ();
# Mandatory information
foreach (qw(date uid reference)) {
my $tmp;
unless ( $tmp = $notif->getAttribute($_) ) {
$self->lmLog( "Attribute $_ is missing", 'error' );
return 0;
}
push @datas, $tmp;
}
# Other information
foreach (qw(condition)) {
my $tmp;
if ( $tmp = $notif->getAttribute($_) ) {
push @datas, $tmp;
}
else { push @datas, ""; }
}
my $result = XML::LibXML::Document->new( $version, $encoding );
my $root = XML::LibXML::Element->new('root');
$root->appendChild($notif);
$result->setDocumentElement($root);
push @notifs, [ @datas, $result ];
}
my $tmp = $self->{type};
my $count;
foreach (@notifs) {
$count++;
my ( $r, $err ) = $self->newNotif(@$_);
die "$err" unless ($r);
}
return $count;
}
## Delete notifications for the connected user
## @param $uid of the user
## @param $myref notification's reference
## @return number of deleted notifications
sub deleteNotification {
my ( $self, $uid, $myref ) = @_;
my @data;
# Check input parameters
unless ( $uid and $myref ) {
$self->lmLog(
"SOAP service deleteNotification called without all parameters",
'error' );
return 0;
}
$self->lmLog(
"SOAP service deleteNotification called for uid $uid and reference $myref",
'debug'
);
# Get notifications
my $user = $self->get($uid);
# Return 0 if no files were found
return 0 unless ($user);
# Counting
my $count = 0;
foreach my $ref ( keys %$user ) {
my $xml = $self->parser->parse_string( $user->{$ref} );
# Browse notification in file
foreach my $notif (
$xml->documentElement->getElementsByTagName('notification') )
{
# Get notification's data
if ( $notif->getAttribute('reference') eq $myref ) {
push @data, $ref;
}
# Delete the notification (really)
foreach (@data) {
if ( $self->purge( $_, 1 ) ) {
$self->lmLog( "Notification $_ was removed.", 'debug' );
$count++;
}
}
}
}
return $count;
}
1;
......@@ -31,9 +31,7 @@ sub init {
my ( $self, $args ) = @_;
$args ||= {};
my $conf = $self->confAcc;
if ( my $localconf = $conf->getLocalConf(MANAGERSECTION) ) {
if ( my $localconf = $self->confAcc->getLocalConf(MANAGERSECTION) ) {
$self->{$_} = $args->{$_} // $localconf->{$_}
foreach ( keys %$localconf );
}
......@@ -57,10 +55,11 @@ sub init {
split( /[,\s]+/, $self->{enabledModules} );
extends 'Lemonldap::NG::Handler::PSGI::Router', @enabledModules;
my @working;
my $conf = $self->confAcc->getConf;
for ( my $i = 0 ; $i < @enabledModules ; $i++ ) {
my $mod = $enabledModules[$i];
no strict 'refs';
if ( &{"${mod}::addRoutes"}($self) ) {
if ( &{"${mod}::addRoutes"}($self,$conf) ) {
$self->lmLog( "Module $mod enabled", 'debug' );
push @working, $mod;
}
......
......@@ -1720,6 +1720,10 @@ qr/^(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][-a-zA-Z0-
'oidcStorageOptions' => {
'type' => 'keyTextContainer'
},
'oldNotifFormat' => {
'default' => 0,
'type' => 'bool'
},
'openIdAttr' => {
'type' => 'text'
},
......
......@@ -660,6 +660,11 @@ sub attributes {
},
# Notification
oldNotifFormat => {
type => 'bool',
default => 0,
documentation => 'Use old XML format',
},
notificationWildcard => {
type => 'text',
default => 'allusers',
......
......@@ -534,6 +534,7 @@ sub tree {
help => 'notifications.html',
nodes => [
'notification',
'oldNotifFormat',
'notificationStorage',
'notificationStorageOptions',
'notificationWildcard',
......
......@@ -27,7 +27,7 @@ our $VERSION = '2.0.0';
use constant defaultRoute => 'manager.html';
sub addRoutes {
my $self = shift;
my($self,$conf) = @_;
# HTML template
$self->addRoute( 'manager.html', undef, ['GET'] )
......
......@@ -7,7 +7,7 @@ use Mouse;
use Lemonldap::NG::Common::Conf::Constants;
use Lemonldap::NG::Common::PSGI::Constants;
use Lemonldap::NG::Common::Conf::ReConstants;
use Lemonldap::NG::Common::Notifications;
require Lemonldap::NG::Common::Notifications;
use feature 'state';
......@@ -24,7 +24,14 @@ has _notifAccess => ( is => 'rw' );
use constant defaultRoute => 'notifications.html';
sub addRoutes {
my $self = shift;
my($self,$conf) = @_;
if($conf->{oldNotifFormat}) {
Lemonldap::NG::Common::Notifications->import('XML');
}
else {
Lemonldap::NG::Common::Notifications->import('JSON');
}
unless ( $self->notifAccess ) {
$self->addRoute( 'notifications.html', 'notEnabled', ['GET'] );
......
......@@ -25,7 +25,7 @@ our $VERSION = '2.0.0';
use constant defaultRoute => 'sessions.html';
sub addRoutes {
my $self = shift;
my($self,$conf) = @_;
# HTML template
$self->addRoute( 'sessions.html', undef, ['GET'] )
......@@ -39,11 +39,6 @@ sub addRoutes {
['DELETE']
);
my $conf = $self->confAcc->getConf(
{ localPrm => $self->confAcc->getLocalConf(SESSIONSEXPLORERSECTION) } );
#
# Return unless configuration is available
return 0 unless ($conf);
$self->setTypes($conf);
$self->{ipField} ||= 'ipAddr';
......
......@@ -461,6 +461,7 @@
"oidcServiceAllowImplicitFlow": "Implicit Flow",
"oidcServiceAllowHybridFlow": "Hybrid Flow",
"ok": "OK",
"oldNotifFormat": "Use old XML format",
"openIdAttr": "OpenID login",
"openIdAuthnLevel": "Authentication level",
"openIdExportedVars": "Exported variables",
......
......@@ -461,6 +461,7 @@
"oidcServiceAllowImplicitFlow": "Implicit Flow",
"oidcServiceAllowHybridFlow": "Hybrid Flow",
"ok": "OK",
"oldNotifFormat": "Utiliser l'ancien format XML",
"openIdAttr": "Identifiant OpenID",
"openIdAuthnLevel": "Niveau d'authentification",
"openIdExportedVars": "Variables exportées",
......
......@@ -132,6 +132,7 @@
"_whatToTrace": "$_auth eq 'SAML' ? \"$_user\\@$_idpConfKey\" : \"$_user\""
},
"notification": 1,
"oldNotifFormat": 1,
"notificationStorage": "File",
"notificationStorageOptions": {
"dirName": "t/notifications"
......
......@@ -2454,4 +2454,8 @@
"id": "cfgLog",
"title": "cfgLog",
"data": "Log"
}, {
"id": "oldNotifFormat",
"title": "oldNotifFormat",
"data": 1
}]
......@@ -46,6 +46,8 @@ lib/Lemonldap/NG/Portal/Lib/Choice.pm
lib/Lemonldap/NG/Portal/Lib/DBI.pm
lib/Lemonldap/NG/Portal/Lib/LDAP.pm
lib/Lemonldap/NG/Portal/Lib/Net/LDAP.pm
lib/Lemonldap/NG/Portal/Lib/Notifications/JSON.pm
lib/Lemonldap/NG/Portal/Lib/Notifications/XML.pm
lib/Lemonldap/NG/Portal/Lib/OneTimeToken.pm
lib/Lemonldap/NG/Portal/Lib/OpenID/Server.pm
lib/Lemonldap/NG/Portal/Lib/OpenID/SREG.pm
......@@ -333,7 +335,6 @@ site/templates/pastel/yubikeyform.tpl
t/01-AuthDemo.t
t/02-Password-Demo.t
t/03-XSS-protection.t
t/04-Notification-File.t
t/20-Auth-and-password-DBI.t
t/21-Auth-and-password-LDAP.t
t/22-Auth-and-password-AD.t
......@@ -361,6 +362,7 @@ t/34-Auth-Proxy-and-SOAP-Server.t
t/35-REST-sessions-with-REST-server.t
t/35-SOAP-sessions-with-SOAP-server.t
t/40-Notifications-DBI.t
t/40-Notifications-File.t
t/41-Captcha.t
t/41-Token.t
t/42-Register-Demo-with-captcha.t
......
package Lemonldap::NG::Portal::Lib::Notifications::XML;
use strict;
use Mouse;
use XML::LibXML;
use XML::LibXSLT;
our $VERSION = '2.0.0';
# 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();
}
);
# 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->{templatesDir} . '/common/notification.xsl'
);
die "$styleFile not found" unless ( -e $styleFile );
return $xslt->parse_stylesheet( $self->parser->parse_file($styleFile) );
}
);
# Underlying notifications storage object (File, DBI, LDAP,...)
has notifObject => ( is => 'rw' );
# INITIALIZATION
sub init {
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;
return 0 unless ($notifs);
# 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->lmLog( "Get reference $reference", 'debug' );
# Check it in session
if ( exists $req->{sessionInfo}->{"notification_$reference"} ) {
# The notification was already accepted
$self->lmLog( "Notification $reference was already accepted",
'debug' );
# Remove it from XML
$notif->unbindNode();
next LOOP;
}
# Check condition if any
my $condition = $notif->getAttribute('condition');
if ($condition) {
$self->lmLog( "Get condition $condition", 'debug' );
unless ( $self->p->HANDLER->safe->reval($condition) ) {
$self->lmLog( "Notification condition not accepted",
'debug' );
# 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->lmLog( "Bad XML file: a notification for $uid was not done ($@)",
'warn' );
return 0;
}
# Stop here if nothing to display
return 0 unless $i;
# Returns HTML fragment
$req->id( $self->p->HANDLER->tsv->{cipher}->encrypt( $req->id ) );
return $form;
}
sub getNotifBack {
my ( $self, $req, $name ) = @_;
# Look if all notifications have been accepted. If not, redirects to
# portal
# Search for Lemonldap::NG cookie (ciphered)
my $id;
unless ( $id = $req->cookies->{ $self->{conf}->{cookieName} } ) {
return $self->p->sendError( $req, 'No cookie found', 401 );
}
$id = $self->p->HANDLER->tsv->{cipher}->decrypt($id)
or return $self->sendError( $req, 'Unable to decrypt', 500 );
# Verify that session exists
$req->userData( $self->p->HANDLER->retrieveSession($id) )
or return $self->sendError( $req, 'Unknown session', 401 );
# Restore datas
$self->p->importHandlerDatas($req);
my $uid = $req->sessionInfo->{ $self->notifObject->notifField };
my ( $notifs, $forUser ) = $self->notifObject->getNotifications($uid);
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;
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
foreach my $notif (
$xml->documentElement->getElementsByTagName('notification') )
{
my $reference = $notif->getAttribute('reference');
# 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->p->userNotice(