Commit c1212147 authored by Christophe Maudoux's avatar Christophe Maudoux

Backport all v2.1 improvements

parent 33a7a1b1
......@@ -193,6 +193,7 @@ t/05-rest-api.t
t/06-rest-api.t
t/07-utf8.t
t/10-save-unchanged-conf.t
t/11-save-changed-conf-with-confirmation.t
t/12-save-changed-conf.t
t/14-bad-changes-in-conf.t
t/15-combination.t
......@@ -206,6 +207,7 @@ t/99-pod.t
t/conf/lmConf-1.json
t/jsonfiles/01-base-tree.json
t/jsonfiles/02-base-tree-all-nodes-opened.json
t/jsonfiles/11-modified-with-confirmation.json
t/jsonfiles/12-modified.json
t/jsonfiles/14-bad.json
t/jsonfiles/15-combination.json
......
......@@ -36,37 +36,40 @@ sub addRoutes {
# HTML template
$self->addRoute( 'manager.html', undef, ['GET'] )
# READ
# Special keys
->addRoute(
# READ
# Special keys
->addRoute(
confs => {
':cfgNum' => [
qw(virtualHosts samlIDPMetaDataNodes samlSPMetaDataNodes
applicationList oidcOPMetaDataNodes oidcRPMetaDataNodes
casSrvMetaDataNodes casAppMetaDataNodes
authChoiceModules grantSessionRules combModules
openIdIDPList)
applicationList oidcOPMetaDataNodes oidcRPMetaDataNodes
casSrvMetaDataNodes casAppMetaDataNodes
authChoiceModules grantSessionRules combModules
openIdIDPList)
]
},
['GET']
)
)
# Other keys
->addRoute( confs => { ':cfgNum' => { '*' => 'getKey' } }, ['GET'] )
# Other keys
->addRoute( confs => { ':cfgNum' => { '*' => 'getKey' } }, ['GET'] )
# New key and conf save
->addRoute(
confs =>
{ newRSAKey => 'newRSAKey', raw => 'newRawConf', '*' => 'newConf' },
# New key and conf save
->addRoute(
confs => {
newRSAKey => 'newRSAKey',
raw => 'newRawConf',
'*' => 'newConf'
},
['POST']
)
)
# Difference between confs
->addRoute( diff => { ':conf1' => { ':conf2' => 'diff' } } )
->addRoute( 'diff.html', undef, ['GET'] )
# Difference between confs
->addRoute( diff => { ':conf1' => { ':conf2' => 'diff' } } )
->addRoute( 'diff.html', undef, ['GET'] )
# Url loader
->addRoute( 'prx', undef, ['POST'] );
# Url loader
->addRoute( 'prx', undef, ['POST'] );
}
# 35 - New RSA key pair on demand
......@@ -82,7 +85,7 @@ sub addRoutes {
sub newRSAKey {
my ( $self, $req, @others ) = @_;
return $self->sendError( $req, 'There is no subkey for "newRSAKey"', 400 )
if (@others);
if (@others);
my $query = $req->jsonBodyToObj;
my $rsa = Crypt::OpenSSL::RSA->generate_key(2048);
my $keys = {
......@@ -124,12 +127,12 @@ sub newRSAKey {
sub prx {
my ( $self, $req, @others ) = @_;
return $self->sendError( $req, 'There is no subkey for "prx"', 400 )
if (@others);
if (@others);
my $query = $req->jsonBodyToObj;
return $self->sendError( $req, 'Missing parameter', 400 )
unless ( $query->{url} );
unless ( $query->{url} );
return $self->sendError( $req, 'Bad parameter', 400 )
unless ( $query->{url} =~ m#^(?:f|ht)tps?://\w# );
unless ( $query->{url} =~ m#^(?:f|ht)tps?://\w# );
$self->ua->timeout(10);
my $response = $self->ua->get( $query->{url} );
......@@ -137,11 +140,12 @@ sub prx {
return $self->sendError( $req,
$response->code . " (" . $response->message . ")", 400 );
}
unless ( $response->header('Content-Type') =~
m#^(?:application/json|(?:application|text)/.*xml).*$# )
unless ( $response->header('Content-Type')
=~ m#^(?:application/json|(?:application|text)/.*xml).*$# )
{
return $self->sendError( $req,
'Content refused for security reason (neither XML or JSON)', 400 );
'Content refused for security reason (neither XML or JSON)',
400 );
}
return $self->sendJSONresponse( $req, { content => $response->content } );
}
......@@ -185,7 +189,7 @@ sub getConfByNum {
sub newConf {
my ( $self, $req, @other ) = @_;
return $self->sendError( $req, 'There is no subkey for "newConf"', 400 )
if (@other);
if (@other);
# Body must be json
my $new = $req->jsonBodyToObj;
......@@ -203,9 +207,9 @@ sub newConf {
return $self->sendError(
$req,
"Configuration "
. $req->params('cfgNum')
. " not available "
. $Lemonldap::NG::Common::Conf::msg,
. $req->params('cfgNum')
. " not available "
. $Lemonldap::NG::Common::Conf::msg,
400
);
}
......@@ -228,10 +232,17 @@ sub newConf {
# "message" fields: note that words enclosed by "__" (__word__) will be
# translated
$res->{message} = $parser->{message};
foreach my $t (qw(errors warnings changes)) {
$res->{details}->{ '__' . $t . '__' } = $parser->$t
if ( @{ $parser->$t } );
$res->{details}->{'__errors__'} = $parser->{errors}
if ( @{ $parser->{errors} } );
unless ( @{ $parser->{errors} } ) {
$res->{details}->{'__needConfirmation__'}
= $parser->{needConfirmation}
if ( @{ $parser->{needConfirmation} } && !$req->params('force') );
$res->{message} = $parser->{message};
foreach my $t (qw(warnings changes)) {
$res->{details}->{ '__' . $t . '__' } = $parser->$t
if ( @{ $parser->$t } );
}
}
if ( $res->{result} ) {
if ( $self->{demoMode} ) {
......@@ -240,7 +251,9 @@ sub newConf {
else {
my %args;
$args{force} = 1 if ( $req->params('force') );
my $s = $self->confAcc->saveConf( $parser->newConf, %args );
my $s = CONFIG_WAS_CHANGED;
$s = $self->confAcc->saveConf( $parser->newConf, %args )
unless ( @{ $parser->{needConfirmation} } && !$args{force} );
if ( $s > 0 ) {
$self->userLogger->notice(
'User ' . $self->userId($req) . " has stored conf $s" );
......@@ -248,18 +261,19 @@ sub newConf {
$res->{cfgNum} = $s;
if ( my $status = $self->applyConf( $parser->newConf ) ) {
push @{ $res->{details}->{__applyResult__} },
{ message => "$_: $status->{$_}" }
foreach ( keys %$status );
{ message => "$_: $status->{$_}" }
foreach ( keys %$status );
}
}
else {
$self->userLogger->notice(
'Saving attempt rejected, asking for confirmation to '
. $self->userId($req) );
. $self->userId($req) );
$res->{result} = 0;
if ( $s == CONFIG_WAS_CHANGED ) {
$res->{needConfirm} = 1;
$res->{message} .= '__needConfirmation__';
$res->{message} .= '__needConfirmation__'
unless @{ $parser->{needConfirmation} };
}
else {
$res->{message} = $Lemonldap::NG::Common::Conf::msg;
......@@ -278,7 +292,7 @@ sub newConf {
sub newRawConf {
my ( $self, $req, @other ) = @_;
return $self->sendError( $req, 'There is no subkey for "newConf"', 400 )
if (@other);
if (@other);
# Body must be json
my $new = $req->jsonBodyToObj;
......@@ -303,7 +317,7 @@ sub newRawConf {
else {
$self->userLogger->notice(
'Raw saving attempt rejected, asking for confirmation to '
. $self->userId($req) );
. $self->userId($req) );
$res->{result} = 0;
$res->{needConfirm} = 1 if ( $s == CONFIG_WAS_CHANGED );
$res->{message} .= '__needConfirmation__';
......@@ -325,8 +339,8 @@ sub applyConf {
$self->api->checkConf();
# Get apply section values
my %reloadUrls =
%{ $self->confAcc->getLocalConf( APPLYSECTION, undef, 0 ) };
my %reloadUrls
= %{ $self->confAcc->getLocalConf( APPLYSECTION, undef, 0 ) };
if ( !%reloadUrls && $newConf->{reloadUrls} ) {
%reloadUrls = %{ $newConf->{reloadUrls} };
}
......@@ -342,10 +356,10 @@ sub applyConf {
my $targetUrl = $url->scheme . "://" . $host;
$targetUrl .= ":" . $url->port if defined( $url->port );
$targetUrl .= $url->full_path;
$r =
HTTP::Request->new( 'GET', $targetUrl,
$r = HTTP::Request->new( 'GET', $targetUrl,
HTTP::Headers->new( Host => $url->host ) );
if ( defined $url->userinfo && $url->userinfo =~ /^([^:]+):(.*)$/ )
if ( defined $url->userinfo
&& $url->userinfo =~ /^([^:]+):(.*)$/ )
{
$r->authorization_basic( $1, $2 );
}
......@@ -353,12 +367,14 @@ sub applyConf {
my $response = $self->ua->request($r);
if ( $response->code != 200 ) {
$status->{$host} =
"Error " . $response->code . " (" . $response->message . ")";
$status->{$host}
= "Error "
. $response->code . " ("
. $response->message . ")";
$self->logger->error( "Apply configuration for $host: error "
. $response->code . " ("
. $response->message
. ")" );
. $response->code . " ("
. $response->message
. ")" );
}
else {
$status->{$host} = "OK";
......@@ -372,14 +388,14 @@ sub applyConf {
sub diff {
my ( $self, $req, @path ) = @_;
return $self->sendError( $req, 'to many arguments in path info', 400 )
if (@path);
my @cfgNum =
( scalar( $req->param('conf1') ), scalar( $req->param('conf2') ) );
if (@path);
my @cfgNum
= ( scalar( $req->param('conf1') ), scalar( $req->param('conf2') ) );
my @conf;
$self->logger->debug(" Loading confs");
# Load the 2 configurations
for ( my $i = 0 ; $i < 2 ; $i++ ) {
for ( my $i = 0; $i < 2; $i++ ) {
if ( %{ $self->currentConf }
and $cfgNum[$i] == $self->currentConf->{cfgNum} )
{
......@@ -390,7 +406,7 @@ sub diff {
{ cfgNum => $cfgNum[$i], raw => 1, noCache => 1 } );
return $self->sendError(
$req,
"Configuration $cfgNum[$i] not available $Lemonldap::NG::Common::Conf::msg",
"Configuration $cfgNum[$i] not available $Lemonldap::NG::Common::Conf::msg",
400
) unless ( $conf[$i] );
}
......@@ -398,8 +414,7 @@ sub diff {
require Lemonldap::NG::Manager::Conf::Diff;
return $self->sendJSONresponse(
$req,
[
$self->Lemonldap::NG::Manager::Conf::Diff::diff(
[ $self->Lemonldap::NG::Manager::Conf::Diff::diff(
$conf[0], $conf[1]
)
]
......
......@@ -46,7 +46,8 @@ has warnings => (
hdebug( 'warnings contains', $_[0]->{warnings} );
}
);
has changes => ( is => 'rw', isa => 'ArrayRef', default => sub { return [] } );
has changes =>
( is => 'rw', isa => 'ArrayRef', default => sub { return [] } );
has message => (
is => 'rw',
isa => 'Str',
......@@ -55,10 +56,10 @@ has message => (
hdebug( "Message becomes " . $_[0]->{message} );
}
);
has needConfirmation =>
( is => 'rw', isa => 'ArrayRef', default => sub { return [] } );
# Booleans
has needConfirm =>
( is => 'rw', isa => 'ArrayRef', default => sub { return [] } );
has confChanged => (
is => 'rw',
isa => 'Bool',
......@@ -69,7 +70,7 @@ has confChanged => (
);
# Properties required during build
has refConf => ( is => 'ro', isa => 'HashRef', required => 1 );
has refConf => ( is => 'ro', isa => 'HashRef', required => 1 );
has req => ( is => 'ro', required => 1 );
has newConf => ( is => 'rw', isa => 'HashRef' );
has tree => ( is => 'rw', isa => 'ArrayRef' );
......@@ -124,14 +125,15 @@ sub scanTree {
# Set cfgNum to ref cfgNum (will be changed when saving), set other
# metadata and set a value to the key if empty
$self->newConf->{cfgNum} = $self->req->params('cfgNum');
$self->newConf->{cfgAuthor} =
$self->req->userData->{ $Lemonldap::NG::Handler::Main::tsv->{whatToTrace}
|| '_whatToTrace' } // "anonymous";
$self->newConf->{cfgAuthor}
= $self->req->userData
->{ $Lemonldap::NG::Handler::Main::tsv->{whatToTrace}
|| '_whatToTrace' } // "anonymous";
$self->newConf->{cfgAuthorIP} = $self->req->address;
$self->newConf->{cfgDate} = time;
$self->newConf->{cfgVersion} = $VERSION;
$self->newConf->{key} ||=
join( '', map { chr( int( rand(94) ) + 33 ) } ( 1 .. 16 ) );
$self->newConf->{key}
||= join( '', map { chr( int( rand(94) ) + 33 ) } ( 1 .. 16 ) );
return 1;
}
......@@ -158,7 +160,7 @@ sub _scanNodes {
hdebug("Looking to $name");
# subnode
my $subNodes = $leaf->{nodes} // $leaf->{_nodes};
my $subNodes = $leaf->{nodes} // $leaf->{_nodes};
my $subNodesCond = $leaf->{nodes_cond} // $leaf->{_nodes_cond};
##################################
......@@ -191,7 +193,7 @@ sub _scanNodes {
$self->confChanged(1);
foreach my $deletedHost (@old) {
push @{ $self->changes },
{ key => $leaf->{id}, old => $deletedHost };
{ key => $leaf->{id}, old => $deletedHost };
}
}
next;
......@@ -208,7 +210,7 @@ sub _scanNodes {
hdebug(" $host becomes $newNames{$host}");
$self->confChanged(1);
push @{ $self->changes },
{ key => $base, old => $host, new => $newNames{$host} };
{ key => $base, old => $host, new => $newNames{$host} };
}
$self->_scanNodes($subNodes);
......@@ -216,15 +218,15 @@ sub _scanNodes {
}
# Other sub levels
elsif ( $leaf->{id} =~
/^($specialNodeKeys)\/([^\/]+)\/([^\/]+)(?:\/(.*))?$/io )
elsif ( $leaf->{id}
=~ /^($specialNodeKeys)\/([^\/]+)\/([^\/]+)(?:\/(.*))?$/io )
{
my ( $base, $key, $oldName, $target, $h ) =
( $1, $newNames{$2}, $2, $3, $4 );
my ( $base, $key, $oldName, $target, $h )
= ( $1, $newNames{$2}, $2, $3, $4 );
hdebug(
"Special node chield subnode detected $leaf->{id}",
" base $base, key $key, target $target, h "
. ( $h ? $h : 'undef' )
. ( $h ? $h : 'undef' )
);
# VirtualHosts
......@@ -233,18 +235,18 @@ sub _scanNodes {
if ( $target =~ /^(?:locationRules|exportedHeaders|post)$/ ) {
if ( $leaf->{cnodes} ) {
hdebug(' unopened subnode');
$self->newConf->{$target}->{$key} =
$self->refConf->{$target}->{$oldName} // {};
$self->newConf->{$target}->{$key}
= $self->refConf->{$target}->{$oldName} // {};
}
elsif ($h) {
hdebug(' 4 levels');
if ( $target eq 'locationRules' ) {
hdebug(' locationRules');
my $k =
$leaf->{comment}
? "(?#$leaf->{comment})$leaf->{re}"
: $leaf->{re};
my $k
= $leaf->{comment}
? "(?#$leaf->{comment})$leaf->{re}"
: $leaf->{re};
$self->set( $target, $key, $k, $leaf->{data} );
}
else {
......@@ -260,7 +262,7 @@ sub _scanNodes {
if ( ref $subNodes ) {
hdebug(' has subnodes');
$self->_scanNodes($subNodes)
or return 0;
or return 0;
}
if ( exists $self->refConf->{$target}->{$key}
and %{ $self->refConf->{$target}->{$key} } )
......@@ -274,10 +276,10 @@ sub _scanNodes {
hdebug(' missing value in old conf');
$self->confChanged(1);
push @{ $self->changes },
{
{
key => "$target, $key",
old => $k,
};
};
}
}
}
......@@ -287,7 +289,7 @@ sub _scanNodes {
hdebug(" '$key' has values");
$self->confChanged(1);
push @{ $self->changes },
{ key => "$target", new => $key };
{ key => "$target", new => $key };
}
}
}
......@@ -297,7 +299,7 @@ sub _scanNodes {
}
else {
push @{ $self->errors },
{ message => "Unknown vhost key $target" };
{ message => "Unknown vhost key $target" };
return 0;
}
next;
......@@ -306,16 +308,18 @@ sub _scanNodes {
# SAML
elsif ( $base =~ /^saml(?:S|ID)PMetaDataNodes$/ ) {
hdebug('SAML');
if ( defined $leaf->{data} and ref( $leaf->{data} ) eq 'ARRAY' )
if ( defined $leaf->{data}
and ref( $leaf->{data} ) eq 'ARRAY' )
{
hdebug(" SAML data is an array, serializing");
$leaf->{data} = join ';', @{ $leaf->{data} };
}
if ( $target =~ /^saml(?:S|ID)PMetaDataExportedAttributes$/ ) {
if ( $target =~ /^saml(?:S|ID)PMetaDataExportedAttributes$/ )
{
if ( $leaf->{cnodes} ) {
hdebug(" $target: unopened node");
$self->newConf->{$target}->{$key} =
$self->refConf->{$target}->{$oldName} // {};
$self->newConf->{$target}->{$key}
= $self->refConf->{$target}->{$oldName} // {};
}
elsif ($h) {
hdebug(" $target: opened node");
......@@ -330,15 +334,17 @@ sub _scanNodes {
}
elsif ( $target =~ /^saml(?:S|ID)PMetaDataXML$/ ) {
hdebug(" $target");
$self->set( $target, [ $oldName, $key ],
$target, $leaf->{data} );
$self->set(
$target, [ $oldName, $key ],
$target, $leaf->{data}
);
}
elsif ( $target =~ /^saml(?:ID|S)PMetaDataOptions/ ) {
my $optKey = $&;
hdebug(" $base sub key: $target");
if ( $target =~
/^(?:$samlIDPMetaDataNodeKeys|$samlSPMetaDataNodeKeys)/o
)
if ( $target
=~ /^(?:$samlIDPMetaDataNodeKeys|$samlSPMetaDataNodeKeys)/o
)
{
$self->set(
$optKey, [ $oldName, $key ],
......@@ -347,13 +353,14 @@ sub _scanNodes {
}
else {
push @{ $self->errors },
{ message => "Unknown SAML metadata option $target" };
{ message =>
"Unknown SAML metadata option $target" };
return 0;
}
}
else {
push @{ $self->errors },
{ message => "Unknown SAML key $target" };
{ message => "Unknown SAML key $target" };
return 0;
}
next;
......@@ -365,7 +372,8 @@ sub _scanNodes {
if ( $target =~ /^oidc(?:O|R)PMetaDataOptions$/ ) {
hdebug(" $target: looking for subnodes");
$self->_scanNodes($subNodes);
$self->set( $target, $key, $leaf->{title}, $leaf->{data} );
$self->set( $target, $key, $leaf->{title},
$leaf->{data} );
}
elsif ( $target =~ /^oidcOPMetaData(?:JSON|JWKS)$/ ) {
hdebug(" $target");
......@@ -375,8 +383,8 @@ sub _scanNodes {
hdebug(" $target");
if ( $leaf->{cnodes} ) {
hdebug(' unopened');
$self->newConf->{$target}->{$key} =
$self->refConf->{$target}->{$oldName} // {};
$self->newConf->{$target}->{$key}
= $self->refConf->{$target}->{$oldName} // {};
}
elsif ($h) {
hdebug(' opened');
......@@ -394,8 +402,8 @@ sub _scanNodes {
if ( $target eq 'oidcRPMetaDataOptionsExtraClaims' ) {
if ( $leaf->{cnodes} ) {
hdebug(' unopened');
$self->newConf->{$target}->{$key} =
$self->refConf->{$target}->{$oldName} // {};
$self->newConf->{$target}->{$key}
= $self->refConf->{$target}->{$oldName} // {};
}
elsif ($h) {
hdebug(' opened');
......@@ -407,9 +415,9 @@ sub _scanNodes {
$self->_scanNodes($subNodes);
}
}
elsif ( $target =~
/^(?:$oidcOPMetaDataNodeKeys|$oidcRPMetaDataNodeKeys)/o
)
elsif ( $target
=~ /^(?:$oidcOPMetaDataNodeKeys|$oidcRPMetaDataNodeKeys)/o
)
{
$self->set(
$optKey, [ $oldName, $key ],
......@@ -418,13 +426,14 @@ sub _scanNodes {
}
else {
push @{ $self->errors },
{ message => "Unknown OIDC metadata option $target" };
{ message =>
"Unknown OIDC metadata option $target" };
return 0;
}
}
else {
push @{ $self->errors },
{ message => "Unknown OIDC key $target" };
{ message => "Unknown OIDC key $target" };
return 0;
}
next;
......@@ -437,14 +446,15 @@ sub _scanNodes {
if ( $target =~ /^cas(?:App|Srv)MetaDataOptions$/ ) {
hdebug(" $target: looking for subnodes");
$self->_scanNodes($subNodes);
$self->set( $target, $key, $leaf->{title}, $leaf->{data} );
$self->set( $target, $key, $leaf->{title},
$leaf->{data} );
}
elsif ( $target =~ /^cas(?:App|Srv)MetaDataExportedVars$/ ) {
hdebug(" $target");
if ( $leaf->{cnodes} ) {
hdebug(' unopened');
$self->newConf->{$target}->{$key} =
$self->refConf->{$target}->{$oldName} // {};
$self->newConf->{$target}->{$key}
= $self->refConf->{$target}->{$oldName} // {};
}
elsif ($h) {
hdebug(' opened');
......@@ -462,8 +472,8 @@ sub _scanNodes {
if ( $target eq 'casSrvMetaDataOptionsProxiedServices' ) {
if ( $leaf->{cnodes} ) {
hdebug(' unopened');
$self->newConf->{$target}->{$key} =
$self->refConf->{$target}->{$oldName} // {};
$self->newConf->{$target}->{$key}
= $self->refConf->{$target}->{$oldName} // {};
}
elsif ($h) {
hdebug(' opened');
......@@ -475,9 +485,9 @@ sub _scanNodes {
$self->_scanNodes($subNodes);
}
}
elsif ( $target =~
/^(?:$casSrvMetaDataNodeKeys|$casAppMetaDataNodeKeys)/o
)
elsif ( $target
=~ /^(?:$casSrvMetaDataNodeKeys|$casAppMetaDataNodeKeys)/o
)
{
$self->set(
$optKey, [ $oldName, $key ],
......@@ -486,20 +496,21 @@ sub _scanNodes {
}
else {
push @{ $self->errors },
{ message => "Unknown CAS metadata option $target" };
{ message =>
"Unknown CAS metadata option $target" };
return 0;
}
}
else {
push @{ $self->errors },
{ message => "Unknown CAS option $target" };
{ message => "Unknown CAS option $target" };
return 0;
}
next;
}
else {
push @{ $self->errors },
{ message => "Fatal: unknown special sub node $base" };
{ message => "Fatal: unknown special sub node $base" };
return 0;
}
}
......@@ -513,35 +524,39 @@ sub _scanNodes {
hdebug( $leaf->{title} );
if ( $leaf->{cnodes} ) {
hdebug(' unopened');
$self->newConf->{applicationList} =
$self->refConf->{applicationList} // {};
$self->newConf->{applicationList}
= $self->refConf->{applicationList} // {};
}
else {
$self->_scanNodes($subNodes) or return 0;
# Check for deleted
my @listCatRef =
map { $self->refConf->{applicationList}->{$_}->{catname} }
keys %{ $self->refConf->{applicationList} };
my @listCatNew =
map { $self->newConf->{applicationList}->{$_}->{catname} }
keys(
%{
ref $self->newConf->{applicationList}
my @listCatRef
= map { $self->refConf->{applicationList}->{$_}->{catname} }
keys %{ $self->refConf->{applicationList} };
my @listCatNew
= map { $self->newConf->{applicationList}->{$_}->{catname} }
keys(
%{ ref $self->newConf->{applicationList}
? $self->newConf->{applicationList}
: {}
}
);
for ( my $i = 0 ; $i < @listCatNew ; $i++ ) {
);
@listCatRef = sort @listCatRef;
@listCatNew = sort @listCatNew;
hdebug( '# @listCatRef : ' . \@listCatRef );
hdebug( '# @listCatNew : ' . \@listCatNew );
for ( my $i = 0; $i < @listCatNew; $i++ ) {
if ( not( defined $listCatRef[$i] )
or $listCatRef[$i] ne $listCatNew[$i] )
{
push @{ $self->changes },
{
{
key => $leaf->{id},