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;