X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FMenu.pm;h=f01b341be5412eaeb301e059a22c7cee4963572c;hb=a8b18c65bfd47c845e641fb0fff9587f4122bf9d;hp=b10fc67b41a2940e3dbc0e70a517c63b60f05987;hpb=d319704a66e9be64da837ccea10af6774c2b0838;p=kivitendo-erp.git diff --git a/SL/Menu.pm b/SL/Menu.pm index b10fc67b4..f01b341be 100644 --- a/SL/Menu.pm +++ b/SL/Menu.pm @@ -1,129 +1,247 @@ -#===================================================================== -# 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 YAML (); +use File::Spec; +use SL::MoreCommon qw(uri_encode); + +our $yaml_xs; +BEGIN { + $yaml_xs = eval { require YAML::XS }; +} + +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; + if ($yaml_xs) { + $data = YAML::XS::LoadFile(File::Spec->catfile($path, $file)); + } else { + $data = YAML::LoadFile(File::Spec->catfile($path, $file)); + } + _merge($nodes, $nodes_by_id, $data); + } - bless $self, $type; -} -sub menuitem { - $main::lxdebug->enter_sub(); + my $self = bless { + nodes => $nodes, + by_id => $nodes_by_id, + }, $package; - my ($self, $myconfig, $form, $item) = @_; + $self->build_tree; - my $module = $form->{script}; - my $action = "section_menu"; - my $target = ""; + $menu_cache{$domain} = $self; + } else { + $menu_cache{$domain}->clear_access; + } + + $menu_cache{$domain}->set_access; - if ($self->{$item}{module}) { - $module = $self->{$item}{module}; + return $menu_cache{$domain}; +} + +sub _merge { + my ($nodes, $by_id, $data) = @_; + + die 'not an array ref' unless $data && 'ARRAY' eq ref $data; # TODO check this sooner, to get better diag to user + + for my $node (@$data) { + my $id = $node->{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}; + } + } } - if ($self->{$item}{action}) { - $action = $self->{$item}{action}; +} + +sub build_tree { + my ($self) = @_; + + # 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}"; } - if ($self->{$item}{target}) { - $target = $self->{$item}{target}; + + my %by_parent; + # order them by parent + for my $node ($self->nodes) { + push @{ $by_parent{ $node->{parent} } //= [] }, $node; } - my $level = $form->escape($item); - my $str = - qq|{path}&action=$action&level=$level&login=$form->{login}&password=$form->{password}|; - my @vars = qw(module action target href); + my $tree = { }; + $self->{by_id}{''} = $tree; - if ($self->{$item}{href}) { - $str = qq|{$item}{href}|; - @vars = qw(module target href); + + for (keys %by_parent) { + my $parent = $self->{by_id}{$_}; + $parent->{children} = [ sort { $a->{order} <=> $b->{order} } @{ $by_parent{$_} } ]; } - map { delete $self->{$item}{$_} } @vars; + _set_level_rec($tree->{children}, 0); - # 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); - } + $self->{tree} = $tree->{children}; +} - if ($target) { - $str .= qq| target=$target|; +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 access_control { - $main::lxdebug->enter_sub(); +sub _tree_walk_rec { + my ($ary_ref, $all) = @_; + map { $_->{children} ? ($_, _tree_walk_rec($_->{children}, $all)) : ($_) } grep { $all || $_->{visible} } @$ary_ref; +} - my ($self, $myconfig, $menulevel) = @_; +sub parse_access_string { + my ($self, $node) = @_; - my @menu = (); + my @stack; + my $cur_ary = []; - if ($menulevel eq "") { - @menu = grep { !/--/ } @{ $self->{ORDER} }; - } else { - @menu = grep { /^${menulevel}--/ } @{ $self->{ORDER} }; + push @stack, $cur_ary; + + my $access = $node->{access}; + + 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; + + } elsif ($token eq ")") { + pop @stack; + if (!@stack) { + die "Error while parsing menu entry $node->{id}: missing '('"; + } + $cur_ary = $stack[-1]; + + } elsif (($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); + } + } + } + + if ($access) { + die "Error while parsing menu entry $node->{id}: unrecognized token at the start of '$access'\n"; } - my @a = split /;/, $myconfig->{acs}; - my $excl = (); + if (1 < scalar @stack) { + die "Error while parsing menu entry $node->{id}: Missing ')'\n"; + } - # remove --AR, --AP from array - grep { ($a, $b) = split /--/; s/--$a$//; } @a; + return SL::Auth::evaluate_rights_ary($stack[0]); +} - map { $excl{$_} = 1 } @a; +sub href_for_node { + my ($self, $node) = @_; - @a = (); - map { push @a, $_ unless $excl{$_} } (@menu); + return undef if !$node->{href} && !$node->{module} && !$node->{params}; - $main::lxdebug->leave_sub(); + my $href = $node->{href} || $node->{module} || 'controller.pl'; + my @tokens; - return @a; + while (my ($key, $value) = each %{ $node->{params} }) { + push @tokens, uri_encode($key, 1) . "=" . uri_encode($value, 1); + } + + return join '?', $href, grep $_, join '&', @tokens; +} + +sub name_for_node { + $::locale->text($_[1]{name}) +} + +sub parse_instance_conf_string { + my ($self, $setting) = @_; + return $::instance_conf->data->{$setting}; +} + +sub clear_access { + my ($self) = @_; + for my $node ($self->tree_walk("all")) { + delete $node->{visible}; + delete $node->{visible_children}; + } +} + +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;