7 use SL::MoreCommon qw(uri_encode);
14 my ($package, $domain) = @_;
16 if (!$menu_cache{$domain}) {
17 my $path = File::Spec->catdir('menus', $domain);
19 opendir my $dir, $path or die "can't open $path: $!";
20 my @files = sort grep -f "$path/$_", grep /\.yaml$/, readdir $dir;
25 for my $file (@files) {
28 $data = SL::YAML::LoadFile(File::Spec->catfile($path, $file));
31 die "Error while parsing $file: $@";
34 # check if this file is internally consistent.
35 die 'not an array ref' unless $data && 'ARRAY' eq ref $data; # TODO get better diag to user
37 # in particular duplicate ids tend to come up as a user error when editing the menu files
39 #$uniq_ids{$_->{id}}++ && die "Error in $file: duplicate id $_->{id}" for @$data;
41 _merge($nodes, $nodes_by_id, $data);
44 my $instance_state = SL::InstanceState->new;
48 by_id => $nodes_by_id,
49 instance_state => $instance_state,
54 $menu_cache{$domain} = $self;
56 $menu_cache{$domain}->clear_access;
59 $menu_cache{$domain}->set_access;
61 return $menu_cache{$domain};
65 my ($nodes, $by_id, $data) = @_;
67 for my $node (@$data) {
70 die "menu: node with name '$node->{name}' does not have an id" if !$id;
72 my $merge_to = $by_id->{$id};
76 $by_id->{$id} = $node;
80 # TODO make this a real recursive merge
81 # TODO add support for arrays
83 # merge keys except params
84 for my $key (keys %$node) {
85 if (ref $node->{$key}) {
86 if ('HASH' eq ref $node->{$key}) {
87 $merge_to->{$key} = {} if !exists $merge_to->{$key} || 'HASH' ne ref $merge_to->{$key};
88 for (keys %{ $node->{params} }) {
89 $merge_to->{$key}{$_} = $node->{params}{$_};
92 die "unsupported structure @{[ ref $node->{$key} ]}";
95 $merge_to->{$key} = $node->{$key};
104 # first, some sanity check. are all parents valid ids or empty?
105 for my $node ($self->nodes) {
106 next if !exists $node->{parent} || !$node->{parent} || $self->{by_id}->{$node->{id}};
107 die "menu: node $node->{id} has non-existent parent $node->{parent}";
111 # order them by parent
112 for my $node ($self->nodes) {
113 push @{ $by_parent{ $node->{parent} // '' } //= [] }, $node;
116 # autovivify order in by_parent, so that numerical sorting for entries without order
117 # preserves their order and position with respect to entries with order.
118 for (values %by_parent) {
121 if (defined $node->{order} && $node->{order} * 1) {
122 $last_order = $node->{order};
124 $node->{order} = ++$last_order;
130 $self->{by_id}{''} = $tree;
133 for (keys %by_parent) {
134 my $parent = $self->{by_id}{$_};
135 $parent->{children} = [ sort { $a->{order} <=> $b->{order} } @{ $by_parent{$_} } ];
138 _set_level_rec($tree->{children}, 0);
140 $self->{tree} = $tree->{children};
144 my ($ary_ref, $level) = @_;
147 $_->{level} = $level;
148 _set_level_rec($_->{children}, $level + 1) if $_->{children};
157 my ($self, $all) = @_;
159 _tree_walk_rec($self->{tree}, $all);
163 my ($ary_ref, $all) = @_;
164 map { $_->{children} ? ($_, _tree_walk_rec($_->{children}, $all)) : ($_) } grep { $all || $_->{visible} } @$ary_ref;
167 sub parse_access_string {
168 my ($self, $node) = @_;
173 push @stack, $cur_ary;
175 my $access = $node->{access};
177 while ($access =~ m/^([a-z_\/]+|\!|\||\&|\(|\)|\s+)/) {
179 substr($access, 0, length($1)) = "";
181 next if ($token =~ /\s/);
184 my $new_cur_ary = [];
185 push @stack, $new_cur_ary;
186 push @{$cur_ary}, $new_cur_ary;
187 $cur_ary = $new_cur_ary;
189 } elsif ($token eq ")") {
192 die "Error while parsing menu entry $node->{id}: missing '('";
194 $cur_ary = $stack[-1];
196 } elsif (($token eq "|") || ($token eq "&") || ($token eq "!")) {
197 push @{$cur_ary}, $token;
200 if ($token =~ m{^ client / (.*) }x) {
201 push @{$cur_ary}, $self->parse_instance_conf_string($1);
202 } elsif ($token =~ m{^ state / (.*) }x) {
203 push @{$cur_ary}, $self->parse_instance_state_string($1);
205 push @{$cur_ary}, $::auth->check_right($::myconfig{login}, $token, 1);
211 die "Error while parsing menu entry $node->{id}: unrecognized token at the start of '$access'\n";
214 if (1 < scalar @stack) {
215 die "Error while parsing menu entry $node->{id}: Missing ')'\n";
218 return SL::Auth::evaluate_rights_ary($stack[0]);
222 my ($self, $node) = @_;
224 return undef if !$node->{href} && !$node->{module} && !$node->{params};
226 return $node->{href_for_node} ||= do {
227 my $href = $node->{href} || $node->{module} || 'controller.pl';
230 while (my ($key, $value) = each %{ $node->{params} }) {
231 push @tokens, uri_encode($key, 1) . "=" . uri_encode($value, 1);
234 join '?', $href, grep $_, join '&', @tokens;
239 $::locale->text($_[1]{name})
242 sub parse_instance_conf_string {
243 my ($self, $setting) = @_;
244 return $::instance_conf->data->{$setting};
247 sub parse_instance_state_string {
248 my ($self, $setting) = @_;
249 return $self->{instance_state}->$setting;
254 for my $node ($self->tree_walk("all")) {
255 delete $node->{visible};
256 delete $node->{visible_children};
262 # 1. evaluate access for all
263 # 2. if a menu has no visible children, its not visible either
265 for my $node (reverse $self->tree_walk("all")) {
266 $node->{visible} = $node->{access} ? $self->parse_access_string($node)
267 : !$node->{children} ? 1
268 : $node->{visible_children} ? 1
270 if ($node->{visible} && $node->{parent}) {
271 $self->{by_id}{ $node->{parent} }{visible_children} = 1;