X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FMenu.pm;h=0c6df2247a976d6efb2939b9fd710bfa7d26e61e;hb=95b5d54bac9dc0cb47c67444c9e19c1d68b0d520;hp=06ea6ee43ed44c04b21459694f7e7ebce836b05b;hpb=63a8dae2dc231828b3d29d082b25acf317f26f9b;p=kivitendo-erp.git diff --git a/SL/Menu.pm b/SL/Menu.pm index 06ea6ee43..0c6df2247 100644 --- a/SL/Menu.pm +++ b/SL/Menu.pm @@ -1,130 +1,177 @@ -#===================================================================== -# 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 SL::Inifile; +use File::Spec; +use SL::MoreCommon qw(uri_encode); +use SL::YAML; -use strict; +our %menu_cache; sub new { - $main::lxdebug->enter_sub(); + my ($package, $domain) = @_; + + if (!$menu_cache{$domain}) { + my $path = File::Spec->catdir('menus', $domain); + + opendir my $dir, $path or die "can't open $path: $!"; + my @files = sort grep -f "$path/$_", grep /\.yaml$/, readdir $dir; + close $dir; + + 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: $@"; + }; + + # check if this file is internally consistent. + die 'not an array ref' unless $data && 'ARRAY' eq ref $data; # TODO get better diag to user + + # 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; + + _merge($nodes, $nodes_by_id, $data); + } - my ($type, @menufiles) = @_; - my $self = bless {}, $type; - my @order; + my $self = bless { + nodes => $nodes, + by_id => $nodes_by_id, + }, $package; - foreach my $menufile (grep { -f } @menufiles) { - my $inifile = Inifile->new($menufile); + $self->build_tree; - push @order, @{ delete($inifile->{ORDER}) || [] }; - $self->{$_} = $inifile->{$_} for keys %{ $inifile }; + $menu_cache{$domain} = $self; + } else { + $menu_cache{$domain}->clear_access; } - $self->{ORDER} = \@order; + $menu_cache{$domain}->set_access; - $self->set_access(); + return $menu_cache{$domain}; +} - $main::lxdebug->leave_sub(); +sub _merge { + my ($nodes, $by_id, $data) = @_; - return $self; -} + for my $node (@$data) { + my $id = $node->{id}; -sub menuitem_new { - $main::lxdebug->enter_sub(LXDebug::DEBUG2()); + die "menu: node with name '$node->{name}' does not have an id" if !$id; - my ($self, $name, $item) = @_; + my $merge_to = $by_id->{$id}; - my $form = $main::form; - my $myconfig = \%main::myconfig; + if (!$merge_to) { + push @$nodes, $node; + $by_id->{$id} = $node; + next; + } - my $module = $self->{$name}->{module} || $form->{script}; - my $action = $self->{$name}->{action}; + # 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}; + } + } + } +} - $item->{target} = $self->{$name}->{target} || "main_window"; - $item->{href} = $self->{$name}->{href} || "${module}?action=" . $form->escape($action); +sub build_tree { + my ($self) = @_; - my @vars = qw(module target href); - push @vars, 'action' unless ($self->{$name}->{href}); + # 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}"; + } - map { delete $self->{$name}{$_} } @vars; + my %by_parent; + # order them by parent + for my $node ($self->nodes) { + push @{ $by_parent{ $node->{parent} // '' } //= [] }, $node; + } - # add other params - foreach my $key (keys %{ $self->{$name} }) { - my ($value, $conf) = split(m/=/, $self->{$name}->{$key}, 2); - $value = $myconfig->{$value} . "/$conf" if ($conf); - $item->{href} .= "&" . $form->escape($key) . "=" . $form->escape($value); + # 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; + } + } } - $main::lxdebug->leave_sub(LXDebug::DEBUG2()); -} + my $tree = { }; + $self->{by_id}{''} = $tree; -sub access_control { - $main::lxdebug->enter_sub(2); - my ($self, $myconfig, $menulevel) = @_; + for (keys %by_parent) { + my $parent = $self->{by_id}{$_}; + $parent->{children} = [ sort { $a->{order} <=> $b->{order} } @{ $by_parent{$_} } ]; + } - my @menu = (); + _set_level_rec($tree->{children}, 0); - if (!$menulevel) { - @menu = grep { !/--/ } @{ $self->{ORDER} }; - } else { - @menu = grep { /^${menulevel}--/ } @{ $self->{ORDER} }; + $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}; } +} + +sub nodes { + @{ $_[0]{nodes} } +} - $main::lxdebug->leave_sub(2); +sub tree_walk { + my ($self, $all) = @_; - return @menu; + _tree_walk_rec($self->{tree}, $all); } -sub parse_access_string { - my $self = shift; - my $key = shift; - my $access = shift; +sub _tree_walk_rec { + my ($ary_ref, $all) = @_; + map { $_->{children} ? ($_, _tree_walk_rec($_->{children}, $all)) : ($_) } grep { $all || $_->{visible} } @$ary_ref; +} - my $form = $main::form; - my $auth = $main::auth; - my $myconfig = \%main::myconfig; +sub parse_access_string { + my ($self, $node) = @_; my @stack; my $cur_ary = []; push @stack, $cur_ary; - while ($access =~ m/^([a-z_]+|\||\&|\(|\)|\s+)/) { + my $access = $node->{access}; + + while ($access =~ m/^([a-z_\/]+|\!|\||\&|\(|\)|\s+)/) { my $token = $1; substr($access, 0, length($1)) = ""; @@ -139,81 +186,81 @@ sub parse_access_string { } elsif ($token eq ")") { pop @stack; if (!@stack) { - $form->error("Error in menu.ini for entry ${key}: missing '('"); + die "Error while parsing menu entry $node->{id}: missing '('"; } $cur_ary = $stack[-1]; - } elsif (($token eq "|") || ($token eq "&")) { + } elsif (($token eq "|") || ($token eq "&") || ($token eq "!")) { push @{$cur_ary}, $token; } else { - push @{$cur_ary}, $auth->check_right($form->{login}, $token, 1); + 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) { - $form->error("Error in menu.ini for entry ${key}: unrecognized token at the start of '$access'\n"); + die "Error while parsing menu entry $node->{id}: unrecognized token at the start of '$access'\n"; } if (1 < scalar @stack) { - $main::form->error("Error in menu.ini for entry ${key}: Missing ')'\n"); + die "Error while parsing menu entry $node->{id}: Missing ')'\n"; } return SL::Auth::evaluate_rights_ary($stack[0]); } -sub set_access { - my $self = shift; - - my $key; - - foreach $key (@{ $self->{ORDER} }) { - my $entry = $self->{$key}; +sub href_for_node { + my ($self, $node) = @_; - $entry->{GRANTED} = $entry->{ACCESS} ? $self->parse_access_string($key, $entry->{ACCESS}) : 1; - $entry->{IS_MENU} = $entry->{submenu} || ($key !~ m/--/); - $entry->{NUM_VISIBLE_CHILDREN} = 0; + return undef if !$node->{href} && !$node->{module} && !$node->{params}; - if ($key =~ m/--/) { - my $parent = $key; - substr($parent, rindex($parent, '--')) = ''; - $entry->{GRANTED} &&= $self->{$parent}->{GRANTED}; - } - - $entry->{VISIBLE} = $entry->{GRANTED}; - } + return $node->{href_for_node} ||= do { + my $href = $node->{href} || $node->{module} || 'controller.pl'; + my @tokens; - foreach $key (reverse @{ $self->{ORDER} }) { - my $entry = $self->{$key}; - - if ($entry->{IS_MENU}) { - $entry->{VISIBLE} &&= $entry->{NUM_VISIBLE_CHILDREN} > 0; + while (my ($key, $value) = each %{ $node->{params} }) { + push @tokens, uri_encode($key, 1) . "=" . uri_encode($value, 1); } - next if (($key !~ m/--/) || !$entry->{VISIBLE}); - - my $parent = $key; - substr($parent, rindex($parent, '--')) = ''; - $self->{$parent}->{NUM_VISIBLE_CHILDREN}++; + join '?', $href, grep $_, join '&', @tokens; } +} -# $self->dump_visible(); +sub name_for_node { + $::locale->text($_[1]{name}) +} - $self->{ORDER} = [ grep { $self->{$_}->{VISIBLE} } @{ $self->{ORDER} } ]; +sub parse_instance_conf_string { + my ($self, $setting) = @_; + return $::instance_conf->data->{$setting}; +} - { no strict 'refs'; - # ToDO: fix this. nuke and pave algorithm without type checking screams for problems. - map { delete @{$self->{$_}}{qw(GRANTED IS_MENU NUM_VISIBLE_CHILDREN VISIBLE ACCESS)} if ($_ ne 'ORDER') } keys %{ $self }; +sub clear_access { + my ($self) = @_; + for my $node ($self->tree_walk("all")) { + delete $node->{visible}; + delete $node->{visible_children}; } } -sub dump_visible { - my $self = shift; - foreach my $key (@{ $self->{ORDER} }) { - my $entry = $self->{$key}; - $main::lxdebug->message(0, "$entry->{GRANTED} $entry->{VISIBLE} $entry->{NUM_VISIBLE_CHILDREN} $key"); +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; -