X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FMenu.pm;h=0c6df2247a976d6efb2939b9fd710bfa7d26e61e;hb=e6d132a190fc0777413850d82a807babf944bba0;hp=0bdf945e4a888690a45dece9f0e684755849b06e;hpb=e848dbf1f17a606e22afb161cb3fb7bd88895f92;p=kivitendo-erp.git diff --git a/SL/Menu.pm b/SL/Menu.pm index 0bdf945e4..0c6df2247 100644 --- a/SL/Menu.pm +++ b/SL/Menu.pm @@ -1,170 +1,266 @@ -#===================================================================== -# LX-Office ERP -# Copyright (C) 2004 -# Based on SQL-Ledger Version 2.1.9 -# Web http://www.lx-office.org -# -#===================================================================== -# SQL-Ledger Accounting -# Copyright (C) 2001 -# -# Author: Dieter Simader -# Email: dsimader@sql-ledger.org -# Web: http://www.sql-ledger.org -# -# Contributors: -# -# This program 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 of the License, 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, write to the Free Software -# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. -#===================================================================== -# -# routines for menu items -# -#===================================================================== - -package Menu; +package SL::Menu; + +use strict; + +use SL::Auth; +use File::Spec; +use SL::MoreCommon qw(uri_encode); +use SL::YAML; + +our %menu_cache; sub new { - $main::lxdebug->enter_sub(); + my ($package, $domain) = @_; - my ($type, $menufile, $level) = @_; + if (!$menu_cache{$domain}) { + my $path = File::Spec->catdir('menus', $domain); - use SL::Inifile; - my $self = Inifile->new($menufile, $level); + opendir my $dir, $path or die "can't open $path: $!"; + my @files = sort grep -f "$path/$_", grep /\.yaml$/, readdir $dir; + close $dir; - $main::lxdebug->leave_sub(); + my $nodes = []; + my $nodes_by_id = {}; + for my $file (@files) { + my $data; + eval { + $data = SL::YAML::LoadFile(File::Spec->catfile($path, $file)); + 1; + } or do { + die "Error while parsing $file: $@"; + }; - bless $self, $type; -} + # check if this file is internally consistent. + die 'not an array ref' unless $data && 'ARRAY' eq ref $data; # TODO get better diag to user -sub menuitem { - $main::lxdebug->enter_sub(); + # in particular duplicate ids tend to come up as a user error when editing the menu files + #my %uniq_ids; + #$uniq_ids{$_->{id}}++ && die "Error in $file: duplicate id $_->{id}" for @$data; - my ($self, $myconfig, $form, $item) = @_; + _merge($nodes, $nodes_by_id, $data); + } - my $module = $form->{script}; - my $action = "section_menu"; - my $target = ""; - if ($self->{$item}{module}) { - $module = $self->{$item}{module}; - } - if ($self->{$item}{action}) { - $action = $self->{$item}{action}; + my $self = bless { + nodes => $nodes, + by_id => $nodes_by_id, + }, $package; + + $self->build_tree; + + $menu_cache{$domain} = $self; + } else { + $menu_cache{$domain}->clear_access; } - if ($self->{$item}{target}) { - $target = $self->{$item}{target}; + + $menu_cache{$domain}->set_access; + + return $menu_cache{$domain}; +} + +sub _merge { + my ($nodes, $by_id, $data) = @_; + + for my $node (@$data) { + my $id = $node->{id}; + + die "menu: node with name '$node->{name}' does not have an id" if !$id; + + my $merge_to = $by_id->{$id}; + + if (!$merge_to) { + push @$nodes, $node; + $by_id->{$id} = $node; + next; + } + + # TODO make this a real recursive merge + # TODO add support for arrays + + # merge keys except params + for my $key (keys %$node) { + if (ref $node->{$key}) { + if ('HASH' eq ref $node->{$key}) { + $merge_to->{$key} = {} if !exists $merge_to->{$key} || 'HASH' ne ref $merge_to->{$key}; + for (keys %{ $node->{params} }) { + $merge_to->{$key}{$_} = $node->{params}{$_}; + } + } else { + die "unsupported structure @{[ ref $node->{$key} ]}"; + } + } else { + $merge_to->{$key} = $node->{$key}; + } + } } +} - my $level = $form->escape($item); +sub build_tree { + my ($self) = @_; - my $str = - qq|{path}&action=$action&level=$level&login=$form->{login}&password=$form->{password}|; + # first, some sanity check. are all parents valid ids or empty? + for my $node ($self->nodes) { + next if !exists $node->{parent} || !$node->{parent} || $self->{by_id}->{$node->{id}}; + die "menu: node $node->{id} has non-existent parent $node->{parent}"; + } - my @vars = qw(module action target href); + my %by_parent; + # order them by parent + for my $node ($self->nodes) { + push @{ $by_parent{ $node->{parent} // '' } //= [] }, $node; + } - if ($self->{$item}{href}) { - $str = qq|{$item}{href}|; - @vars = qw(module target href); + # autovivify order in by_parent, so that numerical sorting for entries without order + # preserves their order and position with respect to entries with order. + for (values %by_parent) { + my $last_order = 0; + for my $node (@$_) { + if (defined $node->{order} && $node->{order} * 1) { + $last_order = $node->{order}; + } else { + $node->{order} = ++$last_order; + } + } } - map { delete $self->{$item}{$_} } @vars; + my $tree = { }; + $self->{by_id}{''} = $tree; + - # add other params - foreach my $key (keys %{ $self->{$item} }) { - $str .= "&" . $form->escape($key, 1) . "="; - ($value, $conf) = split /=/, $self->{$item}{$key}, 2; - $value = $myconfig->{$value} . "/$conf" if ($conf); - $str .= $form->escape($value, 1); + for (keys %by_parent) { + my $parent = $self->{by_id}{$_}; + $parent->{children} = [ sort { $a->{order} <=> $b->{order} } @{ $by_parent{$_} } ]; } - if ($target) { - $str .= qq| target=$target|; + _set_level_rec($tree->{children}, 0); + + $self->{tree} = $tree->{children}; +} + +sub _set_level_rec { + my ($ary_ref, $level) = @_; + + for (@$ary_ref) { + $_->{level} = $level; + _set_level_rec($_->{children}, $level + 1) if $_->{children}; } +} - $str .= ">"; +sub nodes { + @{ $_[0]{nodes} } +} - $main::lxdebug->leave_sub(); +sub tree_walk { + my ($self, $all) = @_; - return $str; + _tree_walk_rec($self->{tree}, $all); } -sub menuitemNew { - my ($self, $myconfig, $form, $item) = @_; +sub _tree_walk_rec { + my ($ary_ref, $all) = @_; + map { $_->{children} ? ($_, _tree_walk_rec($_->{children}, $all)) : ($_) } grep { $all || $_->{visible} } @$ary_ref; +} - my $module = $form->{script}; - my $action = "section_menu"; +sub parse_access_string { + my ($self, $node) = @_; - #if ($self->{$item}{module}) { - $module = $self->{$item}{module}; + my @stack; + my $cur_ary = []; - #} - if ($self->{$item}{action}) { - $action = $self->{$item}{action}; - } + push @stack, $cur_ary; - my $level = $form->escape($item); - my $str = - qq|$module?path=$form->{path}&action=$action&level=$level&login=$form->{login}&password=$form->{password}|; - my @vars = qw(module action target href); + my $access = $node->{access}; - if ($self->{$item}{href}) { - $str = qq|$self->{$item}{href}|; - @vars = qw(module target href); - } + while ($access =~ m/^([a-z_\/]+|\!|\||\&|\(|\)|\s+)/) { + my $token = $1; + substr($access, 0, length($1)) = ""; + + next if ($token =~ /\s/); + + if ($token eq "(") { + my $new_cur_ary = []; + push @stack, $new_cur_ary; + push @{$cur_ary}, $new_cur_ary; + $cur_ary = $new_cur_ary; - map { delete $self->{$item}{$_} } @vars; + } elsif ($token eq ")") { + pop @stack; + if (!@stack) { + die "Error while parsing menu entry $node->{id}: missing '('"; + } + $cur_ary = $stack[-1]; - # add other params - foreach my $key (keys %{ $self->{$item} }) { - $str .= "&" . $form->escape($key, 1) . "="; - ($value, $conf) = split /=/, $self->{$item}{$key}, 2; - $value = $myconfig->{$value} . "/$conf" if ($conf); - $str .= $form->escape($value, 1); + } elsif (($token eq "|") || ($token eq "&") || ($token eq "!")) { + push @{$cur_ary}, $token; + + } else { + if ($token =~ m{^ client / (.*) }x) { + push @{$cur_ary}, $self->parse_instance_conf_string($1); + } else { + push @{$cur_ary}, $::auth->check_right($::myconfig{login}, $token, 1); + } + } } - $str .= " "; + if ($access) { + die "Error while parsing menu entry $node->{id}: unrecognized token at the start of '$access'\n"; + } + if (1 < scalar @stack) { + die "Error while parsing menu entry $node->{id}: Missing ')'\n"; + } + + return SL::Auth::evaluate_rights_ary($stack[0]); } -sub access_control { - $main::lxdebug->enter_sub(); +sub href_for_node { + my ($self, $node) = @_; - my ($self, $myconfig, $menulevel) = @_; + return undef if !$node->{href} && !$node->{module} && !$node->{params}; - my @menu = (); + return $node->{href_for_node} ||= do { + my $href = $node->{href} || $node->{module} || 'controller.pl'; + my @tokens; - if ($menulevel eq "") { - @menu = grep { !/--/ } @{ $self->{ORDER} }; - } else { - @menu = grep { /^${menulevel}--/ } @{ $self->{ORDER} }; - } - - my @a = split /;/, $myconfig->{acs}; - my $excl = (); + while (my ($key, $value) = each %{ $node->{params} }) { + push @tokens, uri_encode($key, 1) . "=" . uri_encode($value, 1); + } - # remove --AR, --AP from array - grep { ($a, $b) = split /--/; s/--$a$//; } @a; + join '?', $href, grep $_, join '&', @tokens; + } +} - map { $excl{$_} = 1 } @a; +sub name_for_node { + $::locale->text($_[1]{name}) +} - @a = (); - map { push @a, $_ unless $excl{$_} } (@menu); +sub parse_instance_conf_string { + my ($self, $setting) = @_; + return $::instance_conf->data->{$setting}; +} - $main::lxdebug->leave_sub(); +sub clear_access { + my ($self) = @_; + for my $node ($self->tree_walk("all")) { + delete $node->{visible}; + delete $node->{visible_children}; + } +} - return @a; +sub set_access { + my ($self) = @_; + # 1. evaluate access for all + # 2. if a menu has no visible children, its not visible either + + for my $node (reverse $self->tree_walk("all")) { + $node->{visible} = $node->{access} ? $self->parse_access_string($node) + : !$node->{children} ? 1 + : $node->{visible_children} ? 1 + : 0; + if ($node->{visible} && $node->{parent}) { + $self->{by_id}{ $node->{parent} }{visible_children} = 1; + } + } } 1; -