7 use SL::MoreCommon qw(uri_encode);
 
  13   my ($package, $domain) = @_;
 
  15   if (!$menu_cache{$domain}) {
 
  16     my $path = File::Spec->catdir('menus', $domain);
 
  18     opendir my $dir, $path or die "can't open $path: $!";
 
  19     my @files = sort grep -f "$path/$_", grep /\.yaml$/, readdir $dir;
 
  24     for my $file (@files) {
 
  27         $data = SL::YAML::LoadFile(File::Spec->catfile($path, $file));
 
  30         die "Error while parsing $file: $@";
 
  33       # check if this file is internally consistent.
 
  34       die 'not an array ref' unless $data && 'ARRAY' eq ref $data; # TODO get better diag to user
 
  36       # in particular duplicate ids tend to come up as a user error when editing the menu files
 
  38       #$uniq_ids{$_->{id}}++ && die "Error in $file: duplicate id $_->{id}" for @$data;
 
  40       _merge($nodes, $nodes_by_id, $data);
 
  46       by_id => $nodes_by_id,
 
  51     $menu_cache{$domain} = $self;
 
  53     $menu_cache{$domain}->clear_access;
 
  56   $menu_cache{$domain}->set_access;
 
  58   return $menu_cache{$domain};
 
  62   my ($nodes, $by_id, $data) = @_;
 
  64   for my $node (@$data) {
 
  67     die "menu: node with name '$node->{name}' does not have an id" if !$id;
 
  69     my $merge_to = $by_id->{$id};
 
  73       $by_id->{$id} = $node;
 
  77     # TODO make this a real recursive merge
 
  78     # TODO add support for arrays
 
  80     # merge keys except params
 
  81     for my $key (keys %$node) {
 
  82       if (ref $node->{$key}) {
 
  83         if ('HASH' eq ref $node->{$key}) {
 
  84           $merge_to->{$key} = {} if !exists $merge_to->{$key} || 'HASH' ne ref $merge_to->{$key};
 
  85           for (keys %{ $node->{params} }) {
 
  86             $merge_to->{$key}{$_} = $node->{params}{$_};
 
  89           die "unsupported structure @{[ ref $node->{$key} ]}";
 
  92         $merge_to->{$key} = $node->{$key};
 
 101   # first, some sanity check. are all parents valid ids or empty?
 
 102   for my $node ($self->nodes) {
 
 103     next if !exists $node->{parent} || !$node->{parent} || $self->{by_id}->{$node->{id}};
 
 104     die "menu: node $node->{id} has non-existent parent $node->{parent}";
 
 108   # order them by parent
 
 109   for my $node ($self->nodes) {
 
 110     push @{ $by_parent{ $node->{parent} // '' } //= [] }, $node;
 
 113   # autovivify order in by_parent, so that numerical sorting for entries without order
 
 114   # preserves their order and position with respect to entries with order.
 
 115   for (values %by_parent) {
 
 118       if (defined $node->{order} && $node->{order} * 1) {
 
 119         $last_order = $node->{order};
 
 121         $node->{order} = ++$last_order;
 
 127   $self->{by_id}{''} = $tree;
 
 130   for (keys %by_parent) {
 
 131     my $parent = $self->{by_id}{$_};
 
 132     $parent->{children} =  [ sort { $a->{order} <=> $b->{order} } @{ $by_parent{$_} } ];
 
 135   _set_level_rec($tree->{children}, 0);
 
 137   $self->{tree} = $tree->{children};
 
 141   my ($ary_ref, $level) = @_;
 
 144     $_->{level} = $level;
 
 145     _set_level_rec($_->{children}, $level + 1) if $_->{children};
 
 154   my ($self, $all) = @_;
 
 156   _tree_walk_rec($self->{tree}, $all);
 
 160   my ($ary_ref, $all) = @_;
 
 161   map { $_->{children} ? ($_, _tree_walk_rec($_->{children}, $all)) : ($_) } grep { $all || $_->{visible} } @$ary_ref;
 
 164 sub parse_access_string {
 
 165   my ($self, $node) = @_;
 
 170   push @stack, $cur_ary;
 
 172   my $access = $node->{access};
 
 174   while ($access =~ m/^([a-z_\/]+|\!|\||\&|\(|\)|\s+)/) {
 
 176     substr($access, 0, length($1)) = "";
 
 178     next if ($token =~ /\s/);
 
 181       my $new_cur_ary = [];
 
 182       push @stack, $new_cur_ary;
 
 183       push @{$cur_ary}, $new_cur_ary;
 
 184       $cur_ary = $new_cur_ary;
 
 186     } elsif ($token eq ")") {
 
 189         die "Error while parsing menu entry $node->{id}: missing '('";
 
 191       $cur_ary = $stack[-1];
 
 193     } elsif (($token eq "|") || ($token eq "&") || ($token eq "!")) {
 
 194       push @{$cur_ary}, $token;
 
 197       if ($token =~ m{^ client / (.*) }x) {
 
 198         push @{$cur_ary}, $self->parse_instance_conf_string($1);
 
 200         push @{$cur_ary}, $::auth->check_right($::myconfig{login}, $token, 1);
 
 206     die "Error while parsing menu entry $node->{id}: unrecognized token at the start of '$access'\n";
 
 209   if (1 < scalar @stack) {
 
 210     die "Error while parsing menu entry $node->{id}: Missing ')'\n";
 
 213   return SL::Auth::evaluate_rights_ary($stack[0]);
 
 217   my ($self, $node) = @_;
 
 219   return undef if !$node->{href} && !$node->{module} && !$node->{params};
 
 221   return $node->{href_for_node} ||= do {
 
 222     my $href = $node->{href} || $node->{module} || 'controller.pl';
 
 225     while (my ($key, $value) = each %{ $node->{params} }) {
 
 226       push @tokens, uri_encode($key, 1) . "=" . uri_encode($value, 1);
 
 229     join '?', $href, grep $_, join '&', @tokens;
 
 234   $::locale->text($_[1]{name})
 
 237 sub parse_instance_conf_string {
 
 238   my ($self, $setting) = @_;
 
 239   return $::instance_conf->data->{$setting};
 
 244   for my $node ($self->tree_walk("all")) {
 
 245     delete $node->{visible};
 
 246     delete $node->{visible_children};
 
 252   # 1. evaluate access for all
 
 253   # 2. if a menu has no visible children, its not visible either
 
 255   for my $node (reverse $self->tree_walk("all")) {
 
 256     $node->{visible} = $node->{access}           ? $self->parse_access_string($node)
 
 257                      : !$node->{children}        ? 1
 
 258                      : $node->{visible_children} ? 1
 
 260     if ($node->{visible} && $node->{parent}) {
 
 261       $self->{by_id}{ $node->{parent} }{visible_children} = 1;