# Module of TWiki Enterprise Collaboration Platform, http://TWiki.org/ # # Copyright (C) 1999-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. # # Additional copyrights apply to some or all of the code in this # file as follows: # Based on parts of Ward Cunninghams original Wiki and JosWiki. # Copyright (C) 1998 Markus Peter - SPiN GmbH (warpi@spin.de) # Some changes by Dave Harris (drh@bhresearch.co.uk) incorporated # # 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. =pod ---+ package TWiki::UI::View UI delegate for view function =cut package TWiki::UI::View; use strict; use integer; require TWiki; require TWiki::UI; require TWiki::Sandbox; require TWiki::OopsException; =pod ---++ StaticMethod view( $session ) =view= command handler. This method is designed to be invoked via the =UI::run= method. Generate a complete HTML page that represents the viewed topics. The view is controlled by CGI parameters as follows: | =rev= | topic revision to view | | =section= | restrict view to a named section | | =raw= | no format body text if set | | =skin= | comma-separated list of skin(s) to use | | =contenttype= | Allows you to specify an alternate content type | =cut sub view { my $session = shift; my $query = $session->{request}; my $webName = $session->{webName}; my $topicName = $session->{topicName}; $session->{plugins}->dispatch('viewRedirectHandler', $session, $webName, $topicName) and return; my $raw = $query->param( 'raw' ) || ''; my $contentType = $query->param( 'contenttype' ); my $showRev = 1; my $logEntry = ''; my $revdate = ''; my $revuser = ''; my $store = $session->{store}; # is this view indexable by search engines? Default yes. my $indexableView = 1; my $status; # bin/view/~jsmith support if ( $session->{pathInfoWithTilde} && $session->{pathInfoWithTilde} =~ m:^/~([^/]*)(/.*)?$: ) { my ($head, $tail) = ($1, $2); my $wikiName; my $users = $session->{users}; if ( $head eq '' ) { $wikiName = $users->getWikiName($session->{user}); } else { $wikiName = $users->getWikiName($users->getCanonicalUserID($head)); } unless ( $tail ) { if ( $TWiki::cfg{EnableUserSubwebs} ) { # if user subwebs are enabled, check it first my $store = $session->{store}; if ( $store->topicExists( $TWiki::cfg{UsersWebName}.'/'.$wikiName, $TWiki::cfg{HomeTopicName}) ) { $tail = '/'; } elsif ( $store->topicExists($TWiki::cfg{UsersWebName}, $wikiName) ) { $tail = ''; } else { $tail = '/'; } } else { $tail = ''; } } $session->redirect( $session->getScriptUrl(1, "view").'/'. $TWiki::cfg{UsersWebName}.'/'.$wikiName.$tail, 1 ); return; } TWiki::UI::checkWebExists( $session, $webName, $topicName, 'view' ); my $skin = $session->getSkin(); my $rev = $store->cleanUpRevID( $query->param( 'rev' )); my $topicExists = $store->topicExists( $webName, $topicName ); if ( !$topicExists && $store->webExists( "$webName/$topicName" ) ) { # If the topic does not exist but the subweb exists, redirect to # the subweb. # It would be nice if we could avoid redirection. # But at this point, $session initialization completed and it's # cumbersome and not so safe to ammend it. $session->redirect( $session->getScriptUrl(1, "view", "$webName/$topicName", $TWiki::cfg{HomeTopicName})); return; } # text and meta of the _latest_ rev of the topic my( $currText, $currMeta ); # text and meta of the chosen rev of the topic my( $meta, $text ); if( $topicExists ) { require TWiki::Time; ( $currMeta, $currText ) = $store->readTopic ( $session->{user}, $webName, $topicName, undef ); TWiki::UI::checkAccess( $session, $webName, $topicName, 'VIEW', $session->{user}, $currText ); ( $revdate, $revuser, $showRev ) = $currMeta->getRevisionInfo(); $revdate = TWiki::Time::formatTime( $revdate ); if ( !$rev || $rev > $showRev ) { $rev = $showRev; } if( $rev < $showRev ) { ( $meta, $text ) = $store->readTopic ( $session->{user}, $webName, $topicName, $rev ); ( $revdate, $revuser ) = $meta->getRevisionInfo(); $revdate = TWiki::Time::formatTime( $revdate ); $logEntry .= 'r'.$rev; } else { # viewing the most recent rev ( $text, $meta ) = ( $currText, $currMeta ); } # So we're reading an existing topic here. It is about time # to apply the 'section' selection (and maybe others in the # future as well). $text is cleared unless a named section # matching the 'section' URL parameter is found. if (my $section = $query->param('section')) { my ( $ntext, $sections ) = TWiki::parseSections( $text ); $text = ''; # in the beginning, there was ... NO section FINDSECTION: for my $s (@$sections) { if ($s->{type} eq 'section' && $s->{name} eq $section) { $text = substr( $ntext, $s->{start}, $s->{end}-$s->{start} ); last FINDSECTION; } } } } else { # Topic does not exist yet if( $query->param( 'createifnotexist' ) ) { # redirect to the save script to force topic creation my $redirecturl = $session->getScriptUrl( 1, 'save', $webName, $topicName ); # force POST method because GET is disabled in save $query->request_method( 'POST' ); # SMELL: Security issue? $query->delete( 'createifnotexist' ); # remove 'createifnotexist' parameter # add crypt token if needed if( $TWiki::cfg{CryptToken}{Enable} ) { $query->param( 'crypttoken', $session->CRYPTTOKEN() ); } # redirect to save script, all URL parameters are taken into account $session->redirect( $redirecturl, 1 ); } $indexableView = 0; $session->enterContext( 'new_topic' ); $rev = 1; if( TWiki::isValidTopicName( $topicName )) { ( $currMeta, $currText ) = TWiki::UI::readTemplateTopic( $session, 'WebTopicViewTemplate' ); } else { ( $currMeta, $currText ) = TWiki::UI::readTemplateTopic( $session, 'WebTopicNonWikiTemplate' ); } ( $text, $meta ) = ( $currText, $currMeta ); $logEntry .= ' ' if( $logEntry ); $logEntry .= '(not exist)'; $status = '404 Not Found'; } if( $raw ) { $indexableView = 0; $logEntry .= ' ' if( $logEntry ); $logEntry .= 'raw='.$raw; if( $raw eq 'debug' || $raw eq 'all' ) { $text = $store->getDebugText( $meta, $text ); } } if( $query->param( 'extralog' ) ) { my $extraLog = $query->param( 'extralog' ); $extraLog =~ s/`\|\x00-\x1f]//go; # Sanitize parameter value $logEntry .= ' ' if( $logEntry ); $logEntry .= $extraLog; } if( $TWiki::cfg{Log}{view} ) { $session->writeLog( 'view', $webName.'.'.$topicName, $logEntry ); } # Note; must enter all contexts before the template is read, as # TMPL:P is expanded on the fly in the template reader. :-( my( $revTitle, $revArg ) = ( '', '' ); my $mode = $session->{contentMode}; if( $mode eq 'read-only' ) { $session->enterContext( 'inactive' ); } elsif( $rev < $showRev ) { $session->enterContext( 'inactive' ); # disable edit of previous revisions $revTitle = '(r'.$rev.')'; $revArg = '&rev='.$rev; } if( $mode eq 'slave' ) { $session->enterContext( 'content_slave' ); } elsif( $mode eq 'master' ) { $session->enterContext( 'content_master' ); } my $template = $query->param( 'template' ) || $session->{prefs}->getPreferencesValue( 'VIEW_TEMPLATE' ) || 'view'; # Always use default view template for raw=debug, raw=all and raw=on if( $raw =~ /^(debug|all|on)$/ ) { $template = 'view'; } elsif ( $template =~ /%[A-Z]/ ) { $template = $session->handleCommonTags($template, $webName, $topicName, $meta); $template =~ s/^\s+//; $template =~ s/\s+$//; } my $tmpl = $session->templates->readTemplate( $template, $skin ); if( !$tmpl && $template ne 'view' ) { $tmpl = $session->templates->readTemplate( 'view', $skin ); } if( !$tmpl ) { throw TWiki::OopsException( 'attention', def => 'no_such_template', web => $webName, topic => $topicName, params => [ $template, 'VIEW_TEMPLATE' ] ); } $tmpl =~ s/%REVTITLE%/$revTitle/g; $tmpl =~ s/%REVARG%/$revArg/g; if( $indexableView && $TWiki::cfg{AntiSpam}{RobotsAreWelcome} && !$query->param() ) { # it's an indexable view type, there are no parameters # on the url, and robots are welcome. Remove the NOINDEX meta tag $tmpl =~ s/]*>//goi; } # Show revisions around the one being displayed # we start at $showRev then possibly jump near $rev if too distant my $revsToShow = $TWiki::cfg{NumberOfRevisions} + 1; $revsToShow = $showRev if $showRev < $revsToShow; my $doingRev = $showRev; my $revs = ''; while( $revsToShow > 0 ) { $revsToShow--; if( $doingRev == $rev) { $revs .= 'r'.$rev; } else { $revs .= CGI::a({ href=>$session->getScriptUrl( 0, 'view', $webName, $topicName, rev => $doingRev ), rel => 'nofollow' }, "r$doingRev" ); } if ( $doingRev - $rev >= $TWiki::cfg{NumberOfRevisions} ) { # we started too far away, need to jump closer to $rev use integer; $doingRev = $rev + $revsToShow / 2; $doingRev = $revsToShow if $revsToShow > $doingRev; $revs .= ' | '; next; } if( $revsToShow ) { $revs .= ' ' . CGI::a ( { href=>$session->getScriptUrl( 0, 'rdiff', $webName, $topicName, rev1 => $doingRev, rev2 => $doingRev-1 ), rel => 'nofollow' }, '<' ) . ' '; } $doingRev--; } $tmpl =~ s/%REVISIONS%/$revs/go; ## SMELL: This is also used in TWiki::_TOC. Could insert a tag in ## TOC and remove all those here, finding the parameters only once my @qparams = (); foreach my $name ( $query->param ) { next if ($name eq 'keywords'); next if ($name eq 'topic'); push @qparams, $name => $query->param($name); } if ( $tmpl =~ /%QUERYPARAMSTRING%/ ) { my $qs = TWiki::_make_params( 1, @qparams ); # Item7595: Sanitize QUERYPARAMSTRING to counter XSS exploits $qs =~ s/(['\/<>])/'%'.sprintf('%02x', ord($1))/ge; $tmpl =~ s/%QUERYPARAMSTRING%/$qs/go; } # extract header and footer from the template, if there is a # %TEXT% tag marking the split point. The topic text is inserted # in place of the %TEXT% tag. The text before this tag is inserted # as header, the text after is inserted as footer. If there is a # %STARTTEXT% tag present, the header text between %STARTTEXT% and # %TEXT is rendered together, as is the footer text between %TEXT% # and %ENDTEXT%, if present. This allows correct handling of TWiki # markup in header or footer if those do require examination of the # topic text to work correctly (e.g., %TOC%). # Note: This feature is experimental and may be replaced by an # alternative solution not requiring additional tags. my( $start, $end ); if( $tmpl =~ m/^(.*)%TEXT%(.*)$/s ) { my @starts = split( /%STARTTEXT%/, $1 ); if ( $#starts > 0 ) { # we know that there is something before %STARTTEXT% $start = $starts[0]; $text = $starts[1] . $text; } else { $start = $1; } my @ends = split( /%ENDTEXT%/, $2 ); if ( $#ends > 0 ) { # we know that there is something after %ENDTEXT% $text .= $ends[0]; $end = $ends[1]; } else { $end = $2; } } else { my @starts = split( /%STARTTEXT%/, $tmpl ); if ( $#starts > 0 ) { # we know that there is something before %STARTTEXT% $start = $starts[0]; $text = $starts[1]; } else { $start = $tmpl; $text = ''; } $end = ''; } # If minimalist is set, images and anchors will be stripped from text my $minimalist = 0; if( $contentType ) { $minimalist = ( $skin =~ /\brss/ ); } elsif( $skin =~ /\brss/ ) { $contentType = 'text/xml'; $minimalist = 1; } elsif( $skin =~ /\bxml/ ) { $contentType = 'text/xml'; $minimalist = 1; } elsif( $raw eq 'text' || $raw eq 'all' || $raw eq 'expandvariables' ) { $contentType = 'text/plain'; } else { $contentType = 'text/html' } $session->{SESSION_TAGS}{MAXREV} = $showRev; $session->{SESSION_TAGS}{CURRREV} = $rev; # Set page generation mode to RSS if using an RSS skin $session->enterContext( 'rss' ) if $skin =~ /\brss/; # Set the meta-object that contains the rendering info # SMELL: hack to get around not having a proper topic object model $session->enterContext( 'can_render_meta', $meta ); my $page; # Legacy: If the _only_ skin is 'text' it is used like this: # http://.../view/Codev/MyTopic?skin=text&contenttype=text/plain&raw=on # which shows the topic as plain text; useful for those who want # to download plain text for the topic. So when the skin is 'text' # we do _not_ want to create a textarea. # raw=on&skin=text is deprecated; use raw=text instead. if ( $raw eq 'expandvariables' ) { $page = $session->handleCommonTags( $text, $webName, $topicName, $meta ); } elsif( $raw eq 'text' || $raw eq 'all' || ( $raw && $skin eq 'text' )) { # use raw text $page = $text; } else { my @args = ( $session, $webName, $topicName, $meta, $minimalist ); $session->enterContext( 'header_text' ); $page = _prepare($start, @args); $session->leaveContext( 'header_text' ); if( $raw ) { if ($text) { my $p = $session->{prefs}; CGI::charset($TWiki::cfg{Site}{CharSet}) if ( $TWiki::cfg{Site}{CharSet} ); $page .= CGI::textarea( -readonly => 'readonly', -rows => $p->getPreferencesValue('EDITBOXHEIGHT'), -cols => $p->getPreferencesValue('EDITBOXWIDTH'), -style => $p->getPreferencesValue('EDITBOXSTYLE'), -class => 'twikiTextarea twikiTextareaRawView', -id => 'topic', -default => $text ); } } else { $session->enterContext( 'body_text' ); $page .= _prepare($text, @args); $session->leaveContext( 'body_text' ); } $session->enterContext( 'footer_text' ); $page .= _prepare($end, @args); $session->leaveContext( 'footer_text' ); } # Output has to be done in one go, because if we generate the header and # then redirect because of some later constraint, some browsers fall over $session->writeCompletePage( $page, 'view', $contentType, $status ); } sub _prepare { my( $text, $session, $webName, $topicName, $meta, $minimalist) = @_; $text = $session->handleCommonTags( $text, $webName, $topicName, $meta ); $text = $session->renderer->getRenderedVersion( $text, $webName, $topicName ); $text =~ s/( ?) *<\/?(nop|noautolink)\/?>\n?/$1/gois; if( $minimalist ) { $text =~ s/]*>//gi; # remove image tags $text =~ s/]*>//gi; # remove anchor tags $text =~ s/<\/a>//gi; # remove anchor tags } return $text; } =pod ---++ StaticMethod viewfile( $session, $web, $topic, $query ) =viewfile= command handler. This method is designed to be invoked via the =UI::run= method. Command handler for viewfile. View a file in the browser. Some parameters are passed in CGI query: | =filename= | Attachment to view | | =rev= | Revision to view | =cut sub viewfile { my $session = shift; my $query = $session->{request}; my $topic = $session->{topicName}; my $webName = $session->{webName}; my @path = split( '/', $query->path_info() ); shift( @path )unless $path[0]; my $fileName; my $retrofit = 0; if( defined( $query->param( 'filename' ))) { $fileName = $query->param( 'filename' ); } else { # Ok file name is not comming from explicit query parameter # Let us cook from @path $fileName = pop( @path ); #Let us redefine web/topic - fix of Item5967 my $debug = $query->param( 'debug' ); $session->writeDebug("viewfile() 1 web = $webName, topic = $topic") if ( $debug ); if( $fileName =~ /\.([^.]+)$/ ) { #file has extension # recalculate web and topic from pathinfo minus filename ($webName, $topic) = $session->TWiki::determineWebTopic('/' . join('/', @path)); $session->writeDebug("viewfile() 2 web = $webName, topic = $topic") if ( $debug ); # You may think this should be only a matter of calculating # new $webName and $topic values from $webName. # In most cases, that's true but there are edges cases. # # For example, if /cgi-bin/viewfile/Web/Topic/File[1].png is # accessed, then $webName has Web/Topic/File1 while $fileName has # "File[1].png". If you manipulate $webName based on # length($fileName), you do it wrong. # Then think about: /cgi-bin/viewfile/Web/Topic/First.Last.png # $webName gets Web/Topic/First/Last # So $webName =~ s:/[^/]+$:: doesn't work either. } else { #file does not have extension #no need to trim webName if( $webName =~ /\/([^\/]+)$/ ) { $topic = $1; #topic name redefined here $webName = substr($webName, 0, -length($topic)-1); } } $retrofit = 1; } if (!$fileName) { throw TWiki::OopsException( 'attention', def => 'no_such_attachment', web => 'Unknown', topic => 'Unknown', params => [ 'viewfile', '?' ] ); } $fileName = TWiki::Sandbox::sanitizeAttachmentName( $fileName ); $session->{plugins}->dispatch('viewFileRedirectHandler', $session, $webName, $topic, $fileName) and return; my $rev = $session->{store}->cleanUpRevID( $query->param( 'rev' ) ); unless( $fileName && $session->{store}->attachmentExists( $webName, $topic, $fileName )) { throw TWiki::OopsException( 'attention', def => 'no_such_attachment', web => $webName, topic => $topic, params => [ 'viewfile', $fileName||'?' ] ); } my $logEntry = $fileName; if( $rev ) { $logEntry .= ", r$rev"; } if( $TWiki::cfg{Log}{viewfile} ) { $session->writeLog( 'viewfile', $webName.'.'.$topic, $logEntry ); } # Retrofit $session->{webName} and $session->{topicName} when filename # parameter is not supplied. This is a bit ugly though. if ( $retrofit ) { $session->{webName} = $webName; $session->{topicName} = $topic; } my $fileContent; if ( $rev ) { $fileContent = $session->{store}->readAttachment( $session->{user}, $webName, $topic, $fileName, $rev ); } else { # get a file handle instead of the file content to handle a large # attachment efficiently $fileContent = $session->{store}->getAttachmentStream( $session->{user}, $webName, $topic, $fileName ); } my $type = TWiki::suffixToMimeType( $fileName ); $fileName = qq("$fileName") if ( $fileName !~ /^[-.\w]+$/ ); # Item7263 my $dispo = 'inline;filename='.$fileName; $session->{response}->header(-type => $type, qq(Content-Disposition="$dispo") ); $session->{response}->body($fileContent); } # _suffixToMimeType($session, $filename) has been moved to # TWiki::suffixToMimeType($filename) 1;