# Module of TWiki Enterprise Collaboration Platform, http://TWiki.org/
#
# Copyright (C) 2001-2018 Peter Thoeny, peter[at]thoeny.org
# and TWiki Contributors. All Rights Reserved. TWiki Contributors
# are listed in the AUTHORS file in the root of this distribution.
# NOTE: Please extend that file, not this notice.
#
# 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 3
# of the License, or (at your option) any later version. For
# more details read LICENSE in the root of this distribution.
#
# 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.
#
# As per the GPL, removal of this notice is prohibited.
package TWiki::Render;
=pod
---+ package TWiki::Render
This module provides most of the actual HTML rendering code in TWiki.
=cut
use strict;
use Assert;
use Error qw(:try);
require TWiki::Time;
# Used to generate unique placeholders for when we lift blocks out of the
# text during rendering.
use vars qw( $placeholderMarker );
$placeholderMarker = 0;
# Used to generate unique anchors
my %anchornames = ();
# defaults for trunctation of summary text
my $TMLTRUNC = 162;
my $PLAINTRUNC = 70;
my $MINTRUNC = 16;
# max number of lines in a summary (best to keep it even)
my $SUMMARYLINES = 6;
# limiting lookbehind and lookahead for wikiwords and emphasis
# use like \b
#SMELL: they really limit the number of places emphasis can happen.
my $STARTWW = qr/^|(?<=[\s\(])/m;
my $ENDWW = qr/$|(?=[\s,.;:!?)])/m;
# marker used to tage the start of a table
my $TABLEMARKER = "\0\1\2TABLE\2\1\0";
# Marker used to indicate table rows that are valid header/footer rows
my $TRMARK = "is\1all\1th";
BEGIN {
# Do a dynamic 'use locale' for this module
if( $TWiki::cfg{UseLocale} ) {
require locale;
import locale();
}
}
=pod
---++ ClassMethod new ($session)
Creates a new renderer
=cut
sub new {
my ( $class, $session ) = @_;
my $this = bless( { session => $session }, $class );
return $this;
}
=begin twiki
---++ ObjectMethod finish()
Break circular references.
=cut
# Note to developers; please undef *all* fields in the object explicitly,
# whether they are references or not. That way this method is "golden
# documentation" of the live fields in the object.
sub finish {
my $this = shift;
undef $this->{NEWLINKFORMAT};
undef $this->{LINKTOOLTIPINFO};
undef $this->{SHOWTOPICTITLELINK};
undef $this->{LIST};
undef $this->{metaCache};
undef $this->{formDefCache};
undef $this->{session};
}
sub _newLinkFormat {
my $this = shift;
unless( $this->{NEWLINKFORMAT} ) {
$this->{NEWLINKFORMAT} =
$this->{session}->{prefs}->getPreferencesValue('NEWLINKFORMAT') ||
''
. '$text ';
}
return $this->{NEWLINKFORMAT};
}
=pod
---++ ObjectMethod renderParent($web, $topic, $meta, $params) -> $text
Render parent meta-data
=cut
sub renderParent {
my( $this, $web, $topic, $meta, $ah ) = @_;
my $dontRecurse = $ah->{dontrecurse} || 0;
my $noWebHome = $ah->{nowebhome} || 0;
my $prefix = $ah->{prefix} || '';
my $suffix = $ah->{suffix} || '';
my $usesep = $ah->{separator} || ' > ';
my $format = $ah->{format} || '[[$web.$topic][$topic]]';
return '' unless $web && $topic;
my %visited;
$visited{$web.'.'.$topic} = 1;
my $pWeb = $web;
my $pTopic;
my $text = '';
my $parentMeta = $meta->get( 'TOPICPARENT' );
my $parent;
my $store = $this->{session}->{store};
$parent = $parentMeta->{name} if $parentMeta;
my @stack;
while( $parent ) {
( $pWeb, $pTopic ) = $this->{session}->normalizeWebTopicName( $pWeb, $parent );
$parent = $pWeb.'.'.$pTopic;
last if( $noWebHome &&
( $pTopic eq $TWiki::cfg{HomeTopicName} ) ||
$visited{$parent} );
$visited{$parent} = 1;
$text = $format;
$text =~ s/\$web/$pWeb/g;
$text =~ s/\$topic/$pTopic/g;
unshift( @stack, $text );
last if $dontRecurse;
$parent = $store->getTopicParent( $pWeb, $pTopic );
}
$text = join( $usesep, @stack );
if( $text) {
$text = $prefix.$text if ( $prefix );
$text .= $suffix if ( $suffix );
}
return $text;
}
=pod
---++ ObjectMethod renderMoved($web, $topic, $meta, $params) -> $text
Render moved meta-data
=cut
sub renderMoved {
my( $this, $web, $topic, $meta, $params ) = @_;
my $text = '';
my $moved = $meta->get( 'TOPICMOVED' );
$web =~ s#\.#/#go;
if( $moved ) {
my( $fromWeb, $fromTopic ) = $this->{session}->normalizeWebTopicName( $web, $moved->{from} );
my( $toWeb, $toTopic ) = $this->{session}->normalizeWebTopicName( $web, $moved->{to} );
my $by = $moved->{by};
my $u = $by;
my $users = $this->{session}->{users};
$by = $users->webDotWikiName($u) if $u;
my $date = TWiki::Time::formatTime( $moved->{date}, '', 'gmtime' );
# Only allow put back if current web and topic match stored information
my $putBack = '';
if( $web eq $toWeb && $topic eq $toTopic ) {
$putBack = ' - '
. '
';
}
$text = CGI::i(
$this->{session}->i18n->maketext("[_1] moved from [_2] on [_3] by [_4]",
"$toWeb.$toTopic",
"$fromWeb.$fromTopic",
$date,
$by)) . $putBack;
}
return $text;
}
# Add a list item, of the given type and indent depth. The list item may
# cause the opening or closing of lists currently being handled.
sub _addListItem {
my( $this, $result, $type, $element, $indent ) = @_;
$indent =~ s/ /\t/g;
my $depth = length( $indent );
my $size = scalar( @{$this->{LIST}} );
# The whitespaces either side of the tags are required for the
# emphasis REs to work.
if( $size < $depth ) {
my $firstTime = 1;
while( $size < $depth ) {
push( @{$this->{LIST}}, { type=>$type, element=>$element } );
push @$result, ' <'.$element.">\n" unless( $firstTime );
push @$result, ' <'.$type.">\n";
$firstTime = 0;
$size++;
}
} else {
while( $size > $depth ) {
my $tags = pop( @{$this->{LIST}} );
push @$result, "\n".$tags->{element}.'>'.$tags->{type}.'> ';
$size--;
}
if( $size ) {
push @$result, "\n".$this->{LIST}->[$size-1]->{element}.'> ';
} else {
push @$result, "\n";
}
}
if ( $size ) {
my $oldt = $this->{LIST}->[$size-1];
if( $oldt->{type} ne $type ) {
push @$result, ' '.$oldt->{type}.'><'.$type.">\n";
pop( @{$this->{LIST}} );
push( @{$this->{LIST}}, { type=>$type, element=>$element } );
}
}
}
# Given that we have just seen the end of a table, work out the thead,
# tbody and tfoot sections
sub _addTHEADandTFOOT {
my( $lines ) = @_;
# scan back to the head of the table
my $i = scalar( @$lines ) - 1;
my @thRows;
my $inFoot = 1;
my $footLines = 0;
my $headLines = 0;
while( $i >= 0 && $lines->[$i] ne $TABLEMARKER ) {
if( $lines->[$i] =~ /^\s*$/ ) {
# Remove blank lines in tables; they generate spurious 's
splice( @$lines, $i, 1 );
}
elsif( $lines->[$i] =~ s/$TRMARK=(["'])(.*?)\1//i) {
if( $2 ) {
if( $inFoot ) {
$footLines++;
} else {
$headLines++;
}
} else {
$inFoot = 0;
$headLines = 0;
}
}
$i--;
}
$lines->[$i] = CGI::start_table(
{ class=>'twikiTable',
border => 1, cellspacing => 0, cellpadding => 0 });
if( $footLines && !$headLines) {
$headLines = $footLines;
$footLines = 0;
}
if( $footLines ) {
push( @$lines, '');
my $firstFoot = scalar( @$lines ) - $footLines;
splice( @$lines, $firstFoot, 0, '
');
} else {
push( @$lines, '');
}
if( $headLines ) {
splice( @$lines, $i + 1 + $headLines, 0, ' ');
splice( @$lines, $i + 1, 0, '');
} else {
splice( @$lines, $i + 1, 0, ' ');
}
}
sub _emitTR {
my ( $this, $theRow ) = @_;
$theRow =~ s/\t/ /g; # change tabs to space
$theRow =~ s/\s*$//; # remove trailing spaces
$theRow =~ s/(\|\|+)/'colspan'.$TWiki::TranslationToken.length($1).'|'/ge; # calc COLSPAN
my $cells = '';
my $containsTableHeader;
my $isAllTH = 1;
foreach( split( /\|/, $theRow ) ) {
my @attr;
# Avoid matching single columns
if ( s/colspan$TWiki::TranslationToken([0-9]+)//o ) {
push( @attr, colspan => $1 );
}
s/^\s+$/ /;
my( $l1, $l2 ) = ( 0, 0 );
if( /^(\s*).*?(\s*)$/ ) {
$l1 = length( $1 );
$l2 = length( $2 );
}
if( $l1 >= 2 ) {
if( $l2 <= 1 ) {
push( @attr, align => 'right' );
} else {
push( @attr, align => 'center' );
}
}
if( /^\s*\*(.*)\*\s*$/ ) {
$cells .= CGI::th( { @attr }, CGI::strong( " $1 " ))."\n";
} else {
$cells .= CGI::td( { @attr }, " $_ " )."\n";
$isAllTH = 0;
}
}
return CGI::Tr({ $TRMARK => $isAllTH }, $cells );
}
sub _fixedFontText {
my( $theText, $theDoBold ) = @_;
# preserve white space, so replace it by ' ' patterns
$theText =~ s/\t/ /g;
$theText =~ s|((?:[\s]{2})+)([^\s])|' ' x (length($1) / 2) . $2|eg;
$theText = CGI->b( $theText ) if $theDoBold;
return CGI->code( $theText );
}
# Build an HTML <Hn> element with suitable anchor for linking from %TOC%
sub _makeAnchorHeading {
my( $this, $text, $theLevel, $topic, $web ) = @_;
$text =~ s/^\s*(.*?)\s*$/$1/;
# - Build ' heading ' markup
# - Initial '' is needed to prevent subsequent matches.
# - filter out $TWiki::regex{headerPatternNoTOC} ( '!!' and '%NOTOC%' )
my $anchorName = $this->makeUniqueAnchorName( $web, $topic, $text, 0 );
# if the generated uniqe anchor name is 'compatible', it won't change:
my $compatAnchorName = $this->makeAnchorName( $anchorName, 1 );
# filter '!!', '%NOTOC%'
$text =~ s/$TWiki::regex{headerPatternNoTOC}//o;
my $html = '';
$html .= CGI::a( { name => $anchorName }, '' );
if( $compatAnchorName ne $anchorName ) {
$compatAnchorName = $this->makeUniqueAnchorName( $web, $topic, $anchorName, 1 );
$html .= CGI::a( { name => $compatAnchorName }, '');
}
$html .= ' '.$text.' ';
return $html;
}
=pod
---++ StaticMethod chompUtf8Fragment($str) -> $str
String truncation may happen in the middle of a UTF-8 byte sequence.
This function gets rid of the truncated fragment.
=cut
sub chompUtf8Fragment {
my $str = shift;
$str =~ /^((?:
[\x00-\x7e] |
[\xc2-\xdf][\x80-\xbf] |
[\xe0-\xef][\x80-\xbf]{2} |
[\xf0-\xf7][\x80-\xbf]{3}
)+)/x;
$str = $1;
return defined($str) ? $str : '_';
}
=pod
---++ ObjectMethod makeAnchorName($anchorName, $compatibilityMode) -> $anchorName
* =$anchorName= - the unprocessed anchor name
* =$compatibilityMode= - SMELL: compatibility with *what*?? Who knows. :-(
Build a valid HTML anchor name
=cut
sub makeAnchorName {
my( $this, $anchorName, $compatibilityMode ) = @_;
if( !$compatibilityMode &&
$anchorName =~ /^$TWiki::regex{anchorRegex}$/ ) {
# accept, already valid -- just remove leading #
return substr($anchorName, 1);
}
# strip out potential links so they don't get rendered.
# remove double bracket link
$anchorName =~ s/\[(?:\[.*?\])?\[(.*?)\]\s*\]/$1/g;
# add an _ before bare WikiWords
$anchorName =~ s/($TWiki::regex{wikiWordRegex})/_$1/go;
if( $compatibilityMode ) {
# remove leading/trailing underscores first, allowing them to be
# reintroduced
$anchorName =~ s/^[\s#_]*//;
$anchorName =~ s/[\s_]*$//;
}
$anchorName =~ s/<\/?[a-zA-Z][^>]*>//gi; # remove HTML tags
$anchorName =~ s/?[a-zA-Z0-9]+;//g; # remove HTML entities
$anchorName =~ s/&//g; # remove &
# filter TOC excludes if not at beginning
$anchorName =~ s/^(.+?)\s*$TWiki::regex{headerPatternNoTOC}.*/$1/o;
# filter '!!', '%NOTOC%'
$anchorName =~ s/$TWiki::regex{headerPatternNoTOC}//o;
# For most common alphabetic-only character encodings (i.e. iso-8859-*),
# remove non-alpha characters
if( !defined($TWiki::cfg{Site}{CharSet}) ||
$TWiki::cfg{Site}{CharSet} =~ /^iso-?8859-?/i ) {
$anchorName =~ s/[^$TWiki::regex{mixedAlphaNum}]+/_/g;
}
elsif ( $TWiki::cfg{Site}{CharSet} =~ /^utf.*8$|euc/i ) {
$anchorName =~ s/[\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/_/g;
}
$anchorName =~ s/__+/_/g; # remove excessive '_' chars
if ( !$compatibilityMode ) {
$anchorName =~ s/^[\s#_]+//; # no leading space nor '#', '_'
}
$anchorName =~ s/^(.{32})(.*)$/$1/; # limit to 32 chars
if ( defined($TWiki::cfg{Site}{CharSet}) &&
$TWiki::cfg{Site}{CharSet} =~ /^utf.*8/i
) {
$anchorName = chompUtf8Fragment($anchorName);
}
if ( !$compatibilityMode ) {
$anchorName =~ s/[\s_]+$//; # no trailing space, nor '_'
}
# No need to encode 8-bit characters in anchor due to UTF-8 URL support
return $anchorName;
}
# dispose of the set of known unique anchornames in order to inhibit the
# 'relabeling' of anchor names if the same topic is processed more than once,
# cf. explanation in TWiki::handleCommonTags()
sub _eraseAnchorNameMemory {
%anchornames = ();
}
=pod
---++ ObjectMethod makeUniqueAnchorName($web, $topic, $anchorName, $compatibility) -> $anchorName
* =$anchorName= - the unprocessed anchor name
* =$compatibilityMode= - SMELL: compatibility with *what*?? Who knows. :-(
Build a valid HTML anchor name (unique w.r.t. the list stored in %anchornames)
=cut
sub makeUniqueAnchorName {
my( $this, $web, $topic, $text, $compatibilityMode ) = @_;
$web = '' if (!defined($web));
$topic = '' if (!defined($topic));
my $anchorName = $this->makeAnchorName( $text, $compatibilityMode );
# ensure that the generated anchor name is unique
my $cnt = 1;
my $prefix = $web . '.' . $topic . '#';
my $suffix = '';
while (exists $anchornames{$prefix . $anchorName . $suffix}) {
# $anchorName.$suffix must _always_ be 'compatible', or things would get complicated
$suffix = '_AN' . $cnt++;
# limit resulting name to 32 chars
$anchorName = substr($anchorName, 0, 32 - length($suffix));
# a UTF-8 character sequence might be truncated in the middle
if ( defined($TWiki::cfg{Site}{CharSet}) &&
$TWiki::cfg{Site}{CharSet} =~ /^utf.*8/i
) {
$anchorName = chompUtf8Fragment($anchorName);
}
# this is only needed because '__' would not be 'compatible'
$anchorName =~ s/_+$//g;
}
$anchorName .= $suffix;
$anchornames{$prefix . $anchorName} = 1;
return $anchorName;
}
# Returns =title='...'= tooltip info in case LINKTOOLTIPINFO perferences variable is set.
# Warning: Slower performance if enabled.
sub _linkToolTipInfo {
my( $this, $theWeb, $theTopic ) = @_;
unless( defined( $this->{LINKTOOLTIPINFO} )) {
$this->{LINKTOOLTIPINFO} =
$this->{session}->{prefs}->getPreferencesValue('LINKTOOLTIPINFO') || '';
$this->{LINKTOOLTIPINFO} = '$username - $date - r$rev: $summary'
if( 'on' eq lc($this->{LINKTOOLTIPINFO}) );
}
return '' unless( $this->{LINKTOOLTIPINFO} );
return '' if( $this->{LINKTOOLTIPINFO} =~ /^off$/i );
return '' unless( $this->{session}->inContext( 'view' ));
# FIXME: This is slow, it can be improved by caching topic rev info and summary
my $store = $this->{session}->{store};
my $users = $this->{session}->{users};
# SMELL: we ought not to have to fake this. Topic object model, please!!
require TWiki::Meta;
my $meta = new TWiki::Meta( $this->{session}, $theWeb, $theTopic );
my( $date, $user, $rev ) = $meta->getRevisionInfo();
my $text = $this->{LINKTOOLTIPINFO};
$text =~ s/\$web/$theWeb/g;
$text =~ s/\$topic/$theTopic/g;
$text =~ s/\$rev/1.$rev/g;
$text =~ s/\$date/TWiki::Time::formatTime( $date )/ge;
$text =~ s/\$username/$users->getLoginName($user)||'unknown'/ge; # 'jsmith'
$text =~ s/\$wikiname/$users->getWikiName($user)||'UnknownUser'/ge; # 'JohnSmith'
$text =~ s/\$wikiusername/$users->webDotWikiName($user)||$TWiki::cfg{UsersWebName}.'UnknownUser'/ge;
# 'Main.JohnSmith'
if( $text =~ /\$summary/ ) {
my $summary = $store->readTopicRaw( undef, $theWeb, $theTopic, undef );
$summary = $this->makeTopicSummary( $summary, $theTopic, $theWeb );
$summary =~ s/[\"\']//g; # remove quotes (not allowed in title attribute)
$text =~ s/\$summary/$summary/g;
}
return $text;
}
# Returns topic meta object, either from cache or from file
sub _getTopicMeta {
my( $this, $theWeb, $theTopic, $theRev ) = @_;
$theRev ||= '';
my $meta = $this->{metaCache}{"$theWeb.$theTopic;$theRev"};
return $meta if ( $meta );
try {
my $store = $this->{session}->{store};
my $dummyText;
( $meta, $dummyText ) = $store->readTopic(
$this->{session}->{user}, $theWeb, $theTopic, $theRev );
$this->{metaCache}{"$theWeb.$theTopic;$theRev"} = $meta;
} catch TWiki::AccessControlException with {
# Ignore access exceptions; just don't read the data.
my $e = shift;
$this->{session}->writeWarning( "Attempt to read meta data failed: ".$e->stringify() );
};
return $meta;
}
# Returns topic title if force is on or if SHOWTOPICTITLELINK
# preferences setting is set.
# Warning: Slower performance if SHOWTOPICTITLELINK enabled.
sub _getTopicTitle {
my( $this, $web, $topic, $link, $force ) = @_;
unless( defined( $this->{SHOWTOPICTITLELINK} )) {
$this->{SHOWTOPICTITLELINK} =
TWiki::isTrue( $this->{session}->{prefs}->getPreferencesValue('SHOWTOPICTITLELINK') );
}
return $link unless( $this->{SHOWTOPICTITLELINK} || $force );
return $link unless( $this->{session}->inContext( 'view' ));
my $meta = $this->_getTopicMeta( $web, $topic );
return $meta->topicTitle() if( $meta );
return $link;
}
=pod
---++ ObjectMethod internalLink ( $theWeb, $theTopic, $theLinkText, $theAnchor, $doLink, $doKeepWeb, $hasExplicitLinkLabel, $theParams ) -> $html
Generate a link.
Note: Topic names may be spaced out. Spaced out names are converted to WikWords,
for example, "spaced topic name" points to "SpacedTopicName".
* =$theWeb= - the web containing the topic
* =$theTopic= - the topic to be link
* =$theLinkText= - text to use for the link
* =$theAnchor= - the link anchor, if any
* =$doLinkToMissingPages= - boolean: false means suppress link for non-existing pages
* =$doKeepWeb= - boolean: true to keep web prefix (for non existing Web.TOPIC)
* =$hasExplicitLinkLabel= - boolean: true in case of [[TopicName][explicit link label]]
* =$theParams= - the URL parameters specified by ?name1=value1;name2=valu2;... excluding the leading ?.
This is added as per Item7505. This parameter's natural position is before
=$theAnchor=. But to minimize code changes, it's introduced as the laster
one
Called by _handleWikiWord and _handleSquareBracketedLink and by Func::internalLink
Calls _renderWikiWord, which in turn will use Plurals.pm to match fold plurals to equivalency with their singular form
SMELL: why is this available to Func?
=cut
sub internalLink {
my( $this, $theWeb, $theTopic, $theLinkText, $theAnchor, $doLinkToMissingPages, $doKeepWeb, $hasExplicitLinkLabel, $theParams ) = @_;
# SMELL - shouldn't it be callable by TWiki::Func as well?
#PN: Webname/Subweb/ -> Webname/Subweb
$theWeb =~ s/\/\Z//o;
if($theLinkText eq $theWeb) {
$theLinkText =~ s/\//\./go;
}
#WebHome links to tother webs render as the WebName
if (($theLinkText eq $TWiki::cfg{HomeTopicName}) &&
($theWeb ne $this->{session}->{webName})) {
$theLinkText = $theWeb;
}
# Get rid of leading/trailing spaces in topic name
$theTopic =~ s/^\s*//o;
$theTopic =~ s/\s*$//o;
# Allow spacing out, etc.
# Plugin authors use $hasExplicitLinkLabel to determine if the link label
# should be rendered differently even if the topic author has used a
# specific link label.
$theLinkText = $this->{session}->{plugins}->dispatch(
'renderWikiWordHandler', $theLinkText, $hasExplicitLinkLabel,
$theWeb, $theTopic ) || $theLinkText;
# Turn spaced-out names into WikiWords - upper case first letter of
# whole link, and first of each word. TODO: Try to turn this off,
# avoiding spaces being stripped elsewhere
$theTopic =~ s/^(.)/\U$1/;
$theTopic =~ s/\s([$TWiki::regex{mixedAlphaNum}])/\U$1/go;
# Add before WikiWord inside link text to prevent double links
$theLinkText =~ s/(?<=[\s\(])([$TWiki::regex{upperAlpha}])/$1/go;
return _renderWikiWord($this, $theWeb, $theTopic, $theLinkText, $theAnchor, $doLinkToMissingPages, $doKeepWeb, $theParams);
}
# TODO: this should be overridable by plugins.
sub _renderWikiWord {
my ($this, $theWeb, $theTopic, $theLinkText, $theAnchor, $doLinkToMissingPages, $doKeepWeb, $theParams) = @_;
my $store = $this->{session}->{store};
my $topicExists = $store->topicExists( $theWeb, $theTopic );
if ( !$topicExists && $store->webExists( $theWeb.'/'.$theTopic ) ) {
$theWeb .= '/'.$theTopic;
$theTopic = $TWiki::cfg{HomeTopicName};
$topicExists = $store->topicExists( $theWeb, $theTopic );
}
my $singular = '';
unless( $topicExists ) {
# topic not found - try to singularise
require TWiki::Plurals;
$singular = TWiki::Plurals::singularForm($theWeb, $theTopic);
if( $singular ) {
$topicExists = $store->topicExists( $theWeb, $singular );
$theTopic = $singular if( $topicExists );
}
}
if( $topicExists ) {
return _renderExistingWikiWord( $this, $theWeb, $theTopic, $theLinkText, $theAnchor, $theParams );
}
if( $doLinkToMissingPages ) {
# CDot: disabled until SuggestSingularNotPlural is resolved
# if ($singular && $singular ne $theTopic) {
# #unshift( @topics, $singular);
# }
return _renderNonExistingWikiWord($this, $theWeb, $theTopic, $theLinkText );
}
if( $doKeepWeb ) {
return $theWeb.'.'.$theLinkText;
}
return $theLinkText;
}
sub _renderExistingWikiWord {
my ($this, $web, $topic, $text, $anchor, $params) = @_;
my $currentWebHome = '';
$currentWebHome = 'twikiCurrentWebHomeLink ' if (($web eq $this->{session}->{webName}) &&
($topic eq $TWiki::cfg{HomeTopicName} ));
my $currentTopic = '';
$currentTopic = 'twikiCurrentTopicLink ' if (($web eq $this->{session}->{webName}) &&
($topic eq $this->{session}->{topicName}));
my @attrs;
my $href = $this->{session}->getScriptUrl( 0, 'view', $web, $topic );
if ( $params ) {
$href .= '?' . $params;
}
if( $anchor ) {
$anchor = $this->makeAnchorName( $anchor );
push( @attrs, class => $currentTopic.$currentWebHome.'twikiAnchorLink', href => $href.'#'.$anchor );
} else {
push( @attrs, class => $currentTopic.$currentWebHome.'twikiLink', href => $href );
}
my $tooltip = _linkToolTipInfo( $this, $web, $topic );
push( @attrs, title => $tooltip ) if( $tooltip );
my $link = CGI::a( { @attrs }, $text );
# When we pass the tooltip text to CGI::a it may contain
# s, and CGI::a will convert the < to <. This is a
# basic problem with .
$link =~ s/<nop>//g;
return $link;
}
sub _renderNonExistingWikiWord {
my ($this, $web, $topic, $text) = @_;
my $ans = $this->_newLinkFormat;
$ans =~ s/\$web/$web/g;
$ans =~ s/\$topic/$topic/g;
$ans =~ s/\$text/$text/g;
$ans =~ s/\$summary/$this->TML2PlainText( $text, $web, $topic, 'entityencode')/ge;
$ans = $this->{session}->handleCommonTags( $ans, $this->{session}{webName}, $this->{session}{topicName} );
return $ans;
}
# _handleWikiWord is called by the TWiki Render routine when it sees a
# wiki word that needs linking.
# Handle the various link constructions. e.g.:
# WikiWord
# Web.WikiWord
# Web.WikiWord#anchor
#
# This routine adds missing parameters before passing off to internallink
sub _handleWikiWord {
my ( $this, $theWeb, $web, $topic, $anchor ) = @_;
my $linkIfAbsent = 1;
my $keepWeb = 0;
my $text;
$web = $theWeb unless (defined($web));
if( defined( $anchor )) {
ASSERT(($anchor =~ m/\#.*/)) if DEBUG; # must include a hash.
} else {
$anchor = '' ;
}
if ( defined( $anchor ) ) {
# 'Web.TopicName#anchor' or 'Web.ABBREV#anchor' link
$text = $topic.$anchor;
} else {
$anchor = '';
# 'Web.TopicName' or 'Web.ABBREV' link:
if ( $topic eq $TWiki::cfg{HomeTopicName} &&
$web ne $this->{session}->{webName} ) {
$text = $web;
} else {
$text = $topic;
}
}
# =$doKeepWeb= boolean: true to keep web prefix (for non existing Web.TOPIC)
# (Necessary to leave "web part" of ABR.ABR.ABR intact if topic not found)
$keepWeb = ( $topic =~ /^$TWiki::regex{abbrevRegex}$/o && $web ne $this->{session}->{webName} );
# false means suppress link for non-existing pages
$linkIfAbsent = ( $topic !~ /^$TWiki::regex{abbrevRegex}$/o );
# SMELL - it seems $linkIfAbsent, $keepWeb are always inverses of each
# other
# TODO: check the spec of doKeepWeb vs $doLinkToMissingPages
return $this->internalLink( $web, $topic, $text, $anchor, $linkIfAbsent, $keepWeb, undef );
}
sub _getNameFromLink {
my $link = shift;
my $name = $link;
$name =~ s/$TWiki::regex{anchorRegex}$//;
$name =~ s/\?.*$//;
$name =~ s:/+$::;
my @path = split(m:/+:, $name);
$name = $path[-1];
$name = $link if ( $name eq '' ); #the last resort
return $name;
}
# Handle SquareBracketed links mentioned on page $theWeb.$theTopic
# format: [[$link]]
# format: [[$link][$text]]
sub _handleSquareBracketedLink {
my( $this, $web, $topic, $link, $text ) = @_;
# Strip leading/trailing spaces
$link =~ s/^\s+//;
$link =~ s/\s+$//;
my $hasExplicitLinkLabel = $text ? 1 : undef;
# Explicit external [[$link][$text]]-style can be handled directly
if( $link =~ m!^($TWiki::regex{linkProtocolPattern}\:|/)! ) {
if (defined $text) {
# [[][]] style - protect text:
# for [[...][$name]]
$text =~ s/\$name\b/_getNameFromLink($link)/ge;
# Prevent automatic WikiWord or CAPWORD linking in explicit links
$text =~ s/(?<=[\s\(])($TWiki::regex{wikiWordRegex}|[$TWiki::regex{upperAlpha}])/$1/go;
}
else {
# [[]] style - take care for legacy:
# Prepare special case of '[[URL#anchor display text]]' link
if ( $link =~ /^(\S+)\s+(.*)$/ ) {
# '[[URL#anchor display text]]' link:
$link = $1;
$text = $2;
$text =~ s/(?<=[\s\(])($TWiki::regex{wikiWordRegex}|[$TWiki::regex{upperAlpha}])/$1/go;
}
}
return _externalLink( $this, $link, $text );
}
# Extract '#anchor'
my $origLink = $link;
my $anchor = '';
if( $link =~ s/($TWiki::regex{anchorRegex}$)// ) {
$anchor = $1;
}
my $forceTopicTitle = 0;
if( $link =~ s/^\+// ) {
# [[+WikiWord]] link
$forceTopicTitle = 1;
}
# Item7505: Extract '?parameter'
my $params;
if ( $link =~ s/\?(.*)$// ) {
$params = $1;
}
$link = $this->buildWikiWord( $link );
$topic = $link if( $link );
# Topic defaults to the current topic
($web, $topic) = $this->{session}->normalizeWebTopicName( $web, $topic );
if( $text ) {
$text =~ s/\$name\b/$topic/g;
$text =~ s/\$topictitle/$this->_getTopicTitle( $web, $topic, $origLink, 1 )/geo;
} else {
$text = $this->_getTopicTitle( $web, $topic, $origLink, $forceTopicTitle );
}
return $this->internalLink( $web, $topic, $text, $anchor, 1, undef, $hasExplicitLinkLabel, $params );
}
# Converts arbitrary text to a WikiWord
sub buildWikiWord {
my( $this, $link ) = @_;
# filter out &any; entities (legacy)
$link =~ s/\&[a-z]+\;//gi;
# filter out { entities (legacy)
$link =~ s/\&\#[0-9]+\;//g;
# Filter junk
$link =~ s/$TWiki::cfg{NameFilter}+/ /g;
# Capitalise first word
$link =~ s/^(.)/\U$1/;
# Collapse spaces and capitalise following letter
$link =~ s/\s([$TWiki::regex{mixedAlphaNum}])/\U$1/go;
# Get rid of remaining spaces, i.e. spaces in front of -'s and ('s
$link =~ s/\s//go;
return $link;
}
sub _readTopicTable {
my( $this, $webTopic, $defaultWeb ) = @_;
if( $webTopic =~ /%/ ) {
$webTopic = TWiki::Func::expandCommonVariables( $webTopic, $defaultWeb );
}
my( $web, $topic ) = TWiki::Func::normalizeWebTopicName( $defaultWeb, $webTopic );
my $wikiName = TWiki::Func::getWikiName();
my( $meta, $text ) = TWiki::Func::readTopic( $web, $topic );
my @result;
if( TWiki::Func::checkAccessPermission( 'VIEW', $wikiName, $text, $topic, $web, $meta ) ) {
if( defined $text && $text ne '' ) {
while( $text =~ /^\s*\|(.*?)\|(?:.*\|)?\s*$/mg ) {
( my $name = $1 ) =~ s/^\s+|\s+$//g;
next if $name =~ /^\*.*\*$/; # exclude "| *Text* |"
push @result, $name;
}
} else {
TWiki::Func::writeWarning( "$webTopic cannot be read" );
}
} else {
TWiki::Func::writeWarning( "$webTopic is not accessible for user $wikiName" );
}
return @result;
}
sub _isInternalLinkDomain {
my( $this, $url ) = @_;
# TWiki internal link if URL starts with TWiki domain or "/"
my $urlHostRegex = quotemeta( $this->{session}{urlHost} );
return 1 if $url =~ /^($urlHostRegex|\/)/;
# Item7191: Check $TWiki::cfg{Links}{InternalLinkDomains}
if( $url =~ m!^$TWiki::regex{linkProtocolPattern}://([^/]+)! ) {
my $domain = $2; # $1 is the protocol part
$domain =~ s/:.*//; # Remove any port number
unless( defined $this->{_internalDomainRegex} ) {
my @targets = (
{ defaultWeb => $TWiki::cfg{SystemWebName},
value => $TWiki::cfg{Links}{InternalLinkDomains} },
{ defaultWeb => $this->{session}{SESSION_TAGS}{BASEWEB},
value => $this->{session}{prefs}->getPreferencesValue( 'INTERNALLINKDOMAINS' ) },
);
my @patterns;
for my $target ( @targets ) {
next if !$target->{value};
my $defaultWeb = $target->{defaultWeb};
for my $entry ( split( /\s*,\s*/, $target->{value} ) ) {
$entry =~ s/^\s+|\s+$//g;
next if $entry eq '';
my @entries;
if( $entry =~ /^topic:(.*)/ ) {
@entries = $this->_readTopicTable( $1, $defaultWeb );
} else {
@entries = ( $entry );
}
# Case 1: $domain is '.': intranet "one-word" domain
# Case 2: $domain starts with '.': subdomains under $domain
# Case 3: otherwise: $domain (no subdomains)
push @patterns, map {
my $domain = $_;
$domain eq '.' ? '^[^\.]+$' :
( $domain =~ /^\./ ? '' : '^' ).quotemeta( $domain ).'$'
} @entries;
}
}
if( @patterns ) {
$this->{_internalDomainRegex} = '('.join( '|', @patterns ).')';
} else {
$this->{_internalDomainRegex} = '';
}
}
if( $this->{_internalDomainRegex} ) {
return 1 if $domain =~ /$this->{_internalDomainRegex}/;
}
}
return 0;
}
# Handle an external link typed directly into text. If it's an image
# (as indicated by the file type), and no text is specified, then use
# an img tag, otherwise generate a link.
sub _externalLink {
my( $this, $url, $text ) = @_;
if( $url =~ /^[^?]*\.(gif|jpg|jpeg|png)$/i && !$text) {
my $filename = $url;
$filename =~ s@.*/([^/]*)@$1@go;
return CGI::img( { src => $url, alt => $filename } );
}
my $prefs = $this->{session}{prefs};
my $externalLinksInNewWindow = $TWiki::cfg{Links}{ExternalLinksInNewWindow};
if( defined( my $prefValue = $prefs->getPreferencesValue( 'EXTERNALLINKSINNEWWINDOW' ) ) ) {
$externalLinksInNewWindow = TWiki::isTrue( $prefValue ) if $prefValue ne '';
}
my $externalLinksIcon = $TWiki::cfg{Links}{ExternalLinksIcon};
if( defined( my $prefValue = $prefs->getPreferencesValue( 'EXTERNALLINKSICON' ) ) ) {
$externalLinksIcon = TWiki::isTrue( $prefValue ) if $prefValue ne '';
}
my $opt = '';
my $icn = '';
if( $url =~ /^mailto:/i ) {
my $twiki = $this->{session};
if( $TWiki::cfg{AntiSpam}{EmailGuestPadding} && !$twiki->inContext( 'authenticated' ) ) {
$url =~ s/(\@[\w\_\-\+]+)(\.)/$1$TWiki::cfg{AntiSpam}{EmailGuestPadding}$2/;
if ($text) {
$text =~ s/(\@[\w\_\-\+]+)(\.)/$1$TWiki::cfg{AntiSpam}{EmailGuestPadding}$2/;
}
} elsif( $TWiki::cfg{AntiSpam}{EmailPadding} ) {
$url =~ s/(\@[\w\_\-\+]+)(\.)/$1$TWiki::cfg{AntiSpam}{EmailPadding}$2/;
if ($text) {
$text =~ s/(\@[\w\_\-\+]+)(\.)/$1$TWiki::cfg{AntiSpam}{EmailPadding}$2/;
}
}
if( $TWiki::cfg{AntiSpam}{HideUserDetails} ) {
# Much harder obfuscation scheme. For link text we only encode '@'
# See also Item2928 and Item3430 before touching this
$url =~ s/(\W)/''.ord($1).';'/ge;
if ($text) {
$text =~ s/\@/''.ord('@').';'/ge;
}
}
} elsif( $this->_isInternalLinkDomain( $url ) ) {
# TWiki internal link if URL is under an internal domain
$opt = ' target="_top"';
} else {
# External link
if( $externalLinksInNewWindow ) {
$opt = ' target="_blank"';
} else {
$opt = ' target="_top"';
}
if( $externalLinksIcon ) {
unless( $this->{externalIcon} ) {
$this->{externalIcon} = CGI::img( { src =>
"$TWiki::cfg{PubUrlPath}/$TWiki::cfg{SystemWebName}"
. "/TWikiDocGraphics/external-link.gif",
alt => '', width => 13, height => 12, border => 0 } );
}
$icn = $this->{externalIcon};
}
}
$text ||= $url;
$url =~ s/ /%20/g; #Item5787: if a url has spaces, escape them so the url has less chance of being broken by later parsing.
# SMELL: Can't use CGI::a here, because it encodes ampersands in
# the link, and those have already been encoded once in the
# rendering loop (they are identified as "stand-alone"). One
# encoding works; two is too many. None would be better for everyone!
return "$text$icn ";
}
# Generate a "mailTo" link
sub _mailLink {
my( $this, $text ) = @_;
my $url = $text;
$url = 'mailto:'.$url unless $url =~ /^mailto:/i;
return _externalLink( $this, $url, $text );
}
# Generate a "@twitter" link
sub _twitterLink {
my( $this, $text ) = @_;
my $url = $TWiki::cfg{Links}{TwitterUrlPattern};
return( '@' . $text ) unless( $url );
$url =~ s/\%ID\%/$text/go;
return _externalLink( $this, $url, '@' . $text );
}
=pod
---++ ObjectMethod renderFORM ( %params, $topic, $web ) -> $tml
Returns TML of a TWiki Form, based on %FORM{}% variable.
=cut
sub renderFORM {
my ( $this, $params, $topic, $web ) = @_;
my @fieldList = split( /, */, $params->{formfields} || 'all' );
my $vWeb = $params->{web} || $web;
my $vTopic = $params->{topic}|| $params->{_DEFAULT} || $topic;
my $default = $params->{default} || '';
my $rev = $params->{rev};
if( $rev ) {
$rev = $this->{session}{store}->cleanUpRevID($rev);
}
my $format = $params->{format} || '| $title: | $value |';
my $newLine = $params->{newline};
if( defined $newLine ) {
$newLine =~ s/\$br\b/ /go;
$newLine =~ s/\$n\b/\n/go;
} elsif( $format =~ /^ *\|/ ) {
# TML table, so use br tag for newlines by default
$newLine = ' ';
} else {
$newLine = "\n";
}
my $separator = $params->{separator} || "\n";
my $header = $params->{header} || '1';
$header = '| *[[$formweb.$formtopic][$formtopic]]* ||' if( $header =~ /^(on|1)$/ );
my $showHidden = $params->{showhidden};
my $encodeType = $params->{encode} || '';
my $session = $this->{session};
( $vWeb, $vTopic ) = $session->normalizeWebTopicName( $web, $vTopic );
my $meta = $this->_getTopicMeta( $vWeb, $vTopic, $rev );
return $default unless( $meta && $meta->get( 'FORM' ) ); # no form on topic
my $formWeb = $vWeb;
my $formTopic = $meta->get( 'FORM' )->{name};
( $formWeb, $formTopic ) = $session->normalizeWebTopicName( $formWeb, $formTopic );
my $formDef = $this->{formDefCache}{"$formWeb.$formTopic"};
if( !$formDef && $session->{store}->topicExists( $formWeb, $formTopic ) ) {
require TWiki::Form;
$formDef = new TWiki::Form( $session, $formWeb, $formTopic );
$this->{formDefCache}{"$formWeb.$formTopic"} = $formDef;
}
my @lines = ();
unless( $header eq 'none' ) {
$header =~ s/\$formweb/$formWeb/go;
$header =~ s/\$formtopic/$formTopic/go;
@lines = ( $header );
}
my @fields = $meta->find( 'FIELD' );
my %seen;
my $attrs = { showhidden => 1, encode => $encodeType, newline => $newLine };
if( $formDef ) {
# get all attributes from form definition, except for value
@fieldList =
map {
if( /^all$/ ) {
map { $_->{name} } @{$formDef->{fields}}
} else {
$_
}
} @fieldList;
# fieldList may contain duplicates that need to be filtered out
foreach my $name ( @fieldList ) {
my $field = $formDef->getField( $name );
if( $field && !$seen{$field} && ( $field->{attributes} !~ /H/ || $showHidden ) ) {
$seen{$field} = 1;
my $title = $field->{title} || $name;
my $attributes = $field->{attributes} || '';
my $text = $format;
my @a = map { $_->{value} } grep { $_->{name} eq $name } @fields;
my $value = '';
$value = $a[0] if( scalar @a );
my $length = length $value;
$text =~ s/\$length/$length/go;
$text =~ s/\$size/$field->{size}/go;
$text =~ s/\$formweb/$formWeb/go;
$text =~ s/\$formtopic/$formTopic/go;
if( $text =~ s/(\$value)\(\s*([^\)]*)\)/$1/go ) {
$attrs->{break} = $2;
} else {
undef $attrs->{break};
}
$text = $field->renderForDisplay( $text, $value, $attrs );
push( @lines, $text );
}
}
} else {
# topic has a form but form definition is unavailable.
# do the best to render form without form definition topic.
@fieldList =
map {
if( /^all$/ ) {
map { $_->{name} } @fields
} else {
$_
}
} @fieldList;
# fieldList may contain duplicates that need to be filtered out
foreach my $name ( @fieldList ) {
my $field = $meta->get( 'FIELD', $name );
if( $field && !$seen{$field} && ( $field->{attributes} !~ /H/ || $showHidden ) ) {
$seen{$field} = 1;
my $title = $field->{title} || $name;
my $attributes = $field->{attributes} || '';
my $text = $format;
if( $text =~ s/(\$value)\(\s*([^\)]*)\)/$1/go ) {
$attrs->{break} = $2;
} else {
undef $attrs->{break};
}
my $value = TWiki::Render::protectFormFieldValue( $field->{value}, $attrs );
my $length = length $value;
$text =~ s/\$length/$length/go;
$text =~ s/\$size/1/go; # fake it
$text =~ s/\$attributes/$attributes/go;
$text =~ s/\$type/unknown/go;
$text =~ s/\$formweb/$formWeb/go;
$text =~ s/\$formtopic/$formTopic/go;
$text =~ s/\$tooltip//go;
$text =~ s/\$title/$title/go;
$text =~ s/\$name/$name/go;
$text =~ s/\$value/$value/go;
push( @lines, $text );
}
}
}
return $default unless( scalar keys %seen ); # no wanted fields found
$separator =~ s/\$br\b/ /go;
$separator =~ s/\$n\b/\n/go;
return join( $separator, @lines );
}
=pod
---++ ObjectMethod renderFORMFIELD ( %params, $topic, $web ) -> $html
Returns the fully rendered expansion of a %FORMFIELD{}% tag.
=cut
sub renderFORMFIELD {
my ( $this, $params, $topic, $web ) = @_;
my $formField = $params->{_DEFAULT};
my $formTopic = $params->{topic};
my $altText = $params->{alttext};
my $default = $params->{default};
my $rev = $params->{rev};
if ( $rev ) {
$rev = $this->{session}{store}->cleanUpRevID($rev);
}
my $format = $params->{format} || '$value';
my $encode = $params->{encode} || '';
my $newLine = $params->{newline};
my $formWeb;
if ( $formTopic ) {
if ($topic =~ /^([^.]+)\.([^.]+)/o) {
( $formWeb, $topic ) = ( $1, $2 );
} else {
# SMELL: Undocumented feature, 'web' parameter
$formWeb = $params->{web};
}
$formWeb = $web unless $formWeb;
} else {
$formWeb = $web;
$formTopic = $topic;
}
my $meta = $this->_getTopicMeta( $formWeb, $formTopic, $rev );
my $text = '';
my $found = 0;
my $title = '';
if ( $meta ) {
my @fields = $meta->find( 'FIELD' );
foreach my $field ( @fields ) {
my $name = $field->{name};
$title = $field->{title} || $name;
if( $title eq $formField || $name eq $formField ) {
$found = 1;
my $value = $field->{value};
my $length = length $value;
if( $length || $format ne '$value' ) {
$text = $format;
$text =~ s/\$length/$length/go;
$text =~ s/\$attributes/$field->{attributes}/go;
$text =~ s/\$formtopic/$meta->get( 'FORM' )->{name}/geo;
$text =~ s/\$title/$title/go; # Item6267: Keep $title in value
$text =~ s/\$name/$name/go;
$text =~ s/\$value\(\s*([^\)]*)\s*\)/breakName( $value, $1 )/ges;
$text =~ s/\$value/$value/go;
} elsif ( defined $default ) {
$text = $default;
}
last; #one hit suffices
}
}
}
unless ( $found ) {
$text = $altText || '';
}
if( defined $newLine ) {
$newLine =~ s/\$br\b/\0-br-\0/go;
$newLine =~ s/\$n\b/\0-n-\0/go;
$text =~ s/\r?\n/$newLine/go;
}
if( $encode ) {
$text = $this->{session}->ENCODE( { _DEFAULT => $text, type => $encode } );
}
if( defined $newLine ) {
$text =~ s/\0-br-\0/ /go;
$text =~ s/\0-n-\0/\n/go;
}
return $text;
}
=pod
---++ ObjectMethod renderEDITFORM ( %params, $topic, $web ) -> $tml
Returns TML of a TWiki Form, based on %EDITFORM{}% variable.
=cut
sub renderEDITFORM {
my ( $this, $params, $topic, $web ) = @_;
my $vWeb = $params->{web} || $web;
my $vTopic = $params->{topic}|| $params->{_DEFAULT} || $topic;
my $formTemplate = $params->{formtemplate} || '';
my @elementList = split( /, */, $params->{elements}
|| 'formstart, header, formfields, submit, hiddenfields, formend' );
my $header = $params->{header} || '1';
$header = '| *[[$formweb.$formtopic][$formtopic]]* ||' if( $header =~ /^(on|1)$/ );
my @fieldList = split( /, */, $params->{formfields} || 'all' );
my $format = $params->{format} || '| $title: $extra | $inputfield |';
my @hiddenList = split( /, */, $params->{hiddenfields} || '' );
my $submit = $params->{submit} || '| | $submit |';
my $onsubmit = $params->{onsubmit} || '';
my $action = $params->{action} || 'save';
my $method = $params->{method} || 'get';
$method = 'post' if( $action eq 'save' );
my $separator = $params->{separator} || "\n";
my $default = $params->{default} || '';
my $session = $this->{session};
# get topic meta data
( $vWeb, $vTopic ) = $session->normalizeWebTopicName( $web, $vTopic );
my $meta = $this->_getTopicMeta( $vWeb, $vTopic );
my $metaFormName = $meta && $meta->get( 'FORM' ) && $meta->get( 'FORM' )->{name};
# get form definition
my $formWeb = $web;
my $formTopic = $formTemplate || $metaFormName;
return $default unless( $formTopic ); # no form on topic and no form template
( $formWeb, $formTopic ) = $session->normalizeWebTopicName( $formWeb, $formTopic );
my $formDef = $this->{formDefCache}{"$formWeb.$formTopic"};
if( !$formDef && $session->{store}->topicExists( $formWeb, $formTopic ) ) {
require TWiki::Form;
$formDef = new TWiki::Form( $session, $formWeb, $formTopic );
$this->{formDefCache}{"$formWeb.$formTopic"} = $formDef;
}
return $default unless( $formDef ); # no form definition found
# loop through element list to compose the form
my @lines = ();
foreach my $element ( @elementList ) {
if( $element eq 'formstart' ) {
if( $action =~ /^[a-z_-]+$/i ) {
$action = $session->getScriptUrl( 0, $action, $vWeb, $vTopic );
} # else use full action URL verbatim
my $html = "' );
}
}
$separator =~ s/\$br\b/ /go;
$separator =~ s/\$n\b/\n/go;
my $text = join( $separator, @lines );
return $text;
}
sub _submitField {
my( $label ) = @_;
$label ||= 'Save';
my $text = ' ';
$text =~ s/\$label/$label/g;
return $text;
}
=pod
---++ ObjectMethod renderEDITFORMFIELD ( %params, $topic, $web ) -> $html
Returns the fully rendered expansion of a %EDITFORMFIELD{}% tag.
=cut
sub renderEDITFORMFIELD {
my ( $this, $params, $topic, $web ) = @_;
my $formField = $params->{_DEFAULT};
my $formTopic = $params->{form};
my $format = $params->{format} || '$inputfield';
my $type = $params->{type} || '';
my $value = $params->{value} || '';
my $vTopic = $params->{topic} || '';
my $session = $this->{session};
my $formWeb;
unless( $formField ) {
return errorMsg( 'Required form field name is missing' );
}
if( $type =~ /^start$/i ) {
# special case: HTML start tag
my $action = $params->{action} || 'view';
my $method = 'get';
if( $params->{method} ) {
$method = $params->{method};
} elsif( $action =~ /^save$/i ) {
$method = 'post';
}
if( $action =~ /^[a-z_-]+$/i ) {
my $vWeb;
$vTopic ||= $topic;
( $vWeb, $vTopic ) = $session->normalizeWebTopicName( $web, $vTopic );
$action = $session->getScriptUrl( 0, $action, $vWeb, $vTopic );
} # else use full action URL verbatim
my $html = "';
return $html;
} elsif( $type =~ /^textarea$/ ) {
# special case, construct tags
my $html = "';
return $html;
} elsif( $type ) {
# special case, construct an tag
my $html = " 'twikiSubmit',
button => 'twikiButton',
text => 'twikiInputField twikiEditFormTextField',
checkbox => 'twikiCheckbox',
radio => 'twikiRadioButton',
};
my $needsClass = 1;
foreach my $key ( keys %$params ) {
next if( $key =~ /^(_DEFAULT|_RAW|name|type|text)$/i );
$needsClass = 0 if( $key =~ /^class$/i );
$html .= " $key=\"" . $params->{$key} . '"';
}
if( $needsClass && $classes->{$type} ) {
$html .= ' class="' . $classes->{$type} . '"';
}
$html .= ' />';
if( $type =~ /^(radio|checkbox)$/i ) {
if( $params->{text} ) {
$html .= ' ' . $params->{text};
} elsif( $params->{value} ) {
$html .= ' ' . $params->{value};
}
$html = " $html ";
}
return $html;
}
unless( $formTopic || $vTopic || $type ) {
return errorMsg( 'Required form, topic or type parameter is missing' );
}
if( $vTopic ) {
# get form name and value from specified topic
my $vWeb;
( $vWeb, $vTopic ) = $session->normalizeWebTopicName( $web, $vTopic );
my $meta = $this->_getTopicMeta( $vWeb, $vTopic );
if ( $meta ) {
if( $meta->get( 'FORM' ) ) {
$formTopic ||= ( $meta->get( 'FORM' )->{name} || '' );
} else {
return errorMsg( "Topic !$vWeb.$vTopic does not have a form" );
}
if( $value eq '' ) {
# get value from specified topic
my @fields = $meta->find( 'FIELD' );
foreach my $field ( @fields ) {
my $title = $field->{title} || '';
if( $title eq $formField || $field->{name} eq $formField ) {
$value = $field->{value};
last; #one hit suffices
}
}
}
}
}
( $formWeb, $formTopic ) = $session->normalizeWebTopicName( $web, $formTopic );
unless( $session->{store}->topicExists( $formWeb, $formTopic ) ) {
return errorMsg( "Topic !$formWeb.$formTopic does not exist" );
}
my $formDef = $this->{formDefCache}{"$formWeb.$formTopic"};
unless( $formDef ) {
require TWiki::Form;
$formDef = new TWiki::Form( $session, $formWeb, $formTopic );
$this->{formDefCache}{"$formWeb.$formTopic"} = $formDef;
}
unless( $formDef ) {
return errorMsg( "Topic $formWeb.$formTopic does not have a form definition" );
}
foreach my $fieldDef ( @{$formDef->{fields}} ) {
if( $fieldDef->{title} eq $formField || $fieldDef->{name} eq $formField ) {
if ( $TWiki::cfg{Site}{CharSet} =~ m/^utf-?8$/i ) {
require Encode;
$value = Encode::decode_utf8($value);
}
my ( $extra, $text ) = $fieldDef->renderForEdit( $web, $topic, $value );
if ( $TWiki::cfg{Site}{CharSet} =~ m/^utf-?8$/i ) {
$extra = Encode::encode_utf8($extra);
$text = Encode::encode_utf8($text);
}
# In value="", escape space, angle brackets, "%" and "|"
$text =~ s/(value=")([^"]*)/$1 . defuseTML($2)/geo;
if( $fieldDef->isMandatory() ) {
$extra .= CGI::span( { class => 'twikiAlert' }, ' *' );
}
my $html = $format;
$html =~ s/\$inputfield/$text/go;
$html =~ s/\$extra/$extra/go;
$html =~ s/\$name/$fieldDef->{name}/go;
$html =~ s/\$title/$fieldDef->{title}/go;
$html =~ s/\$size/$fieldDef->{size}/go;
$html =~ s/\$value/$fieldDef->{value}/go;
$html =~ s/\$tooltip/$fieldDef->{tooltip}/go;
$html =~ s/\$attributes/$fieldDef->{attributes}/go;
return $html;
}
}
return errorMsg( "Topic $formWeb.$formTopic does not have a form field named !"
. $formField );
}
sub defuseTML {
my( $text ) = @_;
$text =~ s/\s/ /g; # defuse TML by escaping space
$text =~ s/\%/%/g; # defuse %VARIABLES%
$text =~ s/\[/[/g; # defuse [[]] links
$text =~ s/\|/|/g; # defuse "|" to allow EDITFIELD in TWiki tables
return $text;
}
sub errorMsg {
my( $msg ) = @_;
return CGI::span( { class => 'twikiAlert' }, "EDITFORMFIELD Error: $msg" );
}
=pod
---++ ObjectMethod getRenderedVersion ( $text, $theWeb, $theTopic ) -> $html
The main rendering function.
=cut
sub getRenderedVersion {
my( $this, $text, $theWeb, $theTopic ) = @_;
return '' unless $text; # nothing to do
$theTopic ||= $this->{session}->{topicName};
$theWeb ||= $this->{session}->{webName};
my $session = $this->{session};
my $plugins = $session->{plugins};
my $prefs = $session->{prefs};
@{$this->{LIST}} = ();
%anchornames = ();
# Initial cleanup
$text =~ s/\r//g;
# whitespace before \n/s;
# Maps of placeholders to tag parameters and text
my $removed = {};
# verbatim before literal - see Item3431
$text = $this->takeOutBlocks( $text, 'verbatim', $removed );
$text = $this->takeOutBlocks( $text, 'literal', $removed );
$text = $this->_takeOutProtected( $text, qr/<\?([^?]*)\?>/s, 'comment', $removed );
$text = $this->_takeOutProtected( $text, qr/]*)>?/mi, 'comment', $removed );
$text = $this->_takeOutProtected( $text, qr//si, 'head', $removed );
$text = $this->_takeOutProtected( $text, qr/