1 # Vend::Parse - Parse Interchange tags
3 # $Id: Parse.pm,v 2.44 2007-12-19 12:33:44 pajamian Exp $
5 # Copyright (C) 2002-2007 Interchange Development Group
6 # Copyright (C) 1996-2002 Red Hat, Inc.
8 # This program was originally based on Vend 0.2 and 0.3
9 # Copyright 1995 by Andrew M. Wilcox <amw@wilcoxsolutions.com>
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public
22 # License along with this program; if not, write to the Free
23 # Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
31 use Vend::Interpolate;
33 use Vend::Data qw/product_field/;
37 @ISA = qw(Exporter Vend::Parser);
39 $VERSION = substr(q$Revision: 2.44 $, 10);
42 @EXPORT_OK = qw(find_matching_end);
45 no warnings qw(uninitialized numeric);
47 use vars qw($VERSION);
49 my($CurrentSearch, $CurrentCode, $CurrentDB, $CurrentWith, $CurrentItem);
50 my(@SavedSearch, @SavedCode, @SavedDB, @SavedWith, @SavedItem);
64 bounce => [qw( href if )],
65 goto => [qw( name if)],
66 label => [qw( name )],
67 if => [qw( type term op compare )],
68 unless => [qw( type term op compare )],
69 or => [qw( type term op compare )],
70 and => [qw( type term op compare )],
71 restrict => [qw( enable )],
146 or => sub { return &Vend::Interpolate::tag_if(@_, 1) },
147 and => sub { return &Vend::Interpolate::tag_if(@_, 1) },
148 if => \&Vend::Interpolate::tag_if,
149 unless => \&Vend::Interpolate::tag_unless,
159 output => sub { return '' },
160 bounce => sub { return '' },
161 if => \&Vend::Interpolate::tag_self_contained_if,
162 unless => \&Vend::Interpolate::tag_unless,
163 or => sub { return &Vend::Interpolate::tag_self_contained_if(@_, 1) },
164 and => sub { return &Vend::Interpolate::tag_self_contained_if(@_, 1) },
165 goto => sub { return '' },
166 label => sub { return '' },
170 ## Put here because we need to call keys %Routine
171 ## Restricts execution of tags by tagname
172 $Routine{restrict} = sub {
173 my ($enable, $opt, $body) = @_;
174 my $save = $Vend::Cfg->{AdminSub};
176 my $save_restrict = $Vend::restricted;
178 $opt->{log} ||= 'all';
181 if("\L$opt->{policy}" eq 'allow') {
182 # Accept all, deny only ones defined in disable
184 $opt->{policy} = 'allow';
187 # This is default, deny all except enabled
189 $opt->{policy} = 'deny';
193 $enable and @enable = split /[\s,\0]+/, $enable;
194 $opt->{disable} and @disable = split /[\s,\0]+/, $opt->{disable};
196 for(@enable, @disable) {
203 $restrict{$_} = $default;
206 $restrict{$_} = undef for @enable;
207 $restrict{$_} = 1 for @disable;
208 $restrict{$_} = 1 for keys %$save;
210 $Vend::Cfg->{AdminSub} = \%restrict;
211 $Vend::restricted = join " ",
212 'default=' . $opt->{policy},
213 'enable=' . join(",", @enable),
214 'disable=' . join(",", @disable),
215 'log=' . $opt->{log},
219 $out = Vend::Interpolate::interpolate_html($body);
221 $Vend::restricted = $save_restrict;
222 $Vend::Cfg->{AdminSub} = $save;
239 'condition' => 'compare',
245 'condition' => 'compare',
251 my %attrDefault = ();
254 getlocale => 'setlocale get=1',
255 process_search => 'area href=search',
258 my %Interpolate = ();
260 my %NoReparse = ( qw/
272 add_tags($Global::UserTag);
274 foreach $tag (keys %Routine) {
276 if ! defined $Order{$tag};
277 next if defined $PosNumber{$tag};
278 $PosNumber{$tag} = scalar @{$Order{$tag}};
285 my $self = new Vend::Parser;
287 add_tags($Vend::Cfg->{UserTag})
288 unless $Vend::Tags_added++;
293 $self->destination('');
297 $self->{OUT} = $self->{DEFAULT_OUT} = \$string;
299 #::logDebug("OUT=$self->{OUT}");
301 if (! $Initialized) {
302 $Initialized = $self;
303 $self->{TOPLEVEL} = 1;
310 my ($s, $name, $attr) = @_;
311 $s->{_outname} ||= [];
313 if(! defined $name) {
314 pop @{$s->{_outname}};
315 $name = pop @{$s->{_outname}};
319 push @{$s->{_outname}}, $name;
322 #::logDebug("destination set to '$name'");
326 $s->{OUT} = \$string;
327 push @Vend::Output, $s->{OUT};
329 my $nary = $Vend::OutPtr{$name} ||= [];
330 push @$nary, $#Vend::Output;
333 #::logDebug("destination extended output settings");
335 my $fary = $Vend::OutFilter{$name};
338 $Vend::MultiOutput = 1;
339 if(! $Vend::OutFilter{''}) {
341 push @$ary, \&Vend::Interpolate::substitute_image
342 unless $::Pragma->{no_image_rewrite};
343 $Vend::OutFilter{''} = $ary;
347 $fary = $Vend::OutFilter{$name} = [];
348 if($attr->{output_filter}) {
349 my $filt = $attr->{output_filter};
352 $$ref = Vend::Interpolate::filter_value($filt, $$ref);
356 if (! $attr->{no_image_parse} and ! $::Pragma->{no_image_rewrite}) {
357 push @$fary, \&Vend::Interpolate::substitute_image;
359 if ($attr->{output_extended}) {
360 $Vend::OutExtended{$name} = $attr;
367 my %noRearrange = qw//;
374 addAttr => \%addAttr,
375 attrAlias => \%attrAlias,
376 attrDefault => \%attrDefault,
377 Documentation => \%Documentation,
378 hasEndTag => \%hasEndTag,
379 NoReparse => \%NoReparse,
380 noRearrange => \%noRearrange,
381 Implicit => \%Implicit,
382 Interpolate => \%Interpolate,
384 PosNumber => \%PosNumber,
385 PosRoutine => \%PosRoutine,
386 Routine => \%Routine,
389 my @myRefs = keys %myRefs;
393 #::logDebug("Parse-do_tag: tag=$tag caller=" . caller() . " args=" . ::uneval_it(\@_) );
394 if (defined $Vend::Cfg->{AdminSub}{$tag}) {
396 if($Vend::restricted) {
398 "Tag '%s' in execution-restricted area: %s",
403 elsif (! $Vend::admin) {
404 die errmsg("Unauthorized for admin tag %s", $tag)
409 if (! defined $Routine{$tag} and $Global::AccumulateCode) {
410 #::logDebug("missing $tag, trying code_from_file");
413 #::logDebug("missing $tag found alias=$tag");
416 $Routine{$tag} = Vend::Config::code_from_file('UserTag', $tag)
421 if (! defined $Routine{$tag}) {
422 #::logDebug("missing $tag, but didn't try code_from_file?");
423 if (! $Alias{$tag}) {
424 ::logError("Tag '$tag' not defined.");
432 my @args = @$ref{ @{$Order{$tag}} };
433 push @args, $ref if $addAttr{$tag};
434 #::logDebug("Parse-do_tag: args now=" . ::uneval_it(\@args) );
435 $Initialized->start($tag, $ref);
439 ( ref($_[-1]) && scalar @{$Order{$tag}} > scalar @_ and ! $noRearrange{$tag})
444 $text = shift if $hasEndTag{$tag};
445 my @args = @$ref{ @{$Order{$tag}} };
446 push @args, $ref if $addAttr{$tag};
447 #::logDebug("Parse-do_tag: args now=" . ::uneval_it(\@args) );
448 return &{$Routine{$tag}}(@args, $text || undef);
451 #::logDebug("Parse-do_tag tag=$tag: args now=" . ::uneval_it(\@_) );
452 return &{$Routine{$tag}}(@_);
458 #::logDebug("resolving args for $tag, attrAlias = $attrAlias{$tag}");
459 if (! defined $Routine{$tag} and $Global::AccumulateCode) {
460 #::logDebug("missing $tag, trying code_from_file");
461 $Routine{$tag} = Vend::Config::code_from_file('UserTag', $tag);
464 return @_ unless defined $Routine{$tag};
467 if(defined $attrAlias{$tag}) {
469 while (($k, $v) = each %{$attrAlias{$tag}} ) {
470 #::logDebug("checking alias $k -> $v");
471 next unless defined $ref->{$k};
472 $ref->{$v} = $ref->{$k};
475 if (defined $attrDefault{$tag}) {
477 while (($k, $v) = each %{$attrDefault{$tag}}) {
478 next if defined $ref->{$k};
479 #::logDebug("using default $k = $v");
483 @list = @{$ref}{@{$Order{$tag}}};
484 push @list, $ref if defined $addAttr{$tag};
485 push @list, (shift || (defined $ref->{body} ? $ref->{body} : '')) if $hasEndTag{$tag};
492 return unless $ref->{Routine} or $ref->{Alias};
495 foreach $area (@myRefs) {
496 next unless $ref->{$area};
497 if($area eq 'Routine') {
498 for (keys %{$ref->{$area}}) {
499 $myRefs{$area}->{$_} = $ref->{$area}->{$_};
503 elsif ($area =~ /HTML$/) {
504 for (keys %{$ref->{$area}}) {
505 $myRefs{$area}->{$_} =
506 defined $myRefs{$area}->{$_}
507 ? $ref->{$area}->{$_} .'|'. $myRefs{$area}->{$_}
508 : $ref->{$area}->{$_};
512 Vend::Util::copyref $ref->{$area}, $myRefs{$area};
515 for (keys %{$ref->{Routine}}) {
516 $Order{$_} = [] if ! $Order{$_};
517 next if defined $PosNumber{$_};
518 $PosNumber{$_} = scalar @{$Order{$_}};
527 my($self, $text) = @_;
528 ${$self->{OUT}} .= $text;
531 my %Monitor = ( qw( tag_ary 1 ) );
534 my ($orig, $attr, $attrseq) = @_;
537 $orig .= qq{ \U$_="} ; # syntax color "
538 $attr->{$_} =~ s/"/\\"/g;
539 $orig .= $attr->{$_};
545 my %implicitHTML = (qw/checked CHECKED selected SELECTED/);
547 sub format_html_attribute {
548 my($attr, $val) = @_;
549 if(defined $implicitHTML{$attr}) {
550 return $implicitHTML{$attr};
552 $val =~ s/"/"/g;
553 return qq{$attr="$val"};
556 sub resolve_if_unless {
558 if(defined $attr->{'unless'}) {
559 return '' if $attr->{'unless'} =~ /^\s*0?\s*$/;
560 return '' if ! $attr->{'unless'};
563 elsif (defined $attr->{'if'}) {
565 ($attr->{'if'} and $attr->{'if'} !~ /^\s*0?\s*$/);
572 my ($name, $buf) = @_;
577 $$buf =~ s!.*?\[label\s+(?:name\s*=\s*(?:["'])?)?($name)['"]*\s*\]!!is
579 $$buf =~ s:.*?</body\s*>::is
588 $msg =~ s/\(eval\s+\d+/(tag '$Vend::CurrentTag'/;
595 my($self, $tag, $attr, $attrseq, $origtext, $empty_container) = @_;
596 $tag =~ tr/-/_/; # canonical
597 $Vend::CurrentTag = $tag = lc $tag;
598 #::logDebug("start tag=$tag");
599 my $buf = \$self->{_buf};
602 if (defined $Vend::Cfg->{AdminSub}{$tag}) {
604 if($Vend::restricted) {
606 $Vend::restricted =~ /\blog=(\w+)/ and $log = lc $1;
607 undef $log if $log eq 'none' or
608 ($log eq 'once' and $Vend::restricted_err{$origtext}++);
611 "Restricted tag (%s) attempted during restriction '%s'",
616 ${$self->{OUT}} .= $origtext;
619 elsif (! $Vend::admin) {
623 "Unauthorized for admin tag %s",
627 return ($self->{ABORT} = 1);
632 # $attr is reference to a HASH, $attrseq is reference to an ARRAY
634 if (! defined $Routine{$tag} and $Global::AccumulateCode) {
636 if($newtag = $Alias{$tag}) {
637 $newtag =~ s/\s+.*//s;
638 Vend::Config::code_from_file('UserTag', $newtag)
639 unless $Routine{$newtag};
642 Vend::Config::code_from_file('UserTag', $tag);
646 unless (defined $Routine{$tag}) {
647 if(defined $Alias{$tag}) {
649 my $alias = $Alias{$tag};
652 #::logDebug("origtext: $origtext tag=$tag alias=$alias");
653 $origtext =~ s/$tag/$alias/i
655 if ($alias =~ /\s/) {
656 # keep old behaviour for aliases like
657 # process_search => 'area href=search'
658 # otherwise we process it like any other tag
659 $$buf = $origtext . $$buf;
665 #::logDebug("no alias. origtext: $origtext");
666 ${$self->{OUT}} .= $origtext;
672 foreach $trib (@$attrseq) {
674 if(defined $attrAlias{$tag} and $attrAlias{$tag}{$trib}) {
675 my $new = $attrAlias{$tag}{$trib} ;
676 $attr->{$new} = delete $attr->{$trib};
679 # Parse tags within tags, only works if the [ is the
681 next unless $attr->{$trib} =~ /\[\w+[-\w]*\s*(?s:.)*\]/;
683 my $p = new Vend::Parse;
684 $p->parse($attr->{$trib});
685 $attr->{$trib} = ${$p->{OUT}};
688 if (defined $attrDefault{$tag}) {
690 while (($k, $v) = each %{$attrDefault{$tag}}) {
691 next if defined $attr->{$k};
692 #::logDebug("using default $k = $v");
697 $attr->{enable_html} = 1 if $Vend::Cfg->{Promiscuous};
700 defined $NoReparse{$tag}
701 || defined $attr->{reparse}
702 || $::Pragma->{no_default_reparse}
707 #::logDebug("tag=$tag order=$Order{$tag}");
708 # Check for old-style positional tag
709 if(!@$attrseq and $origtext =~ s/\[[-\w]+\s+//i) {
710 $origtext =~ s/\]$//;
711 $attr->{interpolate} = 1 if defined $Interpolate{$tag};
712 if(defined $PosNumber{$tag}) {
713 if($PosNumber{$tag} > 1) {
714 @args = split /\s+/, $origtext, $PosNumber{$tag};
715 push(@args, undef) while @args < $PosNumber{$tag};
717 elsif ($PosNumber{$tag}) {
721 @{$attr}{ @{ $Order{$tag} } } = @args;
722 $routine = $PosRoutine{$tag} || $Routine{$tag};
725 $routine = $Routine{$tag};
726 $attr->{interpolate} = 1
727 if defined $Interpolate{$tag} && ! defined $attr->{interpolate};
728 @args = @{$attr}{ @{ $Order{$tag} } };
730 $args[scalar @{$Order{$tag}}] = $attr if $addAttr{$tag};
732 #::logDebug("Interpolate value now='$attr->{interpolate}'") if$Monitor{$tag};
735 #::logDebug(<<EOF) if $Monitor{$tag};
738 #has_end=$hasEndTag{$tag}
740 #interpolate=$attr->{interpolate}
744 if($tag eq 'output') {
745 $self->destination($attr->{name}, $attr);
748 elsif($tag eq 'bounce') {
749 #::logDebug("bouncing...options=" . ::uneval($attr));
750 return 1 if resolve_if_unless($attr);
751 if(! $attr->{href} and $attr->{page}) {
752 $attr->{href} = Vend::Interpolate::tag_area($attr->{page});
755 $attr->{href} = header_data_scrub($attr->{href});
757 $Vend::StatusLine = '' if ! $Vend::StatusLine;
758 $Vend::StatusLine .= "\n" if $Vend::StatusLine !~ /\n$/;
759 $Vend::StatusLine .= <<EOF if $attr->{target};
760 Window-Target: $attr->{target}
762 $attr->{status} ||= '302 moved';
763 $Vend::StatusLine .= <<EOF;
764 Status: $attr->{status}
765 Location: $attr->{href}
767 #::logDebug("bouncing...status line=\n$Vend::StatusLine");
769 $Initialized->{_buf} = '';
771 my $body = qq{Redirecting to <a href="%s">%s</a>.};
772 $body = errmsg($body, $attr->{href}, $attr->{href});
773 #::logDebug("bouncing...body=$body");
774 $::Pragma->{download} = 1;
780 elsif($tag eq 'goto') {
781 return 1 if resolve_if_unless($attr);
784 $Initialized->{_buf} = '';
787 return ($self->{SEND} = 1);
789 goto_buf($args[0], $buf);
791 $self->{SEND} = 1 if ! $$buf;
796 local($SIG{__DIE__}) = \&eval_die;
798 #::logDebug("output attr=$attr->{_output}");
799 $self->destination($attr->{_output}) if $attr->{_output};
801 if($hasEndTag{$tag}) {
802 # Handle embedded tags, but only if interpolate is
803 # defined (always if using old tags)
804 #::logDebug("look end for $tag, buf=" . length($$buf) );
805 $tmpbuf = $empty_container ? '' : find_matching_end($aliasname || $tag, $buf);
806 #::logDebug("FOUND end for $tag\nBuf " . length($$buf) . ":\n" . $$buf . "\nTmpbuf:\n$tmpbuf\n");
807 if ($attr->{interpolate} and !$empty_container) {
808 my $p = new Vend::Parse;
809 my $tagsave = $Vend::CurrentTag;
811 $Vend::CurrentTag = $tagsave;
812 $tmpbuf = $p->{ABORT} ? '' : ${$p->{OUT}};
814 if ($attr->{'hide'}) {
815 $routine->(@args,$tmpbuf);
817 elsif($attr->{reparse} ) {
818 $$buf = ($routine->(@args,$tmpbuf)) . $$buf;
821 ${$self->{OUT}} .= $routine->(@args,$tmpbuf);
824 elsif ($attr->{'hide'}) {
827 elsif($attr->{interpolate}) {
828 $$buf = $routine->(@args) . $$buf;
831 ${$self->{OUT}} .= $routine->(@args);
834 $self->{SEND} = $attr->{'send'} || undef;
835 #::logDebug("Returning from $tag");
836 $self->destination() if $attr->{_output};
841 my($self, $tag) = @_;
843 $tag =~ tr/-/_/; # canonical
844 ${$self->{OUT}} .= "[/$save]";
853 my $close = "</$tag>";
854 ($canon = $tag) =~ s/_/[-_]/g;
856 $$buf =~ s!<$canon\s!<$tag !ig;
857 $$buf =~ s!</$canon\s*>!</$tag>!ig;
858 my $first = index($$buf, $close);
859 return undef if $first < 0;
860 my $int = index($$buf, $open);
862 #::logDebug("find_html_end: tag=$tag open=$open close=$close $first=$first pos=$pos int=$int");
863 while( $int > -1 and $int < $first) {
865 $first = index($$buf, $close, $first + 1);
866 $int = index($$buf, $open, $pos);
867 #::logDebug("find_html_end: tag=$tag open=$open close=$close $first=$first pos=$pos int=$int");
869 #::logDebug("find_html_end: tag=$tag open=$open close=$close $first=$first pos=$pos int=$int");
870 return undef if $first < 0;
871 $first += length($close);
872 #::logDebug("find_html_end (add close): tag=$tag open=$open close=$close $first=$first pos=$pos int=$int");
873 $out = substr($$buf, 0, $first);
874 substr($$buf, 0, $first) = '';
878 sub find_matching_end {
884 my $close = "[/$tag]";
885 ($canon = $tag) =~ s/_/[-_]/g;
887 $$buf =~ s!\[$canon\s![$tag !ig;
889 $$buf =~ s!\[/$canon\]![/$tag]!ig;
890 my $first = index($$buf, $close);
899 my $int = index($$buf, $open);
901 while( $int > -1 and $int < $first) {
903 $first = index($$buf, $close, $first + 1);
904 $int = index($$buf, $open, $pos);
906 $out = substr($$buf, 0, $first);
907 $first = $first < 0 ? $first : $first + length($close);
908 substr($$buf, 0, $first) = '';
912 # Passed some string that might be HTML-style attributes
913 # or might be positional parameters, does the right thing
915 my ($buf, $attrhash, $attrseq) = (@_);
916 return '' if ! $$buf;
921 while ($$buf =~ s|^(([a-zA-Z][-a-zA-Z0-9._]*)\s*)||) {
927 # The attribute might take an optional value (first we
928 # check for an unquoted value)
929 if ($$buf =~ s|(^=\s*([^\"\'\]\s][^\]\s]*)\s*)||) {
932 HTML::Entities::decode($val);
933 # or quoted by " or '
934 } elsif ($$buf =~ s~(^=\s*([\"\'\`\|])(.*?)\2\s*)~~s) {
938 HTML::Entities::decode($val);
940 $val = Vend::Interpolate::tag_calc($val);
948 $val =~ /__[A-Z]\w*[A-Za-z]__|\[.*\]/s
950 my $p = new Vend::Parse;
955 # truncated just after the '=' or inside the attribute
956 } elsif ($$buf =~ m|^(=\s*)$| or
957 $$buf =~ m|^(=\s*[\"\'].*)|s) {
961 # assume attribute with implicit value, which
962 # means in Interchange no value is set and the
963 # eaten value is grown. Note that you should
964 # never use an implicit tag when setting up an Alias.
968 $attrhash->{$attr} = $val;
969 push(@attrseq, $attr);
971 unshift(@$attrseq, @attrseq);
975 # Implicit tag attributes
976 # These are deprecated. Please do not document them,
977 # as they may go away in the future.
979 my($self, $tag, $attr) = @_;
980 # 'int' is special in that it doesn't get pushed on @attrseq
981 return ('interpolate', 1, 1) if $attr eq 'int';
982 return ($attr, undef) unless defined $Implicit{$tag} and $Implicit{$tag}{$attr};
983 my $imp = $Implicit{$tag}{$attr};
984 return ($attr, $imp) if $imp =~ s/^$attr=//i;
985 return ( $Implicit{$tag}{$attr}, $attr );