package Lemonldap::NG::Manager::Build; use strict; use utf8; use Mouse; use Lemonldap::NG::Manager::Build::Attributes; use Lemonldap::NG::Manager::Build::Tree; use Lemonldap::NG::Manager::Build::CTrees; use Lemonldap::NG::Manager::Build::PortalConstants; use Lemonldap::NG::Manager::Conf::Zero; use Data::Dumper; use Regexp::Common 'URI'; use Regexp::Assemble; use JSON; use Getopt::Std; use IO::String; has structFile => ( isa => 'Str', is => 'ro', required => 1 ); has confTreeFile => ( isa => 'Str', is => 'ro', required => 1 ); has managerConstantsFile => ( isa => 'Str', is => 'ro', required => 1 ); has managerAttributesFile => ( isa => 'Str', is => 'ro', required => 1 ); has defaultValuesFile => ( isa => 'Str', is => 'ro', required => 1 ); has confConstantsFile => ( isa => 'Str', is => 'ro', required => 1 ); has firstLmConfFile => ( isa => 'Str', is => 'ro', required => 1 ); has reverseTreeFile => ( isa => 'Str', is => 'ro', required => 1 ); has portalConstantsFile => ( isa => 'Str', is => 'ro', required => 1 ); has handlerStatusConstantsFile => ( isa => 'Str', is => 'ro', required => 1 ); my @managerAttrKeys = qw(keyTest keyMsgFail select type test msgFail default); my $format = 'Creating %-69s: '; my $reIgnoreKeys = qr/^$/; my $module = __PACKAGE__; my @angularScopeVars; my @bool; my @arrayParam; my @cnodesKeys; my %cnodesRe; my @ignoreKeys; my $ignoreKeys; my $mainTree; my @sessionTypes; my @simpleHashKeys; my @doubleHashKeys; my $authParameters; my $issuerParameters; my $samlServiceParameters; my $oidcServiceParameters; my $casServiceParameters = []; my $defaultValues; my $attributes = Lemonldap::NG::Manager::Build::Attributes::attributes(); my $jsonEnc = JSON->new()->allow_nonref; $jsonEnc->canonical(1); $Data::Dumper::Sortkeys = sub { my ($hash) = @_; return [ ( defined $hash->{id} ? ('id') : () ), ( defined $hash->{title} ? ( 'title', ) : () ), ( grep { /^(?:id|title)$/ ? 0 : 1 } sort { return 1 if ( $a =~ /node/ and $b !~ /node/ ); return -1 if ( $b =~ /node/ ); lc($a) cmp lc($b); } keys %$hash ) ]; }; $Data::Dumper::Deparse = 1; $Data::Dumper::Deepcopy = 1; sub run { my $self = shift; $self = $module->new(@_) unless ref $self; # 1. confTree.js printf STDERR $format, $self->confTreeFile; $mainTree = Lemonldap::NG::Manager::Build::CTrees::cTrees(); my $script = 'function templates(tpl,key) { var ind; var scalarTemplate = function(r) { return { "id": tpl+"s/"+(ind++), "title": r, "get": tpl+"s/"+key+"/"+r }; }; switch(tpl){ '; my $reverseScanResult = $self->reverseScan( Lemonldap::NG::Manager::Build::Tree::tree(), '', {} ); # To build confTree.js, each special node is scanned from # Lemonldap::NG::Manager::Build::CTrees foreach my $node ( sort keys %$mainTree ) { @cnodesKeys = (); my $jsonTree = []; $self->scanTree( $mainTree->{$node}, $jsonTree, '__KEY__', '' ); $jsonEnc->pretty(1); my $tmp = $jsonEnc->encode($jsonTree); $tmp =~ s!"__KEY__!tpl+"s/"+key+"/"+"!mg; $tmp =~ s/"(true|false)"/$1/sg; $tmp =~ s/:\s*"(\d+)"\s*(["\}])/:$1$2/sg; $script .= " case '$node': return $tmp; "; # Second step, Manager/Constants.pm file will contain data issued from # this scan my $ra = Regexp::Assemble->new; # Build $oidcOPMetaDataNodeKeys, $samlSPMetaDataNodeKeys,... foreach my $r (@cnodesKeys) { $ra->add($r); } $cnodesRe{$node} = $ra->as_string; push @ignoreKeys, $node; } $script .= " default:\n return [];\n }\n}"; open F, ">", $self->confTreeFile or die $!; print F $script; close F; print STDERR "done\n"; my $ra = Regexp::Assemble->new; foreach my $re (@ignoreKeys) { $ra->add($re); } $ignoreKeys = $ra->as_string; $reIgnoreKeys = $ra->re; # Reinitialize $defaultValues $defaultValues = {}; # 2. struct.json printf STDERR $format, $self->structFile; $mainTree = Lemonldap::NG::Manager::Build::Tree::tree(); my $jsonTree = []; $self->scanTree( $mainTree, $jsonTree, '', '' ); $script = "\n\nfunction setScopeVars(scope) {\n"; foreach my $v (@angularScopeVars) { $script .= " scope.$v->[0] = scope$v->[1];\n scope.getKey(scope.$v->[0]);\n"; } $script .= "}"; open F, ">>", $self->confTreeFile || die $!; print F $script; close F; open F, ">", $self->structFile || die $!; $jsonEnc->pretty(0); my $tmp = $jsonEnc->encode($jsonTree); $tmp =~ s/"(true|false)"/$1/sg; $tmp =~ s/:\s*"(\d+)"\s*(["\}])/:$1$2/sg; print F $tmp; close F; print STDERR "done\n"; $tmp = undef; printf STDERR $format, $self->managerConstantsFile; open F, ">", $self->managerConstantsFile or die($!); my $exportedVars = '$' . join( 'Keys $', 'simpleHash', 'doubleHash', 'specialNode', sort keys %cnodesRe ) . 'Keys $specialNodeHash $authParameters $issuerParameters $samlServiceParameters $oidcServiceParameters $casServiceParameters'; print F < [qw($exportedVars)] ); our \@EXPORT_OK = ( \@{ \$EXPORT_TAGS{'all'} } ); our \@EXPORT = ( \@{ \$EXPORT_TAGS{'all'} } ); our \$specialNodeHash = { virtualHosts => [qw(exportedHeaders locationRules post vhostOptions)], samlIDPMetaDataNodes => [qw(samlIDPMetaDataXML samlIDPMetaDataExportedAttributes samlIDPMetaDataOptions)], samlSPMetaDataNodes => [qw(samlSPMetaDataXML samlSPMetaDataExportedAttributes samlSPMetaDataOptions samlSPMetaDataMacros)], oidcOPMetaDataNodes => [qw(oidcOPMetaDataJSON oidcOPMetaDataJWKS oidcOPMetaDataOptions oidcOPMetaDataExportedVars)], oidcRPMetaDataNodes => [qw(oidcRPMetaDataOptions oidcRPMetaDataExportedVars oidcRPMetaDataOptionsExtraClaims oidcRPMetaDataMacros)], casSrvMetaDataNodes => [qw(casSrvMetaDataOptions casSrvMetaDataExportedVars)], casAppMetaDataNodes => [qw(casAppMetaDataOptions casAppMetaDataExportedVars casAppMetaDataMacros)], }; EOF # Reinitialize $attributes $attributes = Lemonldap::NG::Manager::Build::Attributes::attributes(); $ra = Regexp::Assemble->new; foreach (@doubleHashKeys) { $ra->add($_); } print F "our \$doubleHashKeys = '" . $ra->as_string . "';\n"; $ra = Regexp::Assemble->new; foreach (@simpleHashKeys) { $ra->add($_); } print F "our \$simpleHashKeys = '" . $ra->as_string . "';\n" . "our \$specialNodeKeys = '${ignoreKeys}s';\n"; foreach ( sort keys %cnodesRe ) { print F "our \$${_}Keys = '$cnodesRe{$_}';\n"; } print F "\n"; foreach (qw(authParameters issuerParameters)) { $tmp = "our \$$_ = {\n"; no strict 'refs'; foreach my $k ( sort keys %$$_ ) { my $v = $$_->{$k}; $tmp .= " $k => [qw(" . join( ' ', @$v ) . ")],\n"; } print F "$tmp};\n"; } foreach (qw(samlServiceParameters oidcServiceParameters)) { no strict 'refs'; $tmp = "our \$$_ = [qw(" . join( ' ', @$$_ ) . ")];\n"; print F "$tmp"; } print F "\n1;\n"; close F; print STDERR "done\n"; printf STDERR $format, $self->defaultValuesFile; $defaultValues->{locationRules} = $attributes->{locationRules}->{default}; foreach ( keys %$attributes ) { if ( not /(?:MetaData|vhost)/ and $attributes->{$_}->{default} and not $reverseScanResult->{$_} ) { $defaultValues->{$_} = $attributes->{$_}->{default}; } } my $defaultAttr = mydump( $defaultValues, 'defaultValues' ); $defaultAttr = "# This file is generated by $module. Don't modify it by hand package Lemonldap::NG::Common::Conf::DefaultValues; our \$VERSION = '$Lemonldap::NG::Manager::Build::Attributes::VERSION'; $defaultAttr} 1; "; my $dst; eval { require Perl::Tidy; Perl::Tidy::perltidy( source => IO::String->new($defaultAttr), destination => \$dst ); }; $dst = $defaultAttr if ($@); open( F, ">", $self->defaultValuesFile ) or die($!); print F $dst; close F; print STDERR "done\n"; printf STDERR $format, $self->confConstantsFile; $ra = Regexp::Assemble->new; foreach ( @simpleHashKeys, @doubleHashKeys, sort keys %cnodesRe ) { $ra->add($_); } foreach ( qw( exportedHeaders locationRules post vhostOptions samlIDPMetaDataXML samlIDPMetaDataExportedAttributes samlIDPMetaDataOptions samlSPMetaDataXML samlSPMetaDataExportedAttributes samlSPMetaDataMacros samlSPMetaDataOptions oidcOPMetaDataJSON oidcOPMetaDataJWKS oidcOPMetaDataOptions oidcOPMetaDataExportedVars oidcRPMetaDataOptions oidcRPMetaDataExportedVars oidcRPMetaDataOptionsExtraClaims oidcRPMetaDataMacros oidcRPMetaDataScopeRules casAppMetaDataExportedVars casAppMetaDataOptions casAppMetaDataMacros casSrvMetaDataExportedVars casSrvMetaDataOptions ) ) { $ra->add($_); } my $sessionTypes = join( "', '", @sessionTypes ); my $confConstants = "our \$hashParameters = qr/^" . $ra->as_string . "\$/;\n"; $ra = Regexp::Assemble->new; foreach (@arrayParam) { $ra->add($_); } # Not in Tree.pm foreach (qw(mySessionAuthorizedRWKeys)) { $ra->add($_); } $confConstants .= "our \$arrayParameters = qr/^" . $ra->as_string . "\$/;\n"; $ra = Regexp::Assemble->new; foreach (@bool) { $ra->add($_); } $confConstants .= "our \$boolKeys = qr/^" . $ra->as_string . "\$/;\n"; open( F, ">", $self->confConstantsFile ) or die($!); print F < -1; use constant UNKNOWN_ERROR => -2; use constant DATABASE_LOCKED => -3; use constant UPLOAD_DENIED => -4; use constant SYNTAX_ERROR => -5; use constant DEPRECATED => -6; use constant DEFAULTCONFFILE => "/usr/local/lemonldap-ng/etc/lemonldap-ng.ini"; use constant DEFAULTSECTION => "all"; use constant CONFSECTION => "configuration"; use constant PORTALSECTION => "portal"; use constant HANDLERSECTION => "handler"; use constant MANAGERSECTION => "manager"; use constant SESSIONSEXPLORERSECTION => "sessionsExplorer"; use constant APPLYSECTION => "apply"; # Default configuration backend use constant DEFAULTCONFBACKEND => "File"; use constant DEFAULTCONFBACKENDOPTIONS => ( dirName => '/usr/local/lemonldap-ng/data/conf', ); $confConstants our \@sessionTypes = ( '$sessionTypes' ); sub NO {qr/^(?:off|no|0)?\$/i} our %EXPORT_TAGS = ( 'all' => [ qw( CONFIG_WAS_CHANGED UNKNOWN_ERROR DATABASE_LOCKED UPLOAD_DENIED SYNTAX_ERROR DEPRECATED DEFAULTCONFFILE DEFAULTSECTION CONFSECTION PORTALSECTION HANDLERSECTION MANAGERSECTION SESSIONSEXPLORERSECTION APPLYSECTION DEFAULTCONFBACKEND DEFAULTCONFBACKENDOPTIONS NO \$hashParameters \$arrayParameters \@sessionTypes \$boolKeys ) ] ); our \@EXPORT_OK = ( \@{ \$EXPORT_TAGS{'all'} } ); our \@EXPORT = ( \@{ \$EXPORT_TAGS{'all'} } ); 1; EOF close F; print STDERR "done\n"; printf STDERR $format, $self->managerAttributesFile; my $managerAttr = { map { my @r; foreach my $f (@managerAttrKeys) { push @r, $f, $attributes->{$_}->{$f} if ( defined $attributes->{$_}->{$f} ); } ( $_ => {@r} ); } keys(%$attributes) }; $managerAttr = mydump( $managerAttr, 'attributes' ); my $managerSub = Dumper( \&Lemonldap::NG::Manager::Build::Attributes::perlExpr ); $managerSub =~ s/\$VAR1 = sub/sub perlExpr/s; $managerSub =~ s/^\s*(?:use strict;|package .*?;|)\n//gm; my $managerTypes = mydump( Lemonldap::NG::Manager::Build::Attributes::types(), 'types' ); $managerAttr = "# This file is generated by $module. Don't modify it by hand package Lemonldap::NG::Manager::Attributes; our \$VERSION = '$Lemonldap::NG::Manager::Build::Attributes::VERSION'; $managerSub $managerTypes} $managerAttr} "; eval { Perl::Tidy::perltidy( source => IO::String->new($managerAttr), destination => \$dst ); }; $dst = $managerAttr if ($@); open( F, ">", $self->managerAttributesFile ) or die($!); print F $dst; close F; print STDERR "done\n"; $self->buildZeroConf(); printf STDERR $format, $self->reverseTreeFile; open( F, ">", $self->reverseTreeFile ) or die($!); $jsonEnc->pretty(0); print F $jsonEnc->encode($reverseScanResult); close F; print STDERR "done\n"; $self->buildPortalConstants(); } sub buildZeroConf { my $self = shift; $jsonEnc->pretty(1); printf STDERR $format, $self->firstLmConfFile; open( F, '>', $self->firstLmConfFile ) or die($!); my $tmp = Lemonldap::NG::Manager::Conf::Zero::zeroConf( '__DNSDOMAIN__', '__SESSIONDIR__', '__PSESSIONDIR__', '__NOTIFICATIONDIR__', '__CACHEDIR__' ); $tmp->{cfgNum} = 1; print F $jsonEnc->encode($tmp); close F; print STDERR "done\n"; } sub buildPortalConstants() { my $self = shift; my %portalConstants = %{ Lemonldap::NG::Manager::Build::PortalConstants::portalConstants() }; my %reverseConstants = reverse %portalConstants; die "Duplicate value in portal constants" unless %reverseConstants == %portalConstants; printf STDERR $format, $self->portalConstantsFile; open( F, '>', $self->portalConstantsFile ) or die($!); my $urire = $RE{URI}{HTTP}{ -scheme=>qr/https?/ }{-keep}; my $content = < 'Lemonldap::NG::Handler::PSGI::Main'; use constant URIRE => qr{$urire}; use constant { EOF for my $pe ( sort { $portalConstants{$a} <=> $portalConstants{$b} } keys %portalConstants ) { my $str = $portalConstants{$pe}; $content .= " $pe => $str,\n"; } my $exports = join ", ", map { "'$_'" } sort { $portalConstants{$a} <=> $portalConstants{$b} } keys %portalConstants; my $portalConstsStr .= mydump( \%reverseConstants, 'portalConsts' ); $content .= < [ \@EXPORT_OK, 'import' ], ); our \@EXPORT = qw(import PE_OK); 1; EOF my $dst; eval { Perl::Tidy::perltidy( source => IO::String->new($content), destination => \$dst ); }; $dst = $content if ($@); open( F, '>', $self->portalConstantsFile ) or die($!); print F $dst; close F; print STDERR "done\n"; printf STDERR $format, $self->handlerStatusConstantsFile; # Handler Status file $content = < [ \@EXPORT_OK, 'import' ], ); 1; EOF eval { Perl::Tidy::perltidy( source => IO::String->new($content), destination => \$dst ); }; $dst = $content if ($@); open( F, '>', $self->handlerStatusConstantsFile ) or die($!); print F $dst; close F; print STDERR "done\n"; } sub mydump { my ( $obj, $subname ) = @_; my $t = Dumper($obj); $t =~ s/^\s*(?:use strict;|package .*?;|)\n//gm; $t =~ s/\n\s*BEGIN.*?\}\n/\n/sg; $t =~ s/^\$VAR1\s*=/sub $subname {\n return/; return $t; } sub scanTree { my ( $self, $tree, $json, $prefix, $path ) = @_; unless ( ref($tree) eq 'ARRAY' ) { die 'Not an array'; } $prefix //= ''; my $ord = -1; my $nodeName = $path ? '_nodes' : 'data'; foreach my $leaf (@$tree) { $ord++; my $jleaf = {}; # Grouped leaf if ( ref($leaf) and $leaf->{group} ) { die "'form' is required when using 'group'" unless ( $leaf->{form} ); push @$json, { id => "$prefix$leaf->{title}", title => $leaf->{title}, type => $leaf->{form}, get => $leaf->{group} }; } # Subnode elsif ( ref($leaf) ) { $jleaf->{title} = $jleaf->{id} = $leaf->{title}; $jleaf->{type} = $leaf->{form} if ( $leaf->{form} ); if ( $leaf->{title} =~ /^((?:oidc|saml|cas)Service)MetaData$/ ) { no strict 'refs'; my @tmp = $self->scanLeaf( $leaf->{nodes} ); ${ $1 . 'Parameters' } = \@tmp; } foreach my $n (qw(nodes nodes_cond)) { if ( $leaf->{$n} ) { $jleaf->{"_$n"} = []; $self->scanTree( $leaf->{$n}, $jleaf->{"_$n"}, $prefix, "$path.$nodeName\[$ord\]" ); if ( ( $leaf->{title} eq 'authParams' and $n eq 'nodes_cond' ) or $leaf->{title} eq 'issuerParams' ) { my $vn = $leaf->{title}; $vn =~ s/Params$/Parameters/; foreach my $sn ( @{ $leaf->{$n} } ) { no strict 'refs'; my @cn = $self->scanLeaf( $sn->{nodes} ); ${$vn}->{ $sn->{title} } = \@cn; } } elsif ( $leaf->{title} eq 'issuerParams' ) { } if ( $n eq 'nodes_cond' ) { foreach my $sn ( @{ $jleaf->{"_$n"} } ) { $sn->{show} = 'false'; } } } } $jleaf->{help} = $leaf->{help} if ( $leaf->{help} ); $jleaf->{_nodes_filter} = $leaf->{nodes_filter} if ( $leaf->{nodes_filter} ); push @$json, $jleaf; } # Leaf else { # Get data type and build tree # # Types : PerlModule array bool boolOrExpr catAndAppList file # hostname int keyTextContainer lmAttrOrMacro longtext # openidServerList oidcAttributeContainer pcre rulesContainer # samlAssertion samlAttributeContainer samlService select text # trool url virtualHostContainer word password if ( $leaf =~ s/^\*// ) { push @angularScopeVars, [ $leaf, "$path._nodes[$ord]" ]; } push @sessionTypes, $1 if ( $leaf =~ /^(.*)(?{$leaf} or die("Missing attribute $leaf"); #print STDERR "| $attr->{documentation} | $leaf |\n"; $jleaf = { id => "$prefix$leaf", title => $leaf }; unless ( $attr->{type} ) { print STDERR "Fatal: no type: $leaf\n"; exit; } # TODO: change this $attr->{type} =~ s/^(?:url|word|pcre|lmAttrOrMacro|hostname|PerlModule)$/text/; $jleaf->{type} = $attr->{type} if ( $attr->{type} ne 'text' ); foreach my $w (qw(default help select get template)) { $jleaf->{$w} = $attr->{$w} if ( defined $attr->{$w} ); } if ( defined $jleaf->{default} ) { unless ( $attr->{type} eq 'bool' and $jleaf->{default} == 0 ) { $defaultValues->{$leaf} = $jleaf->{default}; } if ( ref( $jleaf->{default} ) ) { $jleaf->{default} = []; my $type = $attr->{type}; $type =~ s/Container//; foreach my $k ( sort keys( %{ $attr->{default} } ) ) { # Special handling for oidcAttribute my $default = $attr->{default}->{$k}; if ( $attr->{type} eq 'oidcAttributeContainer' ) { $default = [ $default, "string", "auto" ]; } push @{ $jleaf->{default} }, { id => "$prefix$leaf/$k", title => $k, type => $type, data => $default, ( $type eq 'rule' ? ( re => $k ) : () ), }; } } } if ($prefix) { push @cnodesKeys, $leaf; } # issue 2439 # FIXME: in future versions, oidcOPMetaDataJSON and samlIDPMetaDataXML shoud # behave the same if ( $leaf =~ /^oidcOPMetaData(?:JSON|JWKS)$/ ) { push @simpleHashKeys, $leaf; } if ( $attr->{type} =~ /^(?:catAndAppList|\w+Container)$/ ) { $jleaf->{cnodes} = $prefix . $leaf; unless ( $prefix or $leaf =~ $reIgnoreKeys ) { push @simpleHashKeys, $leaf; } } elsif ( $attr->{type} eq 'doubleHash' and $leaf !~ $reIgnoreKeys ) { push @doubleHashKeys, $leaf; } else { if ( $prefix and !$jleaf->{get} ) { $jleaf->{get} = $prefix . $jleaf->{title}; } if ( $attr->{type} eq 'bool' ) { push @bool, $leaf; } if ( $attr->{type} eq 'array' ) { push @arrayParam, $leaf; } } push @$json, $jleaf; } } } sub scanLeaf { my ( $self, $tree ) = @_; my @res; foreach my $k (@$tree) { if ( ref $k ) { push @res, $self->scanLeaf1( $k->{nodes} || $k->{group} ); } else { push @res, $k; } } return @res; } sub scanLeaf1 { my ( $self, $tree ) = @_; my @res; foreach my $k (@$tree) { if ( ref $k ) { push @res, $self->scanLeaf( $k->{nodes} || $k->{group} ); } else { push @res, $k; } } return @res; } sub reverseScan { my ( $self, $tree, $path, $res ) = @_; foreach my $elem (@$tree) { $elem =~ s/^\*//; if ( ref($elem) eq 'HASH' ) { foreach (qw(nodes nodes_cond group)) { $self->reverseScan( $elem->{$_}, "$path$elem->{title}/", $res ) if ( $elem->{$_} ); } } else { my $tmp = $path; $tmp =~ s#/$##; $res->{$elem} = $tmp; } } return $res; } 1; __END__ =head1 NAME =encoding utf8 Lemonldap::NG::Manager::Build - Static files generator of Lemonldap::NG Web-SSO system. =head1 SYNOPSIS use Lemonldap::NG::Manager::Build; Lemonldap::NG::Manager::Build->run( structFile => "site/htdocs/static/struct.json", confTreeFile => "site/htdocs/static/js/conftree.js", managerConstantsFile => "lib/Lemonldap/NG/Common/Conf/ReConstants.pm", managerAttributesFile => 'lib/Lemonldap/NG/Manager/Attributes.pm', defaultValuesFile => "lib/Lemonldap/NG/Common/Conf/DefaultValues.pm", firstLmConfFile => "_example/conf/lmConf-1.json", reverseTreeFile => "site/htdocs/static/reverseTree.json", ); =head1 DESCRIPTION Lemonldap::NG::Manager::Build is only used to build javascript files and Lemonldap::NG constants Perl files. It must be launched after each change. =head2 DEVELOPER CORNER To add a new parameter, you have to: =over =item declare it in Manager/Build/Attributes.pm; =item declare its position in the tree in Manager/Build/Tree.pm (or Manager/Build/CTrees.pm for complex nodes); =item refresh files by using this (or launch any build makefile target at the root of the Lemonldap::NG project sources). =back See below for details. =head3 Files generated `scripts/jsongenerator.pl` file uses Lemonldap::NG::Manager::Build::Attributes, Lemonldap::NG::Manager::Build::Tree and Lemonldap::NG::Manager::Build::CTrees to generate =over =item `site/htdocs/static/struct.json`: Main file containing the tree view; =item `site/htdocs/static/js/conftree.js`: generates Virtualhosts, SAML and OpenID-Connect partners sub-trees; =item `site/htdocs/static/reverseTree.json`: map used by manager diff to find attribute position in the tree; =item `Lemonldap::NG::Manager::Constants`: constants used by all Perl manager components; =item `Lemonldap::NG::Common::Conf::DefaultValues`: constants used to read configuration; =item `Lemonldap::NG::Manager::Attributes`: parameters attributes used by the manager during configuration upload; =item lmConf-1.json: first configuration in file format; =back =head3 Attribute declaration set your new attribute as a key of attributes() function that points to a hash ref containing: =over =item type (required): content type must be declared in sub types() in the same file (except if attribute embeds its own tests) and must match to a form stored in static/forms/ directory; =item help (optional): Relative HTML path to help page (relative to /doc/pages/documentation//); =item default (recommended): default value to set if not defined; =item select (optional): required only if type is `select`. In this case, it must contain an array of { k => , v => } hashref =item documentation (recommended): some words for other developers =item test (optional): if test is not defined for this type or if test must be more restrictive, set here a regular expression or a subroutine. Arguments passed to subroutine are (keyValue, newConf, currentKey). It returns 2 arguments: a boolean result and a message (if non empty message will be displayed as warning or error depending of result); =item msgFail (optional): for regexp based tests, message to display in case of error. Words to translate have to be written as so: __toTranslate__; =item keyTest (optional): for keys/values attributes, test to be applied on key; =item keyMsgFail (optional): for regexp based key tests, same as msgFail for keys test; =back If you want to declare a new type, you have to declare following properties: =over =item test, msgFail, keyTest, keyMsgFail as shown above, =item form: form to use if it doesn't have the same name. =back =head3 Tree location The tree is now very simple: it contains nodes and leaves. Leaves are designed only by their attribute name. All description must be done in the file described above. Nodes are array member designed as this: { title => 'titleToTranslate', help => 'helpUrl', form => 'relativeUrl', nodes => [ ... nodes or leaf ... ] } Explanations: =over =item title (required): it must contain an entry of static/languages/lang.json =item help (recommended): as above, the relative HTML path to the help page (relative to /doc/pages/documentation//); =item form (optional): the name of a static/forms/.html file =item nodes: array of sub nodes and leaf attached to this node =item group: must never be used in conjunction with nodes. Array of leaves only to be displayed in the same form =item nodes_cond: array of sub nodes that will be displayed with a filter. Not yet documented here, see the source code of site/htdocs/static/js/filterFunctions.js. =item nodes_filter: filter entry in site/htdocs/static/js/filterFunctions.js for the same feature. =back =head1 SEE ALSO L =head1 AUTHORS =over =item LemonLDAP::NG team L =back =head1 BUG REPORT Use OW2 system to report bug or ask for features: L Note that if you want to post a ticket for a conf upload problem, please see L before. =head1 DOWNLOAD Lemonldap::NG is available at L =head1 COPYRIGHT AND LICENSE See COPYING file for details. This library is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see L. =cut