diff --git a/core/lib/Foswiki/Access.pm b/core/lib/Foswiki/Access.pm index 7a31eb0af3..5058e08bcd 100644 --- a/core/lib/Foswiki/Access.pm +++ b/core/lib/Foswiki/Access.pm @@ -52,6 +52,7 @@ sub create { print STDERR "using $imp Access Control\n" if MONITOR; + # SMELL Foswiki::local_class() would be preferable instead of eval. my $ok = eval("require $imp; 1;"); ASSERT( $ok, $@ ) if DEBUG; my $this = $imp->new( session => $session, _indirect => 1, ); diff --git a/core/lib/Foswiki/App.pm b/core/lib/Foswiki/App.pm index 4789bd28a3..3ce904f9a6 100644 --- a/core/lib/Foswiki/App.pm +++ b/core/lib/Foswiki/App.pm @@ -3,9 +3,11 @@ package Foswiki::App; use v5.14; +use constant TRACE_REQUEST => 0; + use Cwd; use Try::Tiny; -use Foswiki::Config; +use Foswiki::Config (); use Moo; use namespace::clean; @@ -44,6 +46,14 @@ has request => ( isa => Foswiki::Object::isaCLASS( 'request', 'Foswiki::Request', noUndef => 1, ), ); +has response => ( + is => 'rw', + lazy => 1, + default => sub { new Foswiki::Response }, + isa => Foswiki::Object::isaCLASS( + 'response', 'Foswiki::Response', noUndef => 1, + ), +); has macros => ( is => 'rw', lazy => 1, @@ -64,6 +74,14 @@ has ui => ( return $_[0]->create('Foswiki::UI'); }, ); +has _dispatcherObject => ( + is => 'rw', + isa => Foswiki::Object::isaCLASS( + '_dispatcherObject', 'Foswiki::Object', noUndef => 1 + ), +); +has _dispatcherMethod => ( is => 'rw', ); +has _dispatcherContext => ( is => 'rw', ); # App-local $Foswiki::system_message. has system_message => ( is => 'rw', ); @@ -93,6 +111,31 @@ sub BUILD { $Foswiki::app = $this; + my $cfg = $this->cfg; + if ( $cfg->data->{Store}{overrideUmask} && $cfg->data->{OS} ne 'WINDOWS' ) { + +# Note: The addition of zero is required to force dirPermission and filePermission +# to be numeric. Without the additition, certain values of the permissions cause +# runtime errors about illegal characters in subtraction. "and" with 777 to prevent +# sticky-bits from breaking the umask. + my $oldUmask = umask( + ( + oct(777) - ( + ( + $cfg->data->{Store}{dirPermission} + 0 | + $cfg->data->{Store}{filePermission} + 0 + ) + ) & oct(777) + ) + ); + +#my $umask = sprintf('%04o', umask() ); +#$oldUmask = sprintf('%04o', $oldUmask ); +#my $dirPerm = sprintf('%04o', $Foswiki::cfg{Store}{dirPermission}+0 ); +#my $filePerm = sprintf('%04o', $Foswiki::cfg{Store}{filePermission}+0 ); +#print STDERR " ENGINE changes $oldUmask to $umask from $dirPerm and $filePerm \n"; + } + unless ( defined $this->engine ) { Foswiki::Exception::Fatal->throw( text => "Cannot initialize engine" ); } @@ -159,8 +202,100 @@ sub run { sub handleRequest { my $this = shift; - my $res = Foswiki::UI::handleRequest( $this->request ); - $this->engine->finalize( $res, $this->request ); + my $req = $this->request; + + try { + $this->_prepareDispatcher; + $this->_checkTickle; + + # Get the params cache from the path + my $cache = $req->param('foswiki_redirect_cache'); + if ( defined $cache ) { + $req->delete('foswiki_redirect_cache'); + } + + # If the path specifies a cache path, use that. It's arbitrary + # as to which takes precedence (param or path) because we should + # never have both at once. + my $path_info = $req->pathInfo; + if ( $path_info =~ s#/foswiki_redirect_cache/([a-f0-9]{32})## ) { + $cache = $1; + $req->pathInfo($path_info); + } + + if ( defined $cache && $cache =~ m/^([a-f0-9]{32})$/ ) { + + # implicit untaint required, because $cache may be used in a filename. + # Note that the cache serialises the method and path_info, which + # will be restored. + Foswiki::Request::Cache->new->load( $1, $req ); + } + + if (TRACE_REQUEST) { + print STDERR "INCOMING " + . $req->method() . " " + . $req->url . " -> " + . $sub . "\n"; + print STDERR "validation_key: " + . ( $req->param('validation_key') || 'no key' ) . "\n"; + + #require Data::Dumper; + #print STDERR Data::Dumper->Dump([$req]); + } + + # XXX TODO vrurg – Continue from here... + if ( UNIVERSAL::isa( $Foswiki::engine, 'Foswiki::Engine::CLI' ) ) { + $this->_dispatcherContext->{command_line} = 1; + } + elsif ( + defined $req->method + && ( + ( + defined $dispatcher->{allow} + && !$dispatcher->{allow}->{ uc( $req->method() ) } + ) + || ( defined $dispatcher->{deny} + && $dispatcher->{deny}->{ uc( $req->method() ) } ) + ) + ) + { + $res = new Foswiki::Response(); + $res->header( -type => 'text/html', -status => '405' ); + $res->print( '

Bad Request:

The request method: ' + . uc( $req->method() ) + . ' is denied for the ' + . $req->action() + . ' action.' ); + if ( uc( $req->method() ) eq 'GET' ) { + $res->print( '

' + . 'The ' + . $req->action() + . ' script can only be called with the POST type method' + . '

' + . 'For example:
' + . '   <form method="post" action="%SCRIPTURL{' + . $req->action() + . '}%/%WEB%/%TOPIC%">
' + . '

See System.CommandAndCGIScripts for more information.' + ); + } + return $res; + } + $res = $this->_execute( \&$sub, %{ $dispatcher->{context} } ); + return $res; + + #my $res = Foswiki::UI::handleRequest( $this->request ); + } + catch { + my $e = $_; + } + finally { + # Whatever happens at this stage we shall be able to reply with a valid + # HTTP response using valid HTML. + $this->engine->finalize( $res, $this->request ); + }; } =begin TML @@ -176,6 +311,8 @@ sub create { my $this = shift; my $class = shift; + #Foswiki::load_class($class); + unless ( $class->isa('Foswiki::AppObject') ) { Foswiki::Exception::Fatal->throw( text => "Class $class is not a Foswiki::AppObject descendant." ); @@ -255,9 +392,16 @@ sub _prepareEngine { return $engine; } +# The request attribute default method. sub _prepareRequest { my $this = shift; my $request = $this->engine->prepare; + + # The following is preferable form of Request creation. The request + # constructor will then initialize itself using $app->engine as the source + # of information about the environment we're running under. + + # my $request = Foswiki::Request->prepare(app => $this); return $request; } @@ -267,6 +411,68 @@ sub _readConfig { return $cfg; } +# Determines what dispatcher to use for the action requested. +sub _prepareDispatcher { + my $this = shift; + my $req = $this->request; + + my $dispatcher = $app->cfg->data->{SwitchBoard}{ $req->action }; + unless ( defined $dispatcher ) { + $res = $this->response; + $res->header( -type => 'text/html', -status => '404' ); + my $html = CGI::start_html('404 Not Found'); + $html .= CGI::h1( {}, 'Not Found' ); + $html .= CGI::p( {}, + "The requested URL " + . $req->uri + . " was not found on this server." ); + $html .= CGI::end_html(); + $res->print($html); + Foswiki::Exception::HTTPResponse->throw( status => 404, ); + } + + # SMELL Shouldn't it be deprecated? + if ( ref($dispatcher) eq 'ARRAY' ) { + + # Old-style array entry in switchboard from a plugin + my @array = @$dispatcher; + $dispatcher = { + package => $array[0], + function => $array[1], + context => $array[2], + }; + } + + $dispatcher->{package} //= 'Foswiki::UI'; + $this->_dispatcherObject( $this->create( $dispatcher->{package} ) ); + $this->_dispatcherMethod( $dispatcher->{method} + || $dispatcher->{function} ); + $this->_dispatcherContext( $dispatcher->{context} ); +} + +# If the X-Foswiki-Tickle header is present, this request is an attempt to +# verify that the requested function is available on this Foswiki. Respond with +# the serialised dispatcher, and finish the request. Need to stringify since +# VERSION is a version object. +sub _checkTickle { + my $this = shift; + my $req = $this->request; + + if ( $req->header('X-Foswiki-Tickle') ) { + my $res = $this->response; + my $data = { + SCRIPT_NAME => $ENV{SCRIPT_NAME}, + VERSION => $Foswiki::VERSION->stringify(), + RELEASE => $Foswiki::RELEASE, + }; + $res->header( -type => 'application/json', -status => '200' ); + + my $d = JSON->new->allow_nonref->encode($data); + $res->print($d); + Foswiki::Exception::HTTPResponse->throw; + } +} + 1; __END__ Foswiki - The Free and Open Source Wiki, http://foswiki.org/ diff --git a/core/lib/Foswiki/Engine.pm b/core/lib/Foswiki/Engine.pm index 6b5f6c22f4..219043e4ff 100644 --- a/core/lib/Foswiki/Engine.pm +++ b/core/lib/Foswiki/Engine.pm @@ -24,6 +24,16 @@ use Moo; use namespace::clean; extends qw(Foswiki::AppObject); +has env => ( + is => 'rw', + isa => Foswiki::Object::isaHASH( 'env', noUndef => 1, ), + default => sub { $_[0]->app->env }, +); + +# pathData attribute is a hash with the following keys: action, path_info, uri +# uri key can be undef under certain circumstances. +has pathData => ( is => 'rw', lazy => 1, default => \&_preparePath, ); + BEGIN { if ( $Foswiki::cfg{UseLocale} ) { require locale; @@ -52,7 +62,7 @@ sub start { } elsif ( $params{env}{'psgi.version'} ) { - # We don't have PSGI support yet. + # SMELL TODO We don't have PSGI support yet. $engine = 'Foswiki::Engine::PSGI'; } else { @@ -77,24 +87,24 @@ Constructs an engine object. =begin TML ----++ ObjectMethod run() +---++ Obsolete ObjectMethod run() Start point to Runtime Engines. =cut -sub run { - my $this = shift; - my $req = $this->prepare(); - if ( ref($req) ) { - my $res = Foswiki::UI::handleRequest($req); - $this->finalize( $res, $req ); - } -} +#sub run { +# my $this = shift; +# my $req = $this->prepare(); +# if ( ref($req) ) { +# my $res = Foswiki::UI::handleRequest($req); +# $this->finalize( $res, $req ); +# } +#} =begin TML ----++ ObjectMethod prepare() -> $req +---++ Obsolete ObjectMethod prepare() -> $req Initialize a Foswiki::Request object by calling many preparation methods and returns it, or a status code in case of error. @@ -142,12 +152,16 @@ sub prepare { $this->prepareUploads($req); } catch { + # SMELL returns within Try::Tiny try/catch block doesn't return from the + # calling sub but from the try/catch block itself. my $e = $_; unless ( ref($e) ) { Foswiki::Exception::Fatal->rethrow($e); } - if ( $e->isa('Foswiki::EngineException') ) { + if ( $e->isa('Foswiki::EngineException') + || $e->isa('Foswiki::Exception::Engine') ) + { my $res = $e->response; unless ( defined $res ) { $res = Foswiki::Response->new; @@ -260,16 +274,16 @@ sub prepareHeaders { } =begin TML ----++ ObjectMethod preparePath( $req ) +---++ Private ObjectMethod _preparePath( ) Abstract method, must be defined by inherited classes. - * =$req= - Foswiki::Request object to populate -Should fill $req's uri and pathInfo fields. +Should return a hashref used to initialize the pathData attribute. In other +words, the hashref must containt keys valid for the attribute (see its comment). =cut -sub preparePath { } +sub _preparePath { } =begin TML @@ -376,7 +390,7 @@ sub finalizeUploads { } ---++ ObjectMethod finalizeError( $res, $req ) -Called if some engine especific error happens. +Called if some engine specific error happens. * =$res= - Foswiki::Response object to get data from * =$req= - Foswiki::Request object to get data from diff --git a/core/lib/Foswiki/Engine/CGI.pm b/core/lib/Foswiki/Engine/CGI.pm index 95f3474094..da41d92fbe 100644 --- a/core/lib/Foswiki/Engine/CGI.pm +++ b/core/lib/Foswiki/Engine/CGI.pm @@ -143,40 +143,40 @@ around prepareConnection => sub { my $orig = shift; my ( $this, $req ) = @_; - $req->remoteAddress( $ENV{REMOTE_ADDR} ); - $req->method( $ENV{REQUEST_METHOD} ); + $req->remoteAddress( $this->env->{REMOTE_ADDR} ); + $req->method( $this->env->{REQUEST_METHOD} ); - if ( $ENV{HTTPS} && uc( $ENV{HTTPS} ) eq 'ON' ) { + if ( $this->env->{HTTPS} && uc( $this->env->{HTTPS} ) eq 'ON' ) { $req->secure(1); } - if ( $ENV{SERVER_PORT} && $ENV{SERVER_PORT} == 443 ) { + if ( $this->env->{SERVER_PORT} && $this->env->{SERVER_PORT} == 443 ) { $req->secure(1); } - $req->serverPort( $ENV{SERVER_PORT} ); + $req->serverPort( $this->env->{SERVER_PORT} ); }; around prepareQueryParameters => sub { my $orig = shift; my ( $this, $req ) = @_; - $orig->( $this, $req, $ENV{QUERY_STRING} ) - if $ENV{QUERY_STRING}; + $orig->( $this, $req, $this->env->{QUERY_STRING} ) + if $this->env->{QUERY_STRING}; }; around prepareHeaders => sub { my $orig = shift; my ( $this, $req ) = @_; - foreach my $header ( keys %ENV ) { + foreach my $header ( keys %{ $this->env } ) { next unless $header =~ m/^(?:HTTP|CONTENT|COOKIE)/i; ( my $field = $header ) =~ s/^HTTPS?_//; - $req->header( $field => $ENV{$header} ); + $req->header( $field => $this->env->{$header} ); } - $req->remoteUser( $ENV{REMOTE_USER} ); + $req->remoteUser( $this->env->{REMOTE_USER} ); }; -around preparePath => sub { +around _preparePath => sub { my $orig = shift; - my ( $this, $req ) = @_; + my ($this) = @_; # SMELL: "The Microsoft Internet Information Server is broken with # respect to additional path information. If you use the Perl DLL @@ -189,13 +189,13 @@ around preparePath => sub { # Clean up PATH_INFO problems, e.g. Support.CobaltRaqInstall. A valid # PATH_INFO is '/Main/WebHome', i.e. the text after the script name; # invalid PATH_INFO is often a full path starting with '/cgi-bin/...'. - my $pathInfo = $ENV{PATH_INFO} || ''; + my $pathInfo = $this->env->{PATH_INFO} || ''; if ( $pathInfo =~ m/['"]/g ) { $pathInfo = substr( $pathInfo, 0, ( ( pos $pathInfo ) - 1 ) ); } - unless ( defined $ENV{SCRIPT_NAME} ) { + unless ( defined $this->env->{SCRIPT_NAME} ) { # CGI/1.1 (rfc3875) states that the server MUST set # SCRIPT_NAME, so if it doens't we have a broken server @@ -213,16 +213,16 @@ around preparePath => sub { response => $res ); } - my $cgiScriptPath = $ENV{SCRIPT_NAME}; + my $cgiScriptPath = $this->env->{SCRIPT_NAME}; $pathInfo =~ s{^$cgiScriptPath(?:/+|$)}{/}; my $cgiScriptName = $cgiScriptPath; $cgiScriptName =~ s/.*?(\w+)(\.\w+)?$/$1/; my $action; - if ( exists $ENV{FOSWIKI_ACTION} ) { + if ( exists $this->env->{FOSWIKI_ACTION} ) { # This handles scripts that have set $FOSWIKI_ACTION - $action = $ENV{FOSWIKI_ACTION}; + $action = $this->env->{FOSWIKI_ACTION}; } elsif ( exists $Foswiki::cfg{SwitchBoard}{$cgiScriptName} ) { @@ -245,17 +245,18 @@ around preparePath => sub { } $action ||= 'view'; ASSERT( defined $pathInfo ) if DEBUG; - $req->action($action); - $req->pathInfo($pathInfo); - $req->uri( $ENV{REQUEST_URI} - || $req->url( -absolute => 1, -path => 1, -query => 1 ) ); + return { + action => $action, + path_info => $pathInfo, + uri => $this->env->{REQUEST_URI} // undef, + }; }; around prepareBody => sub { my $orig = shift; my ( $this, $req ) = @_; - return unless $ENV{CONTENT_LENGTH}; + return unless $this->env->{CONTENT_LENGTH}; # Record the master process so we don't reap temp files in # sub-processes (see long comment ****) @@ -277,7 +278,7 @@ around prepareBodyParameters => sub { my $orig = shift; my ( $this, $req ) = @_; - return unless $ENV{CONTENT_LENGTH}; + return unless $this->env->{CONTENT_LENGTH}; my @plist = $this->cgi->multi_param(); foreach my $pname (@plist) { my $upname = NFC( Foswiki::decode_utf8($pname) ); @@ -302,7 +303,7 @@ around prepareUploads => sub { my $orig = shift; my ( $this, $req ) = @_; - return unless $ENV{CONTENT_LENGTH}; + return unless $this->env->{CONTENT_LENGTH}; my %uploads; foreach my $key ( keys %{ $this->uploads } ) { my $fname = $this->cgi->param($key); diff --git a/core/lib/Foswiki/Exception.pm b/core/lib/Foswiki/Exception.pm index 6f059acda0..86b57e746d 100644 --- a/core/lib/Foswiki/Exception.pm +++ b/core/lib/Foswiki/Exception.pm @@ -265,33 +265,67 @@ sub BUILD { my $this = shift; } -package Foswiki::Exception::Engine; +=begin TML + +---++ Exception Foswiki::Exception::HTTPResponse + +Used to send HTTP status responses to the user. + +Attributes: + + * =status= - HTTP status code, integer; response status code used if omitted. + * =response= - a Foswiki::Response object. If not supplied then the default from $Foswiki::app->response is used. + * =text= – read-only, generated using the exception attributes. + +=cut + +package Foswiki::Exception::HTTPResponse; use Moo; use namespace::clean; extends qw(Foswiki::Exception); our @_newParameters = qw(status reason response); -has status => ( is => 'ro', required => 1, ); -has reason => ( is => 'ro', required => 1, ); -has response => ( is => 'ro', required => 1, ); +has status => + ( is => 'ro', lazy => 1, default => sub { $_[0]->response->status, }, ); +has response => + ( is => 'ro', lazy => 1, default => sub { $Foswiki::app->response }, ); +has '+text' => ( + is => 'ro', + lazy => 1, + default => sub { + return 'EngineException: Status code "' . $this->status; + }, +); =begin TML +---++ Exception Foswiki::Exception::Engine ----++ ObjectMethod stringify() -> $string +Descendant of =Foswiki::Exception::HTTPResponse=. -Generate a summary string. This is mainly for debugging. +Attributes: -=cut + * =reason= - reason text, required + * =text= – read-only, generated using the exception attributes. -sub BUILD { - my $this = shift; +=cut - $this->text( 'EngineException: Status code "' +package Foswiki::Exception::Engine; +use Moo; +extends qw(Foswiki::Exception::HTTPResponse); + +has reason => ( is => 'ro', required => 1, ); +has '+text' => ( + is => 'ro', + lazy => 1, + default => sub { + return + 'EngineException: Status code "' . $this->status . ' defined because of "' - . $this->reason ); -} + . $this->reason; + }, +); 1; __END__ diff --git a/core/lib/Foswiki/Macros.pm b/core/lib/Foswiki/Macros.pm index 6d23a9f437..b28ab505d5 100644 --- a/core/lib/Foswiki/Macros.pm +++ b/core/lib/Foswiki/Macros.pm @@ -208,7 +208,7 @@ sub expandMacrosOnTopicCreation { # SMELL Does it really needed in the App model? #local $Foswiki::Plugins::SESSION = $this; - local $Foswiki::app = $app; + local $Foswiki::app = $this->app; my $text = $topicObject->text(); if ($text) { diff --git a/core/lib/Foswiki/Request.pm b/core/lib/Foswiki/Request.pm index 3c0b3c5e18..fc7758bdea 100644 --- a/core/lib/Foswiki/Request.pm +++ b/core/lib/Foswiki/Request.pm @@ -23,6 +23,10 @@ Fields: files * =uri= the request uri +The following fields are parsed from the path_info + * =web= the requested web. Access using web method + * =topic= the requested topic. Access using topic + =cut package Foswiki::Request; @@ -50,13 +54,13 @@ BEGIN { has action => ( is => 'rw', lazy => 1, - default => '', + default => sub { $_[0]->app->engine->pathData->{action} }, trigger => sub { my ( $this, $action ) = @_; # This will set base_action only the first time action would been set. $this->base_action; - $ENV{FOSWIKI_ACTION} = $action; + $this->app->env->{FOSWIKI_ACTION} = $action; }, ); has base_action => ( @@ -68,12 +72,38 @@ has base_action => ( # time. See action attribute trigger. default => sub { return $_[0]->action; }, ); -has path_info => ( is => 'rw', lazy => 1, default => '', ); + +=begin TML + +---++ ObjectAttribute pathInfo + +Request path info. + +Note that the attribute contains a *URL encoded byte string* +i.e. it will only contain characters -A-Za-z0-9_.~!*\'();:@&=+$,/?%#[] +If you intend to analyse it, you will probably have to +Foswiki::urlDecode it first. + +=cut + +has pathInfo => ( + is => 'rw', + lazy => 1, + default => sub { $_[0]->app->engine->pathData->{path_info} }, +); has remote_address => ( is => 'rw', lazy => 1, default => '', ); -has uri => ( is => 'rw', lazy => 1, default => '', ); -has cookies => ( is => 'rw', lazy => 1, default => sub { {} }, ); -has headers => ( is => 'rw', lazy => 1, default => sub { {} }, ); -has _param => ( is => 'rw', lazy => 1, default => sub { {} }, ); +has uri => ( + is => 'rw', + lazy => 1, + default => sub { + my $this = shift; + $this->app->engine->pathData->{uri} + // $this->url( -absolute => 1, -path => 1, -query => 1 ); + }, +); +has cookies => ( is => 'rw', lazy => 1, default => sub { {} }, ); +has headers => ( is => 'rw', lazy => 1, default => sub { {} }, ); +has _param => ( is => 'rw', lazy => 1, default => sub { {} }, ); has uploads => ( is => 'rw', lazy => 1, @@ -90,7 +120,27 @@ has start_time => ( # start_time cannot be lazy, can it? is => 'rw', default => sub { return [Time::HiRes::gettimeofday] }, ); +has web => + ( is => 'rw', lazy => 1, default => sub { $_[0]->_pathParsed->{web} }, ); +has topic => + ( is => 'rw', lazy => 1, default => sub { $_[0]->_pathParsed->{topic} }, ); +has invalidWeb => ( + is => 'rw', + lazy => 1, + default => sub { $_[0]->_pathParsed->{invalidWeb} }, +); +has invalidTopic => ( + is => 'rw', + lazy => 1, + default => sub { $_[0]->_pathParsed->{invalidTopic} }, +); has _initializer => ( is => 'ro', init_arg => "initializer", ); +has _pathParsed => ( + is => 'rw', + lazy => 1, + isa => Foswiki::Object::isaHASH( '_pathParsed', noUndef => 1 ), + default => \&_establishWebTopic, +); # Aliases are to be declared after all attribute handling methods are been # created but before CGI methods gets imported via cgiRequest attribute @@ -184,25 +234,6 @@ Sets/Gets request method (GET, HEAD, POST). =begin TML ----++ ObjectMethod pathInfo( [ $path ] ) -> $path - -Sets/Gets request path info. - -Called without parameters returns current pathInfo. - -This is an alias to =path_info()= ObjectAttribute. - -Note that the string returned is a *URL encoded byte string* -i.e. it will only contain characters -A-Za-z0-9_.~!*\'();:@&=+$,/?%#[] -If you intend to analyse it, you will probably have to -Foswiki::urlDecode it first. - -=cut - -*pathInfo = \&path_info; - -=begin TML - ---++ ObjectMethod protocol() -> $protocol Returns 'https' if secure connection. 'http' otherwise. @@ -535,7 +566,8 @@ sub delete { foreach my $p (@_) { next unless exists $this->_param->{$p}; if ( my $upload = $this->uploads->{ $this->param($p) } ) { - $upload->finish; + + #$upload->finish; CORE::delete $this->uploads->{ $this->param($p) }; } CORE::delete $this->_param->{$p}; @@ -790,12 +822,219 @@ Convenience method to get Referer uri. sub referer { shift->header('Referer') } +=begin TML + +---++ StaticMethod parse([query path]) -> { web => $web, topic => $topic, invalidWeb => optional, invalidTopic => optional } + +Parses the rquests query_path and returns a hash of web and topic names. +If passed a query string, it will parse it and return the extracted +web / topic. + +*This method cannot set the web and topic parsed from the query path.* + +Slash (/) can separate webs, subwebs and topics. +Dot (.) can *only* separate a web path from a topic. +Trailing slash disambiguates a topic from a subweb when both exist with same name. + +If any illegal characters are present, then web and/or topic are undefined. The original bad +components are returned in the invalidWeb or invalidTopic entries. + +webExists and topicExists may be called to disambiguate between subwebs and topics +however the returned web and topic names do not necessarily exist. + +This routine returns two variables when encountering invalid input: + * {invalidWeb} contains original invalid web / pathinfo content when validation fails. + * {invalidTopic} Same function but for topic name + +Ths following paths are supported: + * Main Extracts webname, topic is undef + * Main/Somename Extracts webname. Somename might be a subweb if it exixsts, or a topic. + * Main.Somename Extracts webname and topic. + * Main/Somename/ Forces Somename to be a web, if it also exists as a topic + +=cut + +sub parse { + my $query_path = shift; + + my $web_path; + + print STDERR "Processing path ($query_path)\n" if TRACE; + + Foswiki::Exception::Fatal->throw( + text => 'No valid query path string passed over to ' + . __PACKAGE__ + . '::parse() method' ) + unless defined $query_path && length $query_path > 1; + $query_path =~ s{/+}{/}g; # Remove duplicate slashes + $query_path =~ s{^/}{}g; # Remove leading slash + + # trailingSlash Flag - hint that you want the web even if the topic exists + my $trailingSlash = ( $query_path =~ s/\/$// ); + + # Try the simple, split on dot, maybe it will work. + my ( $tweb, $ttopic ) = split( /\./, $query_path ); + if ( defined $ttopic ) { + + my $web = Foswiki::Sandbox::untaint( $tweb, + \&Foswiki::Sandbox::validateWebName ); + + my $topic = Foswiki::Sandbox::untaint( $ttopic, + \&Foswiki::Sandbox::validateTopicName ); + + my $resp = { web => $web, topic => $topic }; + $resp->{invalidWeb} = $tweb unless defined $web; + $resp->{invalidTopic} = $ttopic unless defined $topic; + + print STDERR Data::Dumper::Dumper( \$resp ) if TRACE; + return $resp; + } + + my @parts = split( /\//, $query_path ); # split the path + #print STDERR Data::Dumper::Dumper( \@parts ) if TRACE; + + my $temptopic; + my @webs; + + foreach (@parts) { + print STDERR "Checking $_\n" if TRACE; + + # Lax check on name to eliminate evil characters. + my $p = Foswiki::Sandbox::untaint( $_, + \&Foswiki::Sandbox::validateTopicName ); + unless ($p) { + + # SMELL: It would be better to throw an exception here, but it's too early + # in initialization. throwing an oops exception mostly works but the display + # has unexpanded macros, and broken links, and no skinning. So for now keep the + # old architecture. + my $resp = { + web => undef, + topic => undef, + invalidWeb => $_ + }; + + #print STDERR Data::Dumper::Dumper( \$resp ) if TRACE; + return $resp; + } + + if ( \$_ == \$parts[-1] ) { # This is the last part of path + print STDERR "Testing last part web " + . join( '/', @webs ) + . "topic $p \n" + if TRACE; + + if ( $trailingSlash + && $Foswiki::Plugins::SESSION->webExists( + join( '/', @webs, $p ) ) ) + { + print STDERR "Web Exists, Trailing slash, don't check topic: " + . join( '/', @webs, $p ) . "\n" + if TRACE; + + # It exists in Store as a web + push @webs, $p; + } + elsif ( + $Foswiki::Plugins::SESSION->topicExists( + join( '/', @webs ), $p + ) + ) + { + + print STDERR "Topic Exists" + . join( '/', @webs ) + . "topic $p \n" + if TRACE; + + $temptopic = $p || ''; + } + elsif ( + $Foswiki::Plugins::SESSION->webExists( join( '/', @webs, $p ) ) + ) + { + + print STDERR "Web Exists " . join( '/', @webs, $p ) . "\n" + if TRACE; + + # It exists in Store as a web + push @webs, $p; + } + elsif ($trailingSlash) { + print STDERR "$p: Not a topic, trailingSlash - treat as web\n" + if TRACE; + push @webs, $p; + } + else { + print STDERR " $p: Just a topic. " . scalar @webs . "\n" + if TRACE; + $temptopic = $p; + } + } + else { + $p = Foswiki::Sandbox::untaint( $_, + \&Foswiki::Sandbox::validateWebName ); + unless ($p) { + my $resp = { + web => undef, + topic => undef, + invalidWeb => $_ + }; + return $resp; + } + else { + push @webs, $p; + } + } + } + my $resp = { web => join( '/', @webs ), topic => $temptopic }; + + #print STDERR Data::Dumper::Dumper( \$resp ) if TRACE; + return $resp; +} + +=begin TML + +---++ private ObjectMethod _establishWebTopic() -> \%parsed_path_info + +Used as default for =_pathParsed= attribute which is then used by +=web,topic,invalidWeb,invalidTopic= attribute defaults. + +=cut + +sub _establishWebTopic { + my $this = shift; + + return unless defined $this->_pathParsed; + + # Allow topic= query param to override the path + my $topicParam = $this->param('topic'); + my $pathInfo = Foswiki::urlDecode( $this->pathInfo ); + + my $parse = Foswiki::Request::parse( $topicParam || $pathInfo ); + + # Item3270 - here's the appropriate place to enforce spec + # http://develop.twiki.org/~twiki4/cgi-bin/view/Bugs/Item3270 + $parse->{topic} = ucfirst( $parse->{topic} ) + if ( defined $parse->{topic} ); + + if ( $topicParam && !$parse->{web} ) { + + # Didn't get a web, so try the path + $parse = Foswiki::Request::parse($pathInfo); + } + + # Note that Web can still be undefined. Caller then determines if the + # defaultweb query param, or the HomeWeb config parameter should be used. + return $parse; +} + 1; __END__ Module of Foswiki - The Free and Open Source Wiki, http://foswiki.org/ -Copyright (C) 2008-2009 Foswiki Contributors. All Rights Reserved. +Copyright (C) 2008-2016 Foswiki Contributors. All Rights Reserved. Foswiki Contributors are listed in the AUTHORS file in the root of this distribution. NOTE: Please extend that file, not this notice. diff --git a/core/lib/Foswiki/UI.pm b/core/lib/Foswiki/UI.pm index 5d491adef4..c657597068 100644 --- a/core/lib/Foswiki/UI.pm +++ b/core/lib/Foswiki/UI.pm @@ -377,9 +377,9 @@ sub logon { && $Foswiki::cfg{LoginManager} eq 'none' ) { throw Foswiki::OopsException( - 'attention', - status => 500, - def => 'login_disabled', + template => 'attention', + status => 500, + def => 'login_disabled', ); }