Commit 591766d6 authored by Christophe Maudoux's avatar Christophe Maudoux

WIP - Messages SPA skeleton (#1772)

parent 8e9f4e51
Pipeline #6175 passed with stage
in 13 minutes and 22 seconds
......@@ -38,7 +38,7 @@ useRedirectOnError = 0
[manager]
enabledModules = conf, sessions, notifications, 2ndFA, viewer
enabledModules = conf, sessions, 2ndFA, notifications, messages, viewer
protection = manager
viewerHiddenKeys = samlIDPMetaDataNodes samlSPMetaDataNodes portalDisplayLogout captcha_login_enabled
......
......@@ -372,7 +372,7 @@ languages = fr, en, it, vi, ar
; Manager modules enabled
; Set here the list of modules you want to see in manager interface
; The first will be used as default module displayed
enabledModules = conf, sessions, notifications, 2ndFA, viewer
enabledModules = conf, sessions, 2ndFA, notifications, messages, viewer
; To avoid restricted users to edit configuration, defaulModule MUST be different than 'conf'
; 'viewer' is set by default
......
package Lemonldap::NG::Common::Messages;
use strict;
use Mouse;
our $VERSION = '2.0.7';
extends 'Lemonldap::NG::Common::Module';
sub import {
# if ( $_[1] eq 'XML' ) {
# extends 'Lemonldap::NG::Common::Notifications::XML',
# 'Lemonldap::NG::Common::Module';
# }
# else {
extends 'Lemonldap::NG::Common::Messages::JSON',
'Lemonldap::NG::Common::Module';
# }
}
has msgField => (
is => 'rw',
builder => sub {
my $uid =
$_[0]->conf->{notificationField}
|| $_[0]->conf->{whatToTrace}
|| 'uid';
$uid =~ s/^\$//;
return $uid;
}
);
sub getMessages {
my ( $self, $uid ) = @_;
my $forUser = $self->get($uid);
my $forAll = $self->get( $self->conf->{notificationWildcard} );
if ( $forUser and $forAll ) {
return { %$forUser, %$forAll };
}
else {
return ( ( $forUser ? $forUser : $forAll ), $forUser );
}
}
1;
## @file
# DBI storage methods for messages
## @class
# DBI storage methods for messages
package Lemonldap::NG::Common::Messages::DBI;
use strict;
use Mouse;
use Time::Local;
use DBI;
use Encode;
our $VERSION = '2.0.7';
extends 'Lemonldap::NG::Common::Messages';
sub import {
shift;
return Lemonldap::NG::Common::Messages->import(@_);
}
has dbiTable => (
is => 'ro',
lazy => 1,
default => sub { $_[0]->{table} || 'notifications' }
);
has dbiChain => (
is => 'ro',
required => 1
);
has dbiUser => (
is => 'ro',
lazy => 1,
default => sub {
$_[0]->{p}->logger->warn('Warning: "dbiUser" parameter is not set');
return '';
}
);
has dbiPassword => ( is => 'ro', default => '' );
# Database handle object
has _dbh => (
is => 'rw',
lazy => 1,
builder => sub {
my $self = shift;
my $r = DBI->connect_cached(
$self->{dbiChain}, $self->{dbiUser},
$self->{dbiPassword}, { RaiseError => 1 }
);
$self->logger->error($DBI::errstr) unless ($r);
return $r;
}
);
# Current query
has sth => ( is => 'rw' );
# Returns messages corresponding to the user $uid.
# If $ref is set, returns only message corresponding to this reference.
sub get {
my ( $self, $uid, $ref ) = @_;
return () unless ($uid);
$self->_execute(
"SELECT * FROM "
. $self->dbiTable
. " WHERE done IS NULL AND uid=?"
. ( $ref ? " AND ref=?" : '' )
. " ORDER BY date",
$uid,
( $ref ? $ref : () )
) or return ();
my $result;
while ( my $h = $self->sth->fetchrow_hashref() ) {
# Get XML message
my $xml = $h->{xml};
# Decode it to get the correct uncoded string
Encode::from_to( $xml, "utf8", "iso-8859-1", Encode::FB_CROAK );
# Store message in result
my $identifier =
&getIdentifier( $self, $h->{uid}, $h->{ref}, $h->{date} );
$result->{$identifier} = $xml;
}
$self->logger->warn( $self->sth->err() ) if ( $self->sth->err() );
return $result;
}
## @method hashref getAll()
# Return all messages not notified.
# @return hashref where keys are internal reference and values are hashref with
# keys date, uid and ref.
sub getAll {
my $self = shift;
$self->_execute( 'SELECT * FROM '
. $self->dbiTable
. ' WHERE done IS NULL ORDER BY date' );
my $result;
while ( my $h = $self->sth->fetchrow_hashref() ) {
$result->{"$h->{date}#$h->{uid}#$h->{ref}"} = {
date => $h->{date},
uid => $h->{uid},
ref => $h->{ref},
condition => $h->{condition}
};
}
$self->logger->warn( $self->sth->err() ) if ( $self->sth->err() );
return $result;
}
## @method boolean delete(string myref)
# Mark a message as done.
# @param $myref identifier returned by get() or getAll()
sub delete {
my ( $self, $myref ) = @_;
my ( $d, $u, $r );
unless ( ( $d, $u, $r ) = ( $myref =~ /^([^#]+)#(.+?)#(.+)$/ ) ) {
$self->logger->warn("Bad reference $myref");
return 0;
}
my @ts = localtime();
$ts[5] += 1900;
$ts[4]++;
return $self->_execute( 'UPDATE '
. $self->dbiTable
. " SET done='$ts[5]-$ts[4]-$ts[3] $ts[2]:$ts[1]' "
. 'WHERE done IS NULL AND uid=? AND ref=? AND date=?',
$u, $r, $d );
}
## @method boolean purge(string myref, boolean force)
# Purge message (really delete record)
# @param $myref identifier returned by get or getAll
# @param $force force purge for not deleted session
# @return true if something was deleted
sub purge {
my ( $self, $myref, $force ) = @_;
my ( $d, $u, $r );
unless ( ( $d, $u, $r ) = ( $myref =~ /^([^#]+)#(.+?)#(.+)$/ ) ) {
$self->logger->warn("Bad reference $myref");
return 0;
}
unless ( $d =~ s/^(\d{4})(\d{2})(\d{2}).*$/$1-$2-$3/
or $d =~ s/^(\d{4}-\d{2}-\d{2}).*$/$1/ )
{
$self->logger->warn("Bad date $d");
return 0;
}
my $clause;
$clause = "done IS NOT NULL AND" unless ($force);
return $self->_execute( 'DELETE FROM '
. $self->dbiTable
. " WHERE $clause uid=? AND ref=? AND date=?",
$u, $r, $d );
}
## @method boolean Msg(string date, string uid, string ref, string condition, string xml)
# Insert a new message
# @param date Date
# @param uid UID
# @param ref Reference of the message
# @param condition Condition for the message
# @param xml XML message
# @return true if succeed
sub newMsg {
my ( $self, $date, $uid, $ref, $condition, $xml ) = @_;
my $res =
$condition =~ /.+/
? $self->_execute( 'INSERT INTO '
. $self->dbiTable
. ' (date,uid,ref,cond,xml) VALUES(?,?,?,?,?)',
$date, $uid, $ref, $condition, $xml )
: $self->_execute( 'INSERT INTO '
. $self->dbiTable
. ' (date,uid,ref,xml) VALUES(?,?,?,?)',
$date, $uid, $ref, $xml );
return $res;
}
## @method hashref getDone()
# Returns a list of message that have been done
# @return hashref where keys are internal reference and values are hashref with
# keys notified, uid and ref.
sub getDone {
my ($self) = @_;
$self->_execute( 'SELECT * FROM '
. $self->dbiTable
. ' WHERE done IS NOT NULL ORDER BY done' );
my $result;
while ( my $h = $self->sth->fetchrow_hashref() ) {
my @t = split( /\D+/, $h->{date} );
my $done =
timelocal( $t[5] || 0, $t[4] || 0, $t[3] || 0, $t[2], $t[1], $t[0] );
$result->{"$h->{date}#$h->{uid}#$h->{ref}"} =
{ notified => $done, uid => $h->{uid}, ref => $h->{ref}, };
}
$self->logger->warn( $self->sth->err() ) if ( $self->sth->err() );
return $result;
}
## @method private object _execute(string query, array args)
# Execute a query and catch errors
# @return number of lines touched or 1 if select succeed
sub _execute {
my ( $self, $query, @args ) = @_;
my $dbh = $self->_dbh or die "DB connection unavailable";
unless ( $self->sth( $dbh->prepare($query) ) ) {
$self->logger->warn( $dbh->errstr() );
return 0;
}
my $tmp;
unless ( $tmp = $self->sth->execute(@args) ) {
$self->logger->warn( $self->sth->errstr() );
return 0;
}
return $tmp;
}
## @method string getIdentifier(string uid, string ref, string date)
# Get message identifier
# @param $uid uid
# @param $ref ref
# @param $date date
# @return the message identifier
sub getIdentifier {
my ( $self, $uid, $ref, $date ) = @_;
return $date . "#" . $uid . "#" . $ref;
}
1;
## @file
# File storage methods for messages
## @class
# File storage methods for messages
package Lemonldap::NG::Common::Messages::File;
use strict;
use Mouse;
use MIME::Base64;
our $VERSION = '2.0.7';
extends 'Lemonldap::NG::Common::Messages';
our $ext = 'json';
sub import {
shift;
$ext = 'xml' if ( $_[0] eq 'XML' );
return Lemonldap::NG::Common::Messages->import(@_);
}
has dirName => ( is => 'ro', required => 1 );
has table => (
is => 'rw',
trigger => sub {
$_[0]->{dirName} =~ s/\/conf\/?$//;
$_[0]->{dirName} .= "/$_[0]->{table}";
}
);
has fileNameSeparator => ( is => 'rw', default => '_' );
# Returns messages corresponding to the user $uid.
# If $ref is set, returns only message corresponding to this reference.
sub get {
my ( $self, $uid, $ref ) = @_;
return () unless ($uid);
my $fns = $self->{fileNameSeparator};
my $identifier = &getIdentifier( $self, $uid, $ref );
opendir D, $self->{dirName};
my @msg = grep /^\d{8}${fns}${identifier}\S*\.$ext$/, readdir(D);
closedir D;
my $files;
foreach my $file (@msg) {
unless ( open F, $self->{dirName} . "/$file" ) {
$self->logger->error(
"Unable to read message $self->{dirName}/$file");
next;
}
$files->{$file} = join( '', <F> );
}
return $files;
}
## @method hashref getAll()
# Return all messages not notified.
# @return hashref where keys are internal reference and values are hashref with
# keys date, uid and ref.
sub getAll {
my $self = shift;
opendir D, $self->{dirName};
my @msg;
my $fns = $self->{fileNameSeparator};
@msg = grep /^\S*\.$ext$/, readdir(D);
my %h = map {
/^(\d{8})${fns}([^\s${fns}]+)${fns}([^\s${fns}]+)(?:${fns}([^\s${fns}]+))?\.$ext$/
? (
$_ => {
date => $1,
uid => $2,
ref => decode_base64($3),
condition => decode_base64( $4 // '' )
}
)
: ()
} @msg;
return \%h;
}
## @method boolean delete(string myref)
# Mark a message as done.
# @param $myref identifier returned by get() or getAll()
sub delete {
my ( $self, $myref ) = @_;
my $new = ( $myref =~ /(.*?)(?:\.$ext)$/ )[0] . '.done';
return rename( $self->{dirName} . "/$myref", $self->{dirName} . "/$new" );
}
## @method boolean purge(string myref)
# Purge message (really delete record)
# @param $myref identifier returned by get() or getAll()
# @return true if something was deleted
sub purge {
my ( $self, $myref ) = @_;
return unlink( $self->{dirName} . "/$myref" );
}
# Insert a new message
sub newMsg {
my ( $self, $date, $uid, $ref, $condition, $content ) = @_;
my $fns = $self->{fileNameSeparator};
$date =~ s/-//g;
return ( 0, "Bad date" ) unless ( $date =~ /^\d{8}/ );
my $filename =
$self->{dirName}
. "/${date}${fns}${uid}${fns}"
. encode_base64( $ref, '' );
$filename .= "${fns}" . encode_base64( $condition, '' ) if $condition;
$filename .= ".$ext";
return ( 0, 'This message still exists' ) if ( -e $filename );
my $old = ( $filename =~ /(.*?)(?:\.$ext)$/ )[0] . '.done';
return ( 0, 'This message has been done' ) if ( -e $old );
open my $F, ">$filename" or return ( 0, "Unable to create $filename ($!)" );
binmode($F);
print $F $content;
return ( 0, "Unable to close $filename ($!)" ) unless ( close $F );
return 1;
}
## @method hashref getDone()
# Returns a list of message that have been done
# @return hashref where keys are internal reference and values are hashref with
# keys notified, uid and ref.
sub getDone {
my ($self) = @_;
opendir D, $self->{dirName};
my @msg;
my $fns = $self->{fileNameSeparator};
@msg = grep /^\d{8}${fns}\S*\.done$/, readdir(D);
my $res;
foreach my $file (@msg) {
my ( $u, $r ) =
( $file =~
/^\d+${fns}([^${fns}]+)${fns}([^${fns}]+)${fns}?([^${fns}]+)\.done$/
);
die unless ( -f "$self->{dirName}/$file" );
my $time = ( stat("$self->{dirName}/$file") )[10];
$res->{$file} = {
'uid' => $u,
'ref' => decode_base64($r),
'notified' => $time,
};
}
return $res;
}
## @method string getIdentifier(string uid, string ref, string date)
# Get message identifier
# @param $uid uid
# @param $ref ref
# @param $date date
# @return the message identifier
sub getIdentifier {
my ( $self, $uid, $ref, $date ) = @_;
my $result;
# Special fix to manage purge from message explorer
return $date if $date;
my $fns = $self->{fileNameSeparator};
if ($date) {
$result .= $date . $fns;
}
$result .= $uid;
if ($ref) {
my $tmp = encode_base64( $ref, '' );
$result .= $fns . $tmp;
}
return $result;
}
1;
package Lemonldap::NG::Common::Messages::JSON;
use strict;
use Mouse;
use JSON qw(from_json to_json);
our $VERSION = '2.0.7';
sub newMessage {
my ( $self, $jsonString ) = @_;
my $json;
eval { $json = from_json( $jsonString, { allow_nonref => 1 } ) };
if ( my $err = $@ ) {
eval { $self->logger->error("Unable to decode JSON file: $err") };
return 0;
}
my @msgs;
$json = [$json] unless ( ref($json) eq 'ARRAY' );
foreach my $msg (@$json) {
my @data;
# Mandatory information
foreach (qw(date uid reference)) {
my $tmp;
unless ( $tmp = $msg->{$_} ) {
my $err = "Attribute $_ is missing";
$self->logger->error("$err");
return ( 0, "$err" );
}
if ( $self->get( $msg->{uid}, $msg->{reference} ) ) {
my $err = "A message already exists with reference "
. $msg->{reference};
$self->logger->error("$err");
return ( 0, "$err" );
}
push @data, $tmp;
}
push @data, ( $msg->{condition} // '' );
push @msgs, [ @data, $jsonString ];
}
my $count;
foreach (@msgs) {
$count++;
my ( $r, $err ) = $self->newMsg(@$_);
die "$err" unless ($r);
}
return $count;
}
sub deleteMessage {
my ( $self, $uid, $myref ) = @_;
my @data;
# Check input parameters
unless ( $uid and $myref ) {
$self->userLogger->error(
'REST service "delete message" called without all parameters');
return 0;
}
$self->logger->debug(
"REST service deleteMessage called for uid $uid and reference $myref"
);
# Get messages
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 $json = from_json( $user->{$ref}, { allow_nonref => 1 } );
$json = [$json] unless ( ref($json) eq 'ARRAY' );
# Browse message in file
foreach my $msg (@$json) {
# Get message's data
if ( $msg->{reference} eq $myref ) {
push @data, $ref;
}
# Delete the message (really)
foreach (@data) {
if ( $self->purge( $_, 1 ) ) {
$self->logger->debug("Message $_ was removed.");
$count++;
}
}
}
}
return $count;
}
1;
This diff is collapsed.
......@@ -17,7 +17,7 @@ use JSON;
use Lemonldap::NG::Common::Conf::Constants;
use Lemonldap::NG::Common::PSGI::Constants;
our $VERSION = '2.0.6';
our $VERSION = '2.0.7';
extends 'Lemonldap::NG::Common::Conf::AccessLib',
'Lemonldap::NG::Handler::PSGI::Router';
......@@ -52,7 +52,7 @@ sub init {
return 0;
}
$self->{enabledModules} ||= "conf, sessions, notifications, 2ndFA, viewer";
$self->{enabledModules} ||= "conf, sessions, notifications, 2ndFA, messages, viewer";
my @links;
my @enabledModules =
map { push @links, $_; "Lemonldap::NG::Manager::" . ucfirst($_) }
......@@ -101,6 +101,7 @@ sub init {
'conf' => 'cog',
'sessions' => 'duplicate',
'notifications' => 'bell',
'messages' => 'bell',
'2ndFA' => 'wrench',
'viewer' => 'eye-open',
};
......
This diff is collapsed.
###
# LemonLDAP::NG Messages Explorer client
###
scheme = [
(v) ->
"groupBy=substr(uid,1)"
(v) ->
"uid=#{v}*&groupBy=uid"
(v) ->
"uid=#{v}"
]
# Session menu
menu =
actives: [
title: 'markAsDone'
icon: 'check'
]
done: [
title: 'deleteMessage'
icon: 'trash'
]
new: [
title: 'save'
icon: 'save'
]
home: []
# AngularJS application
llapp = angular.module 'llngMessagesExplorer', [ 'ui.tree', 'ui.bootstrap', 'llApp' ]
# Main controller
llapp.controller 'MessagesExplorerCtrl', [ '$scope', '$translator', '$location', '$q', '$http', '$uibModal', ($scope, $translator, $location, $q, $http, $uibModal) ->
$scope.links = links
$scope.menulinks = menulinks
$scope.staticPrefix = staticPrefix
$scope.scriptname = scriptname
$scope.formPrefix = formPrefix
$scope.availableLanguages = availableLanguages
$scope.waiting = true
$scope.showM = false
$scope.showT = true
$scope.showForm = false
$scope.data = []
$scope.form = {}
$scope.formPost = {}
$scope.currentScope = null
$scope.currentMessage = null
$scope.menu = menu
# Import translation functions
$scope.translateP = $translator.translateP
$scope.translate = $translator.translate
$scope.translateTitle = (node) ->
$translator.translateField node, 'title'
# Handler menu items
$scope.menuClick = (button) ->
if button.popup
window.open button.popup
else
button.action or= button.title
switch typeof button.action
when 'function'
button.action $scope.currentNode, $scope
when 'string'
$scope[button.action]()
else
console.log typeof button.action
$scope.showM = false
# Message management
$scope.markAsDone = ->
$scope.waiting = true
$http.put("#{scriptname}messages/#{$scope.type}/#{$scope.currentMessage.uid}_#{$scope.currentMessage.reference}", {done:1}).then (response) ->
$scope.currentMessage = null
$scope.currentScope.remove()