8 use SL::MoreCommon qw(uri_encode);
 
  12    $yaml_xs =  eval { require YAML::XS };
 
  18   my ($package, $domain) = @_;
 
  20   if (!$menu_cache{$domain}) {
 
  21     my $path = File::Spec->catdir('menus', $domain);
 
  23     opendir my $dir, $path or die "can't open $path: $!";
 
  24     my @files = sort grep -f "$path/$_", grep /\.yaml$/, readdir $dir;
 
  29     for my $file (@files) {
 
  33           $data = YAML::XS::LoadFile(File::Spec->catfile($path, $file));
 
  35           $data = YAML::LoadFile(File::Spec->catfile($path, $file));
 
  39         die "Error while parsing $file: $@";
 
  42       # check if this file is internally consistent.
 
  43       die 'not an array ref' unless $data && 'ARRAY' eq ref $data; # TODO get better diag to user
 
  45       # in particular duplicate ids tend to come up as a user error when editing the menu files
 
  47       #$uniq_ids{$_->{id}}++ && die "Error in $file: duplicate id $_->{id}" for @$data;
 
  49       _merge($nodes, $nodes_by_id, $data);
 
  55       by_id => $nodes_by_id,
 
  60     $menu_cache{$domain} = $self;
 
  62     $menu_cache{$domain}->clear_access;
 
  65   $menu_cache{$domain}->set_access;
 
  67   return $menu_cache{$domain};
 
  71   my ($nodes, $by_id, $data) = @_;
 
  73   for my $node (@$data) {
 
  76     die "menu: node with name '$node->{name}' does not have an id" if !$id;
 
  78     my $merge_to = $by_id->{$id};
 
  82       $by_id->{$id} = $node;
 
  86     # TODO make this a real recursive merge
 
  87     # TODO add support for arrays
 
  89     # merge keys except params
 
  90     for my $key (keys %$node) {
 
  91       if (ref $node->{$key}) {
 
  92         if ('HASH' eq ref $node->{$key}) {
 
  93           $merge_to->{$key} = {} if !exists $merge_to->{$key} || 'HASH' ne ref $merge_to->{$key};
 
  94           for (keys %{ $node->{params} }) {
 
  95             $merge_to->{$key}{$_} = $node->{params}{$_};
 
  98           die "unsupported structure @{[ ref $node->{$key} ]}";
 
 101         $merge_to->{$key} = $node->{$key};
 
 110   # first, some sanity check. are all parents valid ids or empty?
 
 111   for my $node ($self->nodes) {
 
 112     next if !exists $node->{parent} || !$node->{parent} || $self->{by_id}->{$node->{id}};
 
 113     die "menu: node $node->{id} has non-existent parent $node->{parent}";
 
 117   # order them by parent
 
 118   for my $node ($self->nodes) {
 
 119     push @{ $by_parent{ $node->{parent} // '' } //= [] }, $node;
 
 122   # autovivify order in by_parent, so that numerical sorting for entries without order
 
 123   # preserves their order and position with respect to entries with order.
 
 124   for (values %by_parent) {
 
 127       if (defined $node->{order} && $node->{order} * 1) {
 
 128         $last_order = $node->{order};
 
 130         $node->{order} = ++$last_order;
 
 136   $self->{by_id}{''} = $tree;
 
 139   for (keys %by_parent) {
 
 140     my $parent = $self->{by_id}{$_};
 
 141     $parent->{children} =  [ sort { $a->{order} <=> $b->{order} } @{ $by_parent{$_} } ];
 
 144   _set_level_rec($tree->{children}, 0);
 
 146   $self->{tree} = $tree->{children};
 
 150   my ($ary_ref, $level) = @_;
 
 153     $_->{level} = $level;
 
 154     _set_level_rec($_->{children}, $level + 1) if $_->{children};
 
 163   my ($self, $all) = @_;
 
 165   _tree_walk_rec($self->{tree}, $all);
 
 169   my ($ary_ref, $all) = @_;
 
 170   map { $_->{children} ? ($_, _tree_walk_rec($_->{children}, $all)) : ($_) } grep { $all || $_->{visible} } @$ary_ref;
 
 173 sub parse_access_string {
 
 174   my ($self, $node) = @_;
 
 179   push @stack, $cur_ary;
 
 181   my $access = $node->{access};
 
 183   while ($access =~ m/^([a-z_\/]+|\!|\||\&|\(|\)|\s+)/) {
 
 185     substr($access, 0, length($1)) = "";
 
 187     next if ($token =~ /\s/);
 
 190       my $new_cur_ary = [];
 
 191       push @stack, $new_cur_ary;
 
 192       push @{$cur_ary}, $new_cur_ary;
 
 193       $cur_ary = $new_cur_ary;
 
 195     } elsif ($token eq ")") {
 
 198         die "Error while parsing menu entry $node->{id}: missing '('";
 
 200       $cur_ary = $stack[-1];
 
 202     } elsif (($token eq "|") || ($token eq "&") || ($token eq "!")) {
 
 203       push @{$cur_ary}, $token;
 
 206       if ($token =~ m{^ client / (.*) }x) {
 
 207         push @{$cur_ary}, $self->parse_instance_conf_string($1);
 
 209         push @{$cur_ary}, $::auth->check_right($::myconfig{login}, $token, 1);
 
 215     die "Error while parsing menu entry $node->{id}: unrecognized token at the start of '$access'\n";
 
 218   if (1 < scalar @stack) {
 
 219     die "Error while parsing menu entry $node->{id}: Missing ')'\n";
 
 222   return SL::Auth::evaluate_rights_ary($stack[0]);
 
 226   my ($self, $node) = @_;
 
 228   return undef if !$node->{href} && !$node->{module} && !$node->{params};
 
 230   return $node->{href_for_node} ||= do {
 
 231     my $href = $node->{href} || $node->{module} || 'controller.pl';
 
 234     while (my ($key, $value) = each %{ $node->{params} }) {
 
 235       push @tokens, uri_encode($key, 1) . "=" . uri_encode($value, 1);
 
 238     join '?', $href, grep $_, join '&', @tokens;
 
 243   $::locale->text($_[1]{name})
 
 246 sub parse_instance_conf_string {
 
 247   my ($self, $setting) = @_;
 
 248   return $::instance_conf->data->{$setting};
 
 253   for my $node ($self->tree_walk("all")) {
 
 254     delete $node->{visible};
 
 255     delete $node->{visible_children};
 
 261   # 1. evaluate access for all
 
 262   # 2. if a menu has no visible children, its not visible either
 
 264   for my $node (reverse $self->tree_walk("all")) {
 
 265     $node->{visible} = $node->{access}           ? $self->parse_access_string($node)
 
 266                      : !$node->{children}        ? 1
 
 267                      : $node->{visible_children} ? 1
 
 269     if ($node->{visible} && $node->{parent}) {
 
 270       $self->{by_id}{ $node->{parent} }{visible_children} = 1;