source: trunk/core/lib/Foswiki/Macros/INCLUDE.pm @ 14649

Revision 14649, 14.1 KB checked in by SvenDowideit, 13 months ago (diff)

Item11783: INCLUDE supports a list of topics to try to include

Line 
1# See bottom of file for license and copyright information
2package Foswiki;
3
4use strict;
5use warnings;
6
7# applyPatternToIncludedText( $text, $pattern ) -> $text
8# Apply a pattern on included text to extract a subset
9# Package-private; used by IncludeHandlers.
10sub applyPatternToIncludedText {
11    my ( $text, $pattern ) = @_;
12
13    $pattern = Foswiki::Sandbox::untaint( $pattern, \&validatePattern );
14
15    my $ok = 0;
16    eval {
17
18        # The eval acts as a try block in case there is anything evil in
19        # the pattern.
20
21        # The () ensures that $1 is defined if $pattern matches
22        # but does not capture anything
23        if ( $text =~ m/$pattern()/is ) {
24            $text = $1;
25        }
26        else {
27
28            # The pattern did not match, so return nothing
29            $text = '';
30        }
31        $ok = 1;
32    };
33    $text = '' unless $ok;
34
35    return $text;
36}
37
38# Replace web references in a topic. Called from forEachLine, applying to
39# each non-verbatim and non-literal line.
40sub _fixupIncludedTopic {
41    my ( $text, $options ) = @_;
42
43    my $fromWeb = $options->{web};
44
45    unless ( $options->{in_noautolink} ) {
46
47        # 'TopicName' to 'Web.TopicName'
48        $text =~
49s#(?:^|(?<=[\s(]))($Foswiki::regex{wikiWordRegex})(?=\s|\)|$)#$fromWeb.$1#go;
50    }
51
52    # Handle explicit [[]] everywhere
53    # '[[TopicName][...]]' to '[[Web.TopicName][...]]'
54    $text =~ s/\[\[([^]]+)\](?:\[([^]]+)\])?\]/
55      _fixIncludeLink( $fromWeb, $1, $2 )/geo;
56
57    return $text;
58}
59
60# Add a web reference to a [[...][...]] link in an included topic
61sub _fixIncludeLink {
62    my ( $web, $link, $label ) = @_;
63
64    # Detect absolute and relative URLs and web-qualified wikinames
65    if ( $link =~
66m#^($Foswiki::regex{webNameRegex}\.|$Foswiki::regex{defaultWebNameRegex}\.|$Foswiki::regex{linkProtocolPattern}:|/)#o
67      )
68    {
69        if ($label) {
70            return "[[$link][$label]]";
71        }
72        else {
73            return "[[$link]]";
74        }
75    }
76    elsif ( !$label ) {
77
78        # Must be wikiword or spaced-out wikiword (or illegal link :-/)
79        $label = $link;
80    }
81
82    # If link is only an anchor, leave it as is (Foswikitask:Item771)
83    return "[[$link][$label]]" if $link =~ /^#/;
84    return "[[$web.$link][$label]]";
85}
86
87# generate an include warning
88# SMELL: varying number of parameters idiotic to handle for customized $warn
89sub _includeWarning {
90    my $this    = shift;
91    my $warn    = shift;
92    my $message = shift;
93
94    if ( $warn eq 'on' ) {
95        return $this->inlineAlert( 'alerts', $message, @_ );
96    }
97    elsif ( isTrue($warn) ) {
98
99        # different inlineAlerts need different argument counts
100        my $argument = '';
101        if ( $message eq 'topic_not_found' ) {
102            my ( $web, $topic ) = @_;
103            $argument = "$web.$topic";
104        }
105        else {
106            $argument = shift;
107        }
108        $warn =~ s/\$topic/$argument/go if $argument;
109        return $warn;
110    }    # else fail silently
111    return '';
112}
113
114sub _includeProtocol {
115    my ( $this, $handler, $control, $params ) = @_;
116
117    eval 'use Foswiki::IncludeHandlers::' . $handler . ' ()';
118    if ($@) {
119        return $this->_includeWarning( $control->{warn}, 'bad_include_path',
120            $control->{_DEFAULT} );
121    }
122    else {
123        $handler = 'Foswiki::IncludeHandlers::' . $handler;
124        return $handler->INCLUDE( $this, $control, $params );
125    }
126}
127
128sub _includeTopic {
129    my ( $this, $includingTopicObject, $control, $params ) = @_;
130
131    my $includedWeb;
132    my $includedTopic = $control->{_DEFAULT};
133    $includedTopic =~ s/\.txt$//;    # strip optional (undocumented) .txt
134
135    ( $includedWeb, $includedTopic ) =
136      $this->normalizeWebTopicName( $includingTopicObject->web,
137        $includedTopic );
138
139    if ( !Foswiki::isValidTopicName( $includedTopic, 1 ) ) {
140        return $this->_includeWarning( $control->{warn}, 'bad_include_path',
141            $control->{_DEFAULT} ), 'bad_include_path';
142    }
143
144    # See Codev.FailedIncludeWarning for the history.
145    unless ( $this->{store}->topicExists( $includedWeb, $includedTopic ) ) {
146        return _includeWarning( $this, $control->{warn}, 'topic_not_found',
147            $includedWeb, $includedTopic ), 'topic_not_found';
148    }
149
150    # prevent recursive includes. Note that the inclusion of a topic into
151    # itself is not blocked; however subsequent attempts to include the
152    # topic will fail. There is a hard block of 99 on any recursive include.
153    my $key = $includingTopicObject->web . '.' . $includingTopicObject->topic;
154    my $count = grep( $key, keys %{ $this->{_INCLUDES} } );
155    $key .= $control->{_sArgs};
156    if ( $this->{_INCLUDES}->{$key} || $count > 99 ) {
157        return _includeWarning( $this, $control->{warn}, 'already_included',
158            "$includedWeb.$includedTopic", '' ), 'already_included';
159    }
160
161    # Push the topic context to the included topic, so we can create
162    # local (SESSION) macro definitions without polluting the including
163    # topic namespace.
164    $this->{prefs}->pushTopicContext( $this->{webName}, $this->{topicName} );
165
166    $this->{_INCLUDES}->{$key} = 1;
167
168    my $includedTopicObject =
169      Foswiki::Meta->load( $this, $includedWeb, $includedTopic,
170        $control->{rev} );
171    unless ( $includedTopicObject->haveAccess('VIEW') ) {
172        if ( isTrue( $control->{warn} ) ) {
173            return $this->inlineAlert( 'alerts', 'access_denied',
174                "[[$includedWeb.$includedTopic]]" ), 'access_denied';
175        }    # else fail silently
176        return '', 'access_denied';
177    }
178    my $memWeb   = $this->{prefs}->getPreference('INCLUDINGWEB');
179    my $memTopic = $this->{prefs}->getPreference('INCLUDINGTOPIC');
180
181    my $text = '';
182    my $error = '';
183    my $verbatim   = {};
184    my $dirtyAreas = {};
185    try {
186
187        # Copy params into session level preferences. That way finalisation
188        # will apply to them. These preferences will be popped when the topic
189        # context is restored after the include.
190        $this->{prefs}->setSessionPreferences(%$params);
191
192        # Set preferences that finalisation does *not* apply to
193        $this->{prefs}->setInternalPreferences(
194            INCLUDINGWEB   => $includingTopicObject->web,
195            INCLUDINGTOPIC => $includingTopicObject->topic
196        );
197
198        $text = $includedTopicObject->text;
199
200        # Simplify leading, and remove trailing, newlines. If we don't remove
201        # trailing, it becomes impossible to %INCLUDE a topic into a table.
202        $text =~ s/^[\r\n]+/\n/;
203        $text =~ s/[\r\n]+$//;
204
205        # remove everything before and after the default include block unless
206        # a section is explicitly defined
207        if ( !$control->{section} ) {
208            $text =~ s/.*?%STARTINCLUDE%//s;
209            $text =~ s/%STOPINCLUDE%.*//s;
210        }
211
212        # prevent dirty areas in included topics from being parsed
213        $text = takeOutBlocks( $text, 'dirtyarea', $dirtyAreas )
214          if $Foswiki::cfg{Cache}{Enabled};
215
216        # handle sections
217        my ( $ntext, $sections ) = parseSections($text);
218
219        my $interesting = ( defined $control->{section} );
220        if ( $interesting || scalar(@$sections) ) {
221
222            # Rebuild the text from the interesting sections
223            $text = '';
224            foreach my $s (@$sections) {
225                if (   $control->{section}
226                    && $s->{type} eq 'section'
227                    && $s->{name} eq $control->{section} )
228                {
229                    $text .=
230                      substr( $ntext, $s->{start}, $s->{end} - $s->{start} );
231                    $interesting = 1;
232                    last;
233                }
234                elsif ( $s->{type} eq 'include' && !$control->{section} ) {
235                    $text .=
236                      substr( $ntext, $s->{start}, $s->{end} - $s->{start} );
237                    $interesting = 1;
238                }
239            }
240        }
241
242        if ( $interesting and ( length($text) eq 0 ) ) {
243            $text =
244              _includeWarning( $this, $control->{warn},
245                'topic_section_not_found', $includedWeb, $includedTopic,
246                $control->{section} );
247            $error = 'topic_section_not_found';
248        }
249        else {
250
251            # If there were no interesting sections, restore the whole text
252            $text = $ntext unless $interesting;
253
254            $text = applyPatternToIncludedText( $text, $control->{pattern} )
255              if ( $control->{pattern} );
256
257            # Do not show TOC in included topic if TOC_HIDE_IF_INCLUDED
258            # preference has been set
259            if ( isTrue( $this->{prefs}->getPreference('TOC_HIDE_IF_INCLUDED') )
260              )
261            {
262                $text =~ s/%TOC(?:{(.*?)})?%//g;
263            }
264
265            $this->innerExpandMacros( \$text, $includedTopicObject );
266
267        # Item9569: remove verbatim blocks from text passed to commonTagsHandler
268            $text = takeOutBlocks( $text, 'verbatim', $verbatim );
269
270            # 4th parameter tells plugin that its called for an included file
271            $this->{plugins}
272              ->dispatch( 'commonTagsHandler', $text, $includedTopic,
273                $includedWeb, 1, $includedTopicObject );
274            putBackBlocks( \$text, $verbatim, 'verbatim' );
275
276            # We have to expand tags again, because a plugin may have inserted
277            # additional tags.
278            $this->innerExpandMacros( \$text, $includedTopicObject );
279
280            # If needed, fix all 'TopicNames' to 'Web.TopicNames' to get the
281            # right context so that links continue to work properly
282            if ( $includedWeb ne $includingTopicObject->web ) {
283                my $removed = {};
284
285                $text = $this->renderer->forEachLine(
286                    $text,
287                    \&_fixupIncludedTopic,
288                    {
289                        web        => $includedWeb,
290                        pre        => 1,
291                        noautolink => 1
292                    }
293                );
294
295                # handle tags again because of plugin hook
296                innerExpandMacros( $this, \$text, $includedTopicObject );
297            }
298        }
299    }
300    finally {
301
302        # always restore the context, even in the event of an error
303        delete $this->{_INCLUDES}->{$key};
304
305        $this->{prefs}->setInternalPreferences(
306            INCLUDINGWEB   => $memWeb,
307            INCLUDINGTOPIC => $memTopic
308        );
309
310        # restoring dirty areas
311        putBackBlocks( \$text, $dirtyAreas, 'dirtyarea' )
312          if $Foswiki::cfg{Cache}{Enabled};
313
314        ( $this->{webName}, $this->{topicName} ) =
315          $this->{prefs}->popTopicContext();
316    };
317
318    return ($text, $error);
319}
320
321# Processes a specific instance %<nop>INCLUDE{...}% syntax.
322# Returns the text to be inserted in place of the INCLUDE command.
323# $includingTopicObject should be for the immediate parent topic in the
324# include hierarchy. Works for both URLs and absolute server paths.
325sub INCLUDE {
326    my ( $this, $params, $includingTopicObject ) = @_;
327
328    # remember args for the key before mangling the params
329    my $args = $params->stringify();
330
331    # Remove params, so they don't get expanded in the included page
332    my %control;
333    for my $p (qw(_DEFAULT pattern rev section raw warn)) {
334        $control{$p} = $params->remove($p);
335    }
336    $control{_sArgs} = $args;
337
338    $control{warn} ||= $this->{prefs}->getPreference('INCLUDEWARNING');
339
340    my $text;
341
342    # make sure we have something to include. If we don't do this, then
343    # normalizeWebTopicName will default to WebHome. TWikibug:Item2209.
344    unless ( $control{_DEFAULT} ) {
345        $text =
346          $this->_includeWarning( $control{warn}, 'bad_include_path', '' );
347    }
348
349    # Filter out '..' from path to prevent includes of '../../file'
350    elsif ( $Foswiki::cfg{DenyDotDotInclude} && $control{_DEFAULT} =~ /\.\./ ) {
351        $text =
352          $this->_includeWarning( $control{warn}, 'bad_include_path',
353            $control{_DEFAULT} );
354    }
355
356    else {
357
358        # no sense in considering an empty string as an unfindable section
359        delete $control{section}
360          if ( defined( $control{section} ) && $control{section} eq '' );
361        $control{raw} ||= '';
362        $control{inWeb}   = $includingTopicObject->web;
363        $control{inTopic} = $includingTopicObject->topic;
364
365        # Protocol links e.g. http:, https:, doc:
366        if ( $control{_DEFAULT} =~ /^([a-z]+):/ ) {
367            $text = $this->_includeProtocol( $1, \%control, $params );
368        }
369        else {
370            my @topics = split(/,\s*/, $control{_DEFAULT});
371            my $error;
372            foreach my $t (@topics) {
373                $control{_DEFAULT} = $t;
374                ($text, $error) =
375                  $this->_includeTopic( $includingTopicObject, \%control, $params );
376                last if ($error eq '');
377            } 
378        }
379    }
380
381    # Apply any heading offset
382    my $hoff = $params->{headingoffset};
383    if ( $hoff && $hoff =~ /^([-+]?\d+)$/ && $1 != 0 ) {
384        my ( $off, $noff ) = ( 0 + $1, 0 - $1 );
385        $text = "<ho off=\"$off\"/>\n$text\n<ho off=\"$noff\">";
386    }
387
388    return $text;
389}
390
3911;
392__END__
393Foswiki - The Free and Open Source Wiki, http://foswiki.org/
394
395Copyright (C) 2008-2009 Foswiki Contributors. Foswiki Contributors
396are listed in the AUTHORS file in the root of this distribution.
397NOTE: Please extend that file, not this notice.
398
399Additional copyrights apply to some or all of the code in this
400file as follows:
401
402Copyright (C) 1999-2007 Peter Thoeny, peter@thoeny.org
403and TWiki Contributors. All Rights Reserved. TWiki Contributors
404are listed in the AUTHORS file in the root of this distribution.
405Based on parts of Ward Cunninghams original Wiki and JosWiki.
406Copyright (C) 1998 Markus Peter - SPiN GmbH (warpi@spin.de)
407Some changes by Dave Harris (drh@bhresearch.co.uk) incorporated
408
409This program is free software; you can redistribute it and/or
410modify it under the terms of the GNU General Public License
411as published by the Free Software Foundation; either version 2
412of the License, or (at your option) any later version. For
413more details read LICENSE in the root of this distribution.
414
415This program is distributed in the hope that it will be useful,
416but WITHOUT ANY WARRANTY; without even the implied warranty of
417MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
418
419As per the GPL, removal of this notice is prohibited.
Note: See TracBrowser for help on using the repository browser.