lib/TWiki/UI/View.pm
changeset 0 414e01d06fd5
child 1 e2915a7cbdfa
equal deleted inserted replaced
-1:000000000000 0:414e01d06fd5
       
     1 # Module of TWiki Enterprise Collaboration Platform, http://TWiki.org/
       
     2 #
       
     3 # Copyright (C) 1999-2007 Peter Thoeny, peter@thoeny.org
       
     4 # and TWiki Contributors. All Rights Reserved. TWiki Contributors
       
     5 # are listed in the AUTHORS file in the root of this distribution.
       
     6 # NOTE: Please extend that file, not this notice.
       
     7 #
       
     8 # Additional copyrights apply to some or all of the code in this
       
     9 # file as follows:
       
    10 # Based on parts of Ward Cunninghams original Wiki and JosWiki.
       
    11 # Copyright (C) 1998 Markus Peter - SPiN GmbH (warpi@spin.de)
       
    12 # Some changes by Dave Harris (drh@bhresearch.co.uk) incorporated
       
    13 #
       
    14 # This program is free software; you can redistribute it and/or
       
    15 # modify it under the terms of the GNU General Public License
       
    16 # as published by the Free Software Foundation; either version 2
       
    17 # of the License, or (at your option) any later version. For
       
    18 # more details read LICENSE in the root of this distribution.
       
    19 #
       
    20 # This program is distributed in the hope that it will be useful,
       
    21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
       
    22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
       
    23 #
       
    24 # As per the GPL, removal of this notice is prohibited.
       
    25 
       
    26 =pod
       
    27 
       
    28 ---+ package TWiki::UI::View
       
    29 
       
    30 UI delegate for view function
       
    31 
       
    32 =cut
       
    33 
       
    34 package TWiki::UI::View;
       
    35 
       
    36 use strict;
       
    37 use integer;
       
    38 use Monitor;
       
    39 
       
    40 use CGI::Carp qw( fatalsToBrowser );
       
    41 use CGI qw( -any ); # pretty basic, this
       
    42 
       
    43 require TWiki;
       
    44 require TWiki::UI;
       
    45 require TWiki::Sandbox;
       
    46 require TWiki::OopsException;
       
    47 
       
    48 =pod
       
    49 
       
    50 ---++ StaticMethod view( $session )
       
    51 
       
    52 =view= command handler.
       
    53 This method is designed to be
       
    54 invoked via the =UI::run= method.
       
    55 
       
    56 Generate a complete HTML page that represents the viewed topics.
       
    57 The view is controlled by CGI parameters as follows:
       
    58 
       
    59 | =rev= | topic revision to view |
       
    60 | =section= | restrict view to a named section |
       
    61 | =raw= | no format body text if set |
       
    62 | =skin= | comma-separated list of skin(s) to use |
       
    63 | =contenttype= | Allows you to specify an alternate content type |
       
    64 
       
    65 =cut
       
    66 
       
    67 sub view {
       
    68     my $session = shift;
       
    69 
       
    70     my $query = $session->{cgiQuery};
       
    71     my $webName = $session->{webName};
       
    72     my $topicName = $session->{topicName};
       
    73 
       
    74     my $raw = $query->param( 'raw' ) || '';
       
    75     my $contentType = $query->param( 'contenttype' );
       
    76 
       
    77     my $showRev = 1;
       
    78     my $logEntry = '';
       
    79     my $revdate = '';
       
    80     my $revuser = '';
       
    81     my $store = $session->{store};
       
    82     # is this view indexable by search engines? Default yes.
       
    83     my $indexableView = 1;
       
    84 
       
    85     TWiki::UI::checkWebExists( $session, $webName, $topicName, 'view' );
       
    86 
       
    87     my $skin = $session->getSkin();
       
    88 
       
    89     my $rev = $store->cleanUpRevID( $query->param( 'rev' ));
       
    90 
       
    91     my $topicExists =
       
    92       $store->topicExists( $webName, $topicName );
       
    93 
       
    94     # text and meta of the _latest_ rev of the topic
       
    95     my( $currText, $currMeta );
       
    96     # text and meta of the chosen rev of the topic
       
    97     my( $meta, $text );
       
    98     if( $topicExists ) {
       
    99         require TWiki::Time;
       
   100         ( $currMeta, $currText ) = $store->readTopic
       
   101           ( $session->{user}, $webName, $topicName, undef );
       
   102         TWiki::UI::checkAccess( $session, $webName, $topicName,
       
   103                                 'VIEW', $session->{user}, $currText );
       
   104         ( $revdate, $revuser, $showRev ) = $currMeta->getRevisionInfo();
       
   105         $revdate = TWiki::Time::formatTime( $revdate );
       
   106 
       
   107         if ( !$rev || $rev > $showRev ) {
       
   108             $rev = $showRev;
       
   109         }
       
   110 
       
   111         if( $rev < $showRev ) {
       
   112             ( $meta, $text ) = $store->readTopic
       
   113               ( $session->{user}, $webName, $topicName, $rev );
       
   114 
       
   115             ( $revdate, $revuser ) = $meta->getRevisionInfo();
       
   116             $revdate = TWiki::Time::formatTime( $revdate );
       
   117             $logEntry .= 'r'.$rev;
       
   118         } else {
       
   119             # viewing the most recent rev
       
   120             ( $text, $meta ) = ( $currText, $currMeta );
       
   121         }
       
   122 
       
   123         # So we're reading an existing topic here.  It is about time
       
   124         # to apply the 'section' selection (and maybe others in the
       
   125         # future as well).  $text is cleared unless a named section
       
   126         # matching the 'section' URL parameter is found.
       
   127         if (my $section  =  $query->param('section')) {
       
   128             my ( $ntext, $sections ) = TWiki::parseSections( $text );
       
   129             $text = ''; # in the beginning, there was ... NO section
       
   130           FINDSECTION:
       
   131             for my $s (@$sections) {
       
   132                 if ($s->{type} eq 'section'  &&  $s->{name} eq $section) {
       
   133                     $text = substr( $ntext, $s->{start}, $s->{end}-$s->{start} );
       
   134                     last FINDSECTION;
       
   135                 }
       
   136             }
       
   137         }
       
   138 
       
   139     } else { # Topic does not exist yet
       
   140         $indexableView = 0;
       
   141         $session->enterContext( 'new_topic' );
       
   142         $rev = 1;
       
   143         if( TWiki::isValidTopicName( $topicName )) {
       
   144             ( $currMeta, $currText ) =
       
   145               TWiki::UI::readTemplateTopic( $session, 'WebTopicViewTemplate' );
       
   146         } else {
       
   147             ( $currMeta, $currText ) =
       
   148               TWiki::UI::readTemplateTopic( $session, 'WebTopicNonWikiTemplate' );
       
   149         }
       
   150         ( $text, $meta ) = ( $currText, $currMeta );
       
   151         $logEntry .= ' (not exist)';
       
   152     }
       
   153 
       
   154     if( $raw ) {
       
   155         $indexableView = 0;
       
   156         $logEntry .= ' raw='.$raw;
       
   157         if( $raw eq 'debug' || $raw eq 'all' ) {
       
   158             $text = $store->getDebugText( $meta, $text );
       
   159         }
       
   160     }
       
   161 
       
   162     if( $TWiki::cfg{Log}{view} ) {
       
   163         $session->writeLog( 'view', $webName.'.'.$topicName, $logEntry );
       
   164     }
       
   165 
       
   166     my( $mirrorSiteName, $mirrorViewURL, $mirrorLink, $mirrorNote ) =
       
   167       $session->readOnlyMirrorWeb( $webName );
       
   168 
       
   169     # Note; must enter all contexts before the template is read, as
       
   170     # TMPL:P is expanded on the fly in the template reader. :-(
       
   171     my( $revTitle, $revArg ) = ( '', '' );
       
   172     if( $mirrorSiteName ) {
       
   173         $session->enterContext( 'inactive' );
       
   174         unless( $topicExists ) {
       
   175             $text = '';
       
   176         }
       
   177     } elsif( $rev < $showRev ) {
       
   178         $session->enterContext( 'inactive' );
       
   179         # disable edit of previous revisions
       
   180         $revTitle = '(r'.$rev.')';
       
   181         $revArg = '&rev='.$rev;
       
   182     }
       
   183 
       
   184     my $template = $query->param( 'template' ) ||
       
   185       $session->{prefs}->getPreferencesValue( 'VIEW_TEMPLATE' ) ||
       
   186         'view';
       
   187 
       
   188     # Item4756: Always use default view template for raw=debug and raw=all
       
   189     if( $raw eq 'debug' || $raw eq 'all' ) {
       
   190         $template = 'view';
       
   191     }
       
   192 
       
   193     my $tmpl = $session->templates->readTemplate( $template, $skin );
       
   194     if( !$tmpl && $template ne 'view' ) {
       
   195         $tmpl = $session->templates->readTemplate( 'view', $skin );
       
   196     }
       
   197 
       
   198     if( !$tmpl ) {
       
   199         throw TWiki::OopsException( 'attention',
       
   200                                     def => 'no_such_template',
       
   201                                     web => $webName,
       
   202                                     topic => $topicName,
       
   203                                     params => [ $template, 'VIEW_TEMPLATE' ] );
       
   204     }
       
   205 
       
   206     $tmpl =~ s/%REVINFO%/%REVINFO%$mirrorNote/go;
       
   207     $tmpl =~ s/%REVTITLE%/$revTitle/g;
       
   208     $tmpl =~ s/%REVARG%/$revArg/g;
       
   209 
       
   210     if( $indexableView &&
       
   211           $TWiki::cfg{AntiSpam}{RobotsAreWelcome} &&
       
   212             !$query->param() ) {
       
   213         # it's an indexable view type, there are no parameters
       
   214         # on the url, and robots are welcome. Remove the NOINDEX meta tag
       
   215         $tmpl =~ s/<meta name="robots"[^>]*>//goi;
       
   216     }
       
   217 
       
   218     # Show revisions around the one being displayed
       
   219     # we start at $showRev then possibly jump near $rev if too distant
       
   220     my $revsToShow = $TWiki::cfg{NumberOfRevisions} + 1;
       
   221     $revsToShow = $showRev if $showRev < $revsToShow;
       
   222     my $doingRev = $showRev;
       
   223     my $revs = '';
       
   224     while( $revsToShow > 0 ) {
       
   225         $revsToShow--;
       
   226         if( $doingRev == $rev) {
       
   227             $revs .= 'r'.$rev;
       
   228         } else {
       
   229             $revs .= CGI::a({
       
   230                              href=>$session->getScriptUrl( 0,
       
   231                                                            'view',
       
   232                                                            $webName,
       
   233                                                            $topicName,
       
   234                                                            rev => $doingRev ),
       
   235                              rel => 'nofollow'
       
   236                             },
       
   237                             "r$doingRev" );
       
   238         }
       
   239         if ( $doingRev - $rev >= $TWiki::cfg{NumberOfRevisions} ) {
       
   240             # we started too far away, need to jump closer to $rev
       
   241             use integer;
       
   242             $doingRev = $rev + $revsToShow / 2;
       
   243             $doingRev = $revsToShow if $revsToShow > $doingRev;
       
   244             $revs .= ' | ';
       
   245             next;
       
   246         }
       
   247         if( $revsToShow ) {
       
   248             $revs .= '&nbsp;' . CGI::a
       
   249               ( { href=>$session->getScriptUrl(
       
   250                   0, 'rdiff', $webName, $topicName,
       
   251                   rev1 => $doingRev,
       
   252                   rev2 => $doingRev-1 ),
       
   253                   rel => 'nofollow' },
       
   254                 '&lt;' ) . '&nbsp;';
       
   255         }
       
   256         $doingRev--;
       
   257     }
       
   258 
       
   259     my $ri = $session->renderer->renderRevisionInfo( $webName,
       
   260                                                      $topicName,
       
   261                                                      $meta );
       
   262     $tmpl =~ s/%REVINFO%/$ri/go;
       
   263     $tmpl =~ s/%REVISIONS%/$revs/go;
       
   264 
       
   265     ## SMELL: This is also used in TWiki::_TOC. Could insert a tag in
       
   266     ## TOC and remove all those here, finding the parameters only once
       
   267     my @qparams = ();
       
   268     foreach my $name ( $query->param ) {
       
   269       next if ($name eq 'keywords');
       
   270       next if ($name eq 'topic');
       
   271       push @qparams, $name => $query->param($name);
       
   272     }
       
   273     $tmpl =~ s/%QUERYPARAMSTRING%/TWiki::_make_params(1,@qparams)/geo;
       
   274 
       
   275     # extract header and footer from the template, if there is a
       
   276     # %TEXT% tag marking the split point. The topic text is inserted
       
   277     # in place of the %TEXT% tag. The text before this tag is inserted
       
   278     # as header, the text after is inserted as footer. If there is a
       
   279     # %STARTTEXT% tag present, the header text between %STARTTEXT% and
       
   280     # %TEXT is rendered together, as is the footer text between %TEXT%
       
   281     # and %ENDTEXT%, if present. This allows correct handling of TWiki
       
   282     # markup in header or footer if those do require examination of the
       
   283     # topic text to work correctly (e.g., %TOC%).
       
   284     # Note: This feature is experimental and may be replaced by an
       
   285     # alternative solution not requiring additional tags.
       
   286     my( $start, $end );
       
   287     if( $tmpl =~ m/^(.*)%TEXT%(.*)$/s ) {
       
   288         my @starts = split( /%STARTTEXT%/, $1 );
       
   289         if ( $#starts > 0 ) {
       
   290             # we know that there is something before %STARTTEXT%
       
   291             $start = $starts[0];
       
   292             $text = $starts[1] . $text;
       
   293         } else {
       
   294             $start = $1;
       
   295         }
       
   296         my @ends = split( /%ENDTEXT%/, $2 );
       
   297         if ( $#ends > 0 ) {
       
   298             # we know that there is something after %ENDTEXT%
       
   299             $text .= $ends[0];
       
   300             $end = $ends[1];
       
   301         } else {
       
   302             $end = $2;
       
   303         }
       
   304     } else {
       
   305         my @starts = split( /%STARTTEXT%/, $tmpl );
       
   306         if ( $#starts > 0 ) {
       
   307             # we know that there is something before %STARTTEXT%
       
   308             $start = $starts[0];
       
   309             $text = $starts[1];
       
   310         } else {
       
   311             $start = $tmpl;
       
   312             $text = '';
       
   313         }
       
   314         $end = '';
       
   315     }
       
   316 
       
   317     # If minimalist is set, images and anchors will be stripped from text
       
   318     my $minimalist = 0;
       
   319     if( $contentType ) {
       
   320         $minimalist = ( $skin =~ /\brss/ );
       
   321     } elsif( $skin =~ /\brss/ ) {
       
   322         $contentType = 'text/xml';
       
   323         $minimalist = 1;
       
   324     } elsif( $skin =~ /\bxml/ ) {
       
   325         $contentType = 'text/xml';
       
   326         $minimalist = 1;
       
   327     } elsif( $raw eq 'text' || $raw eq 'all' ) {
       
   328         $contentType = 'text/plain';
       
   329     } else {
       
   330         $contentType = 'text/html'
       
   331     }
       
   332     $session->{SESSION_TAGS}{MAXREV} = $showRev;
       
   333     $session->{SESSION_TAGS}{CURRREV} = $rev;
       
   334 
       
   335     # Set page generation mode to RSS if using an RSS skin
       
   336     $session->enterContext( 'rss' ) if $skin =~ /\brss/;
       
   337 
       
   338     # Set the meta-object that contains the rendering info
       
   339     # SMELL: hack to get around not having a proper topic object model
       
   340     $session->enterContext( 'can_render_meta', $meta );
       
   341 
       
   342     my $page;
       
   343     # Legacy: If the _only_ skin is 'text' it is used like this:
       
   344     # http://.../view/Codev/MyTopic?skin=text&contenttype=text/plain&raw=on
       
   345     # which shows the topic as plain text; useful for those who want
       
   346     # to download plain text for the topic. So when the skin is 'text'
       
   347     # we do _not_ want to create a textarea.
       
   348     # raw=on&skin=text is deprecated; use raw=text instead.
       
   349     Monitor::MARK('Ready to render');
       
   350     if( $raw eq 'text' || $raw eq 'all' || ( $raw && $skin eq 'text' )) {
       
   351         # use raw text
       
   352         $page = $text;
       
   353     } else {
       
   354         my @args = ( $session, $webName, $topicName, $meta, $minimalist );
       
   355 
       
   356         $session->enterContext( 'header_text' );
       
   357         $page = _prepare($start, @args);
       
   358         $session->leaveContext( 'header_text' );
       
   359         Monitor::MARK('Rendered header');
       
   360 
       
   361         if( $raw ) {
       
   362             if ($text) {
       
   363                 my $p = $session->{prefs};
       
   364                 $page .=
       
   365                   CGI::textarea(
       
   366                       -readonly => 'readonly',
       
   367                       -rows => $p->getPreferencesValue('EDITBOXHEIGHT'),
       
   368                       -cols => $p->getPreferencesValue('EDITBOXWIDTH'),
       
   369                       -style => $p->getPreferencesValue('EDITBOXSTYLE'),
       
   370                       -class => 'twikiTextarea twikiTextareaRawView',
       
   371                       -id => 'topic',
       
   372                       -default => $text
       
   373                      );
       
   374             }
       
   375         } else {
       
   376             $session->enterContext( 'body_text' );
       
   377             $page .= _prepare($text, @args);
       
   378             $session->leaveContext( 'body_text' );
       
   379         }
       
   380 
       
   381         Monitor::MARK('Rendered body');
       
   382         $session->enterContext( 'footer_text' );
       
   383         $page .= _prepare($end, @args);
       
   384         $session->leaveContext( 'footer_text' );
       
   385         Monitor::MARK('Rendered footer');
       
   386     }
       
   387     # Output has to be done in one go, because if we generate the header and
       
   388     # then redirect because of some later constraint, some browsers fall over
       
   389     $session->writeCompletePage( $page, 'view', $contentType );
       
   390     Monitor::MARK('Wrote HTML');
       
   391 }
       
   392 
       
   393 sub _prepare {
       
   394     my( $text, $session, $webName, $topicName, $meta, $minimalist) = @_;
       
   395 
       
   396     $text = $session->handleCommonTags( $text, $webName, $topicName, $meta );
       
   397     $text = $session->renderer->getRenderedVersion( $text, $webName, $topicName );
       
   398     $text =~ s/( ?) *<\/?(nop|noautolink)\/?>\n?/$1/gois;
       
   399 
       
   400     if( $minimalist ) {
       
   401         $text =~ s/<img [^>]*>//gi;  # remove image tags
       
   402         $text =~ s/<a [^>]*>//gi;    # remove anchor tags
       
   403         $text =~ s/<\/a>//gi;        # remove anchor tags
       
   404     }
       
   405 
       
   406     return $text;
       
   407 }
       
   408 
       
   409 =pod
       
   410 
       
   411 ---++ StaticMethod viewfile( $session, $web, $topic, $query )
       
   412 
       
   413 =viewfile= command handler.
       
   414 This method is designed to be
       
   415 invoked via the =UI::run= method.
       
   416 Command handler for viewfile. View a file in the browser.
       
   417 Some parameters are passed in CGI query:
       
   418 | =filename= | Attachment to view |
       
   419 | =rev= | Revision to view |
       
   420 
       
   421 =cut
       
   422 
       
   423 sub viewfile {
       
   424     my $session = shift;
       
   425 
       
   426     my $query = $session->{cgiQuery};
       
   427 
       
   428     my $topic = $session->{topicName};
       
   429     my $webName = $session->{webName};
       
   430 
       
   431     my @path = split( '/', $query->path_info() );
       
   432     shift( @path )unless $path[0];
       
   433     my $fileName;
       
   434     if( defined( $query->param( 'filename' ))) {
       
   435         $fileName = $query->param( 'filename' );
       
   436     } else {
       
   437         $fileName = pop( @path );
       
   438     }
       
   439     if (!$fileName) {
       
   440         throw TWiki::OopsException( 'attention',
       
   441                                     def => 'no_such_attachment',
       
   442                                     web => 'Unknown',
       
   443                                     topic => 'Unknown',
       
   444                                     params => [ 'viewfile', '?' ] );
       
   445     }
       
   446 
       
   447     $fileName = TWiki::Sandbox::sanitizeAttachmentName( $fileName );
       
   448 
       
   449     my $rev = $session->{store}->cleanUpRevID( $query->param( 'rev' ) );
       
   450     unless( $fileName && $session->{store}->attachmentExists(
       
   451         $webName, $topic, $fileName )) {
       
   452         throw TWiki::OopsException( 'attention',
       
   453                                     def => 'no_such_attachment',
       
   454                                     web => $webName,
       
   455                                     topic => $topic,
       
   456                                     params => [ 'viewfile', $fileName||'?' ] );
       
   457     }
       
   458     my $fileContent = $session->{store}->readAttachment(
       
   459         $session->{user}, $webName, $topic, $fileName, $rev );
       
   460 
       
   461     my $type = _suffixToMimeType( $session, $fileName );
       
   462     my $length =  length( $fileContent );
       
   463     my $dispo = 'inline;filename='.$fileName;
       
   464 
       
   465     print <<HERE;
       
   466 Content-type: $type
       
   467 Content-length: $length
       
   468 Content-Disposition: $dispo
       
   469 
       
   470 $fileContent
       
   471 HERE
       
   472 }
       
   473 
       
   474 sub _suffixToMimeType {
       
   475     my( $session, $theFilename ) = @_;
       
   476 
       
   477     my $mimeType = 'text/plain';
       
   478     if( $theFilename =~ /\.([^.]+)$/ ) {
       
   479         my $suffix = $1;
       
   480         my @types = grep{ s/^\s*([^\s]+).*?\s$suffix\s.*$/$1/i }
       
   481           map{ $_.' ' }
       
   482             split( /[\n\r]/,
       
   483                    TWiki::readFile( $TWiki::cfg{MimeTypesFileName} ) );
       
   484         $mimeType = $types[0] if( @types );
       
   485     }
       
   486     return $mimeType;
       
   487 }
       
   488 
       
   489 1;