diff --git a/CommentPlugin/test/unit/CommentPlugin/CommentPluginSuite.pm b/CommentPlugin/test/unit/CommentPlugin/CommentPluginSuite.pm index 522c5ffaa3..362ddcd6f3 100755 --- a/CommentPlugin/test/unit/CommentPlugin/CommentPluginSuite.pm +++ b/CommentPlugin/test/unit/CommentPlugin/CommentPluginSuite.pm @@ -2,7 +2,7 @@ package CommentPluginSuite; use v5.14; -use Moo; +use Foswiki::Class; extends qw(Unit::TestSuite); sub include_tests { return 'CommentPluginTests' } diff --git a/CommentPlugin/test/unit/CommentPlugin/CommentPluginTests.pm b/CommentPlugin/test/unit/CommentPlugin/CommentPluginTests.pm index 713c5db67e..64188a0f3e 100755 --- a/CommentPlugin/test/unit/CommentPlugin/CommentPluginTests.pm +++ b/CommentPlugin/test/unit/CommentPlugin/CommentPluginTests.pm @@ -3,17 +3,12 @@ package CommentPluginTests; use v5.14; -use Unit::Request(); -use Unit::Request::Rest(); -use Unit::Response(); -use Foswiki(); -use Foswiki::UI::Save(); +use Foswiki; use Foswiki::Plugins::CommentPlugin(); use Foswiki::Plugins::CommentPlugin::Comment(); -use CGI; +use Try::Tiny; -use Moo; -use namespace::clean; +use Foswiki::Class; extends qw( FoswikiFnTestCase ); has target_web => ( is => 'rw', ); @@ -32,11 +27,14 @@ around set_up => sub { undef $webObject; Foswiki::Func::getContext()->{view} = 1; - $Foswiki::cfg{Plugins}{CommentPlugin}{RequiredForSave} = 'CHANGE'; - $Foswiki::cfg{Plugins}{CommentPlugin}{GuestCanComment} = 1; - $Foswiki::cfg{Plugins}{CommentPlugin}{TestMode} = 1; - $Foswiki::cfg{Sessions}{TopicsRequireGuestSessions} = + $this->app->cfg->data->{Plugins}{CommentPlugin}{RequiredForSave} = 'CHANGE'; + $this->app->cfg->data->{Plugins}{CommentPlugin}{GuestCanComment} = 1; + $this->app->cfg->data->{Plugins}{CommentPlugin}{TestMode} = 1; + $this->app->cfg->data->{Sessions}{TopicsRequireGuestSessions} = '(CommentPluginTestsTarget|CommentPluginTests|Registration|RegistrationParts|ResetPassword)$'; + + # Do it manually because normally this is done in $app->handleRequest. + $this->app->plugins->enable; }; around tear_down => sub { @@ -46,6 +44,13 @@ around tear_down => sub { $orig->($this); }; +# Foswiki::App handleRequestException callback function. +sub _cbHRE { + my $obj = shift; + my %args = @_; + $args{params}{exception}->rethrow; +} + sub fixture_groups { return ( [ 'viewContext', 'staticContext' ], ); } @@ -243,30 +248,40 @@ HERE $html ); # Compose the query - my $comm = "This is the comment"; - my $query = Unit::Request->new( - initializer => { - 'comment_action' => 'save', - 'comment_type' => $type, - 'comment' => $comm, - 'topic' => "$web.$topic", - } - ); - $query->path_info("/CommentPlugin/comment"); + my $comm = "This is the comment"; + + my @reqParams; if ($anchor) { - $query->param( -name => 'comment_anchor', -value => $anchor ); + push @reqParams, 'comment_anchor', $anchor; } elsif ($location) { - $query->param( -name => 'comment_location', -value => $location ); + push @reqParams, 'comment_location', $location; } else { - $query->param( -name => 'comment_index', -value => $eidx ); + push @reqParams, 'comment_index', $eidx; } - $this->createNewFoswikiSession( $Foswiki::cfg{DefaultUserLogin}, $query ); + $this->createNewFoswikiApp( + requestParams => { + initializer => { + 'comment_action' => 'save', + 'comment_type' => $type, + 'comment' => $comm, + 'topic' => "$web.$topic", + @reqParams, + }, + }, + engineParams => { + initialAttributes => { + action => 'rest', + path_info => "/CommentPlugin/comment", + user => $this->app->cfg->data->{DefaultUserLogin}, + }, + }, + ); # invoke the save handler - $this->captureWithKey( rest => $this->getUIFn('rest'), $this->session ); + $this->captureWithKey( rest => sub { $this->app->handleRequest; }, ); my ( $meta, $text ) = Foswiki::Func::readTopic( $web, $topic ); $this->assert_matches( qr/$comm/, $text, "$web.$topic: $text" ); @@ -461,7 +476,7 @@ qr/]*name="redirectto" value="$test_web.WebPreferences\?blah=01#AnchO ) unless ( Foswiki::Func::getContext()->{static} ); # Redirect with fully qualified web.topic?uri#anchor - my $systemweb = $Foswiki::cfg{SystemWebName}; + my $systemweb = $this->app->cfg->data->{SystemWebName}; $html = Foswiki::Func::expandCommonVariables( "%COMMENT{type=\"bottom\" target=\"" . $this->test_web @@ -529,22 +544,29 @@ HERE } # Compose the query - my $comm = "This is the comment"; - my $query = Unit::Request->new( - initializer => { - 'comment_action' => 'save', - 'comment_type' => 'above', - 'comment' => $comm, - 'comment_nopost' => 'on', - 'topic' => $this->test_web . "." . $this->test_topic - } + my $comm = "This is the comment"; + + $this->createNewFoswikiApp( + requestParams => { + initializer => { + 'comment_action' => 'save', + 'comment_type' => 'above', + 'comment' => $comm, + 'comment_nopost' => 'on', + 'topic' => $this->test_web . "." . $this->test_topic, + }, + }, + engineParams => { + initialAttributes => { + action => 'rest', + path_info => "/CommentPlugin/comment", + user => $this->app->cfg->data->{DefaultUserLogin}, + }, + }, ); - $query->path_info("/CommentPlugin/comment"); - - $this->createNewFoswikiSession( $Foswiki::cfg{DefaultUserLogin}, $query ); # invoke the save handler - $this->captureWithKey( rest => $this->getUIFn('rest'), $this->session ); + $this->captureWithKey( rest => sub { $this->app->handleRequest; }, ); my ( $meta, $text ) = Foswiki::Func::readTopic( $this->test_web, $this->test_topic ); @@ -578,23 +600,30 @@ HERE } # Compose the query - my $comm = "This is the comment"; - my $query = Unit::Request->new( - initializer => { - 'comment_action' => 'save', - 'comment_type' => 'above', - 'comment' => $comm, - 'comment_remove' => '0', - 'comment_index' => '99', - 'topic' => $this->test_web . "/" . $this->test_topic, - } + my $comm = "This is the comment"; + + $this->createNewFoswikiApp( + requestParams => { + initializer => { + 'comment_action' => 'save', + 'comment_type' => 'above', + 'comment' => $comm, + 'comment_remove' => '0', + 'comment_index' => '99', + 'topic' => $this->test_web . "/" . $this->test_topic, + }, + }, + engineParams => { + initialAttributes => { + action => 'rest', + path_info => "/CommentPlugin/comment", + user => $this->app->cfg->data->{DefaultUserLogin}, + }, + }, ); - $query->path_info("/CommentPlugin/comment"); - - $this->createNewFoswikiSession( $Foswiki::cfg{DefaultUserLogin}, $query ); # invoke the save handler - $this->captureWithKey( rest => $this->getUIFn('rest'), $this->session ); + $this->captureWithKey( rest => sub { $this->app->handleRequest; }, ); my ( $meta, $text ) = Foswiki::Func::readTopic( $this->test_web, $this->test_topic ); @@ -658,22 +687,29 @@ HERE } # Compose the query - my $comm = "This is the comment"; - my $query = Unit::Request->new( - initializer => { - 'comment_action' => 'save', - 'comment_type' => 'above', - 'comment' => $comm, - 'comment_anchor' => '#LatestComment', - 'topic' => $this->test_web . "." . $this->test_topic, - } + my $comm = "This is the comment"; + + $this->createNewFoswikiApp( + requestParams => { + initializer => { + 'comment_action' => 'save', + 'comment_type' => 'above', + 'comment' => $comm, + 'comment_anchor' => '#LatestComment', + 'topic' => $this->test_web . "." . $this->test_topic, + }, + }, + engineParams => { + initialAttributes => { + action => 'rest', + path_info => "/CommentPlugin/comment", + user => $this->app->cfg->data->{DefaultUserLogin}, + }, + }, ); - $query->path_info("/CommentPlugin/comment"); - - $this->createNewFoswikiSession( $Foswiki::cfg{DefaultUserLogin}, $query ); # invoke the save handler - $this->captureWithKey( rest => $this->getUIFn('rest'), $this->session ); + $this->captureWithKey( rest => sub { $this->app->handleRequest; }, ); my ( $meta, $text ) = Foswiki::Func::readTopic( $this->test_web, $this->test_topic ); @@ -719,22 +755,29 @@ HERE } # Compose the query - my $comm = "This is the comment"; - my $query = Unit::Request->new( - initializer => { - 'comment_action' => 'save', - 'comment_type' => 'below', - 'comment' => $comm, - 'comment_anchor' => '#LatestComment', - 'topic' => $this->test_web . "." . $this->test_topic, - } + my $comm = "This is the comment"; + + $this->createNewFoswikiApp( + requestParams => { + initializer => { + 'comment_action' => 'save', + 'comment_type' => 'below', + 'comment' => $comm, + 'comment_anchor' => '#LatestComment', + 'topic' => $this->test_web . "." . $this->test_topic, + }, + }, + engineParams => { + initialAttributes => { + action => 'rest', + path_info => "/CommentPlugin/comment", + user => $this->app->cfg->data->{DefaultUserLogin}, + }, + }, ); - $query->path_info("/CommentPlugin/comment"); - - $this->createNewFoswikiSession( $Foswiki::cfg{DefaultUserLogin}, $query ); # invoke the save handler - $this->captureWithKey( rest => $this->getUIFn('rest'), $this->session ); + $this->captureWithKey( rest => sub { $this->app->handleRequest; }, ); my ( $meta, $text ) = Foswiki::Func::readTopic( $this->test_web, $this->test_topic ); @@ -759,7 +802,7 @@ HERE sub verify_acl_COMMENT { my $this = shift; - $Foswiki::cfg{Plugins}{CommentPlugin}{GuestCanComment} = 0; + $this->app->cfg->data->{Plugins}{CommentPlugin}{GuestCanComment} = 0; my $test_user_wikiname = $this->test_user_wikiname; my $sample = <new( - initializer => { - 'comment_action' => 'save', - 'comment_type' => 'above', - 'comment' => $comm, - topic => $this->test_web . "." . $this->test_topic, - } - ); - $query->path_info("/CommentPlugin/comment"); + my $comm = "This is the comment"; - $Foswiki::cfg{Plugins}{CommentPlugin}{RequiredForSave} = 'CHANGE'; + $this->app->cfg->data->{Plugins}{CommentPlugin}{RequiredForSave} = 'CHANGE'; my ( $responseText, $result, $stdout, $stderr ); # First make sure we can't *change* it - $this->createNewFoswikiSession( $this->test_user_login, $query ); + + $this->createNewFoswikiApp( + requestParams => { + initializer => { + 'comment_action' => 'save', + 'comment_type' => 'above', + 'comment' => $comm, + topic => $this->test_web . "." . $this->test_topic, + }, + }, + engineParams => { + action => 'rest', + path_info => "/CommentPlugin/comment", + user => $this->test_user_login, + }, + callbacks => { handleRequestException => \&_cbHRE, }, + ); # invoke the save handler - eval { - ( $responseText, $result, $stdout, $stderr ) = $this->captureWithKey( - rest => $this->getUIFn('rest'), - $this->session - ); + try { + ( $responseText, $result, $stdout, $stderr ) = + $this->captureWithKey( rest => sub { $this->app->handleRequest; }, ); + $this->assert( 0, "Change request unexpectedly passed." ); + } + catch { + my $e = Foswiki::Exception::Fatal->transmute( $_, 0 ); + $this->assert( $e->isa('Foswiki::AccessControlException'), $e ); }; - #print STDERR ( $responseText || '' ), ' )', ( $stdout || '' ), ' E', - # ( $stderr || '' ) . "\n"; - $this->assert_matches( qr"AccessControlException", $@ ); - # Now make sure we *can* change it, given COMMENT access - $Foswiki::cfg{Plugins}{CommentPlugin}{RequiredForSave} = 'COMMENT'; - - $this->createNewFoswikiSession( $this->test_user_login, $query ); + $this->app->cfg->data->{Plugins}{CommentPlugin}{RequiredForSave} = + 'COMMENT'; + + $this->createNewFoswikiApp( + requestParams => { + initializer => { + 'comment_action' => 'save', + 'comment_type' => 'above', + 'comment' => $comm, + topic => $this->test_web . "." . $this->test_topic, + }, + }, + engineParams => { + initialAttributes => { + action => 'rest', + path_info => "/CommentPlugin/comment", + user => $this->test_user_login, + }, + }, + callbacks => { handleRequestException => \&_cbHRE, }, + ); # invoke the save handler - eval { - ( $responseText, $result, $stdout, $stderr ) = $this->captureWithKey( - rest => $this->getUIFn('rest'), - $this->session - ); + try { + ( $responseText, $result, $stdout, $stderr ) = + $this->captureWithKey( rest => sub { $this->app->handleRequest; }, ); + } + catch { + my $e = Foswiki::Exception::Fatal->transmute( $_, 0 ); + $this->assert( 0, "Change request failed: " . $e ); }; - $this->assert( !$@, $@ ); + $this->assert_matches( qr/Status: 302/, $responseText ); my ( $meta, $text ) = @@ -844,24 +913,28 @@ HERE $sample ); # other tests have already covered the non-ajax, no endpoint mode - my $query = Unit::Request->new( - initializer => { - 'comment_action' => 'save', - 'comment_type' => 'above', - 'comment' => "Arfle barfle gloop", - topic => $this->test_web . "." . $this->test_topic, - } - ); - $query->header( 'X-Requested-With' => 'XMLHttpRequest' ); - $query->path_info("/CommentPlugin/comment"); my ( $responseText, $result, $stdout, $stderr ); - $this->createNewFoswikiSession( undef, $query ); - eval { - ( $responseText, $result, $stdout, $stderr ) = $this->captureWithKey( - rest => $this->getUIFn('rest'), - $this->session - ); - }; + + $this->createNewFoswikiApp( + requestParams => { + initializer => { + 'comment_action' => 'save', + 'comment_type' => 'above', + 'comment' => "Arfle barfle gloop", + topic => $this->test_web . "." . $this->test_topic, + }, + }, + engineParams => { + initialAttributes => { + action => 'rest', + path_info => "/CommentPlugin/comment", + headers => { 'X-Requested-With' => 'XMLHttpRequest', }, + }, + }, + ); + + ( $responseText, $result, $stdout, $stderr ) = + $this->captureWithKey( rest => sub { $this->app->handleRequest; }, ); $this->assert_matches( qr/Status: 404/, $responseText ); } @@ -909,34 +982,44 @@ qr/app->cfg->data->{TempfileDir} . "/CommentPluginTestsWarnings"; unlink "$warningLog" if ( -f "$warningLog" ); - $Foswiki::cfg{WarningFileName} = "$warningLog"; - $Foswiki::cfg{Log}{Implementation} = 'Foswiki::Logger::Compatibility'; + $this->app->cfg->data->{WarningFileName} = "$warningLog"; + $this->app->cfg->data->{Log}{Implementation} = + 'Foswiki::Logger::Compatibility'; # Compose the query - my $comm = "This is the comment"; - my $query = Unit::Request->new( - initializer => { - 'comment_action' => 'save', - 'comment_type' => 'returntab', - 'redirectto' => $this->test_web . "." - . $this->test_topic - . "\?tab=discuss", - 'comment' => $comm, - 'topic' => $this->test_web . "." . $this->test_topic - } + my $comm = "This is the comment"; + + $this->createNewFoswikiApp( + requestParams => { + initializer => { + 'comment_action' => 'save', + 'comment_type' => 'returntab', + 'redirectto' => $this->test_web . "." + . $this->test_topic + . "\?tab=discuss", + 'comment' => $comm, + 'topic' => $this->test_web . "." . $this->test_topic + }, + }, + engineParams => { + initialAttributes => { + path_info => "/CommentPlugin/comment", + user => $this->app->cfg->data->{DefaultUserLogin}, + action => 'rest', + }, + }, ); - $query->path_info("/CommentPlugin/comment"); - $this->createNewFoswikiSession( $Foswiki::cfg{DefaultUserLogin}, $query ); my $text = "Ignore this text"; # invoke the save handler # $responseText, $result, $stdout, $stderr my ( $response, $result, $stdout, $stderr ) = - $this->captureWithKey( rest => $this->getUIFn('rest'), $this->session ); + $this->captureWithKey( rest => sub { $this->app->handleRequest; }, ); $this->assert_matches( qr/^Status: 302/ms, $response ); $this->assert_matches( @@ -948,7 +1031,7 @@ qr/new( - initializer => { - 'comment_action' => 'save', - 'comment_type' => 'above', - 'comment' => $comm, - topic => $this->test_web . "." . $this->test_topic, - } - ); - $query->path_info("/CommentPlugin/comment"); + my $comm = "This is the %TOPIC% comment"; - $Foswiki::cfg{Plugins}{CommentPlugin}{RequiredForSave} = 'CHANGE'; + $this->app->cfg->data->{Plugins}{CommentPlugin}{RequiredForSave} = 'CHANGE'; my ( $responseText, $result, $stdout, $stderr ); # Now make sure we *can* change it, given COMMENT access - $Foswiki::cfg{Plugins}{CommentPlugin}{RequiredForSave} = 'COMMENT'; - - $this->createNewFoswikiSession( $this->test_user_login, $query ); + $this->app->cfg->data->{Plugins}{CommentPlugin}{RequiredForSave} = + 'COMMENT'; + + $this->createNewFoswikiApp( + requestParams => { + initializer => { + 'comment_action' => 'save', + 'comment_type' => 'above', + 'comment' => $comm, + topic => $this->test_web . "." . $this->test_topic, + }, + }, + engineParams => { + initialAttributes => { + action => 'rest', + path_info => "/CommentPlugin/comment", + user => $this->test_user_login, + }, + }, + callbacks => { handleRequestException => \&_cbHRE, }, + ); # invoke the save handler - eval { - ( $responseText, $result, $stdout, $stderr ) = $this->captureWithKey( - rest => $this->getUIFn('rest'), - $this->session - ); + try { + ( $responseText, $result, $stdout, $stderr ) = + $this->captureWithKey( rest => sub { $this->app->handleRequest; }, ); + } + catch { + my $e = Foswiki::Exception::Fatal->transmute( $_, 0 ); + $this->assert( 0, $e ); }; - $this->assert( !$@, $@ ); + $this->assert_matches( qr/Status: 302/, $responseText ); my ( $meta, $text ) = @@ -1011,7 +1105,7 @@ HERE sub test_comment_encoding_guest { my $this = shift; - $Foswiki::cfg{Plugins}{CommentPlugin}{GuestCanComment} = 1; + $this->app->cfg->data->{Plugins}{CommentPlugin}{GuestCanComment} = 1; my $test_user_wikiname = $this->test_user_wikiname; my $sample = <new( - initializer => { - 'comment_action' => 'save', - 'comment_type' => 'above', - 'comment' => $comm, - topic => $this->test_web . "." . $this->test_topic, - } - ); - $query->path_info("/CommentPlugin/comment"); + my $comm = "This is the %TOPIC% comment"; - $Foswiki::cfg{Plugins}{CommentPlugin}{RequiredForSave} = 'CHANGE'; + $this->app->cfg->data->{Plugins}{CommentPlugin}{RequiredForSave} = 'CHANGE'; my ( $responseText, $result, $stdout, $stderr ); # Now make sure we *can* change it, given COMMENT access - $Foswiki::cfg{Plugins}{CommentPlugin}{RequiredForSave} = 'COMMENT'; - - $this->createNewFoswikiSession( $Foswiki::cfg{DefaultUserLogin}, $query ); + $this->app->cfg->data->{Plugins}{CommentPlugin}{RequiredForSave} = + 'COMMENT'; + + $this->createNewFoswikiApp( + requestParams => { + initializer => { + 'comment_action' => 'save', + 'comment_type' => 'above', + 'comment' => $comm, + topic => $this->test_web . "." . $this->test_topic, + }, + }, + engineParams => { + initialAttributes => { + action => 'rest', + path_info => "/CommentPlugin/comment", + user => $this->app->cfg->data->{DefaultUserLogin}, + }, + }, + callbacks => { handleRequestException => \&_cbHRE, }, + ); # invoke the save handler - eval { - ( $responseText, $result, $stdout, $stderr ) = $this->captureWithKey( - rest => $this->getUIFn('rest'), - $this->session - ); + try { + ( $responseText, $result, $stdout, $stderr ) = + $this->captureWithKey( rest => sub { $this->app->handleRequest; }, ); + } + catch { + my $e = Foswiki::Exception::Fatal->transmute( $_, 0 ); + $this->assert( 0, $e ); }; - $this->assert( !$@, $@ ); + $this->assert_matches( qr/Status: 302/, $responseText ); my ( $meta, $text ) = diff --git a/FastCGIEngineContrib/lib/Foswiki/Contrib/FastCGIEngineContrib/DEPENDENCIES b/FastCGIEngineContrib/lib/Foswiki/Contrib/FastCGIEngineContrib/DEPENDENCIES index 354d3c6376..0e92bf115c 100644 --- a/FastCGIEngineContrib/lib/Foswiki/Contrib/FastCGIEngineContrib/DEPENDENCIES +++ b/FastCGIEngineContrib/lib/Foswiki/Contrib/FastCGIEngineContrib/DEPENDENCIES @@ -1,2 +1,2 @@ -FCGI, >0.67, cpan, Required for nginx, and other web servers when configured for FastCGI support -FCGI::ProcManager, >0.23, cpan, Optional. Required on nginx for dynamic FCGI handler management. +FCGI, >0.67, cpan, Optional. Required for nginx, and other web servers when configured for FastCGI support +FCGI::ProcManager, >0.23, cpan, Optional. Required on nginx for dynamic FCGI handler management. Not used with Apache. diff --git a/JsonRpcContrib/lib/Foswiki/Contrib/JsonRpcContrib/Response.pm b/JsonRpcContrib/lib/Foswiki/Contrib/JsonRpcContrib/Response.pm index 8bf5ab6140..8c50e514d7 100644 --- a/JsonRpcContrib/lib/Foswiki/Contrib/JsonRpcContrib/Response.pm +++ b/JsonRpcContrib/lib/Foswiki/Contrib/JsonRpcContrib/Response.pm @@ -22,10 +22,8 @@ use Assert; use Compress::Zlib (); use JSON (); -use Moo; -use namespace::clean; +use Foswiki::Class qw(app); extends qw(Foswiki::Object); -with qw(Foswiki::AppObject); use constant TRACE => 0; # toggle me @@ -107,7 +105,7 @@ sub encode { $message = { jsonrpc => "2.0", error => { - code => $code, + code => $code + 0, # Sometimes code comes as a string, not int. message => $message, }, }; diff --git a/JsonRpcContrib/lib/Foswiki/Contrib/JsonRpcContrib/Server.pm b/JsonRpcContrib/lib/Foswiki/Contrib/JsonRpcContrib/Server.pm index 9f2d7d3fb3..08e1971ae3 100644 --- a/JsonRpcContrib/lib/Foswiki/Contrib/JsonRpcContrib/Server.pm +++ b/JsonRpcContrib/lib/Foswiki/Contrib/JsonRpcContrib/Server.pm @@ -82,16 +82,20 @@ sub dispatch { return; } + # ---------- OBSOLETE BEGIN ---------- + # XXX This code is obsolete since the request object is handling this logic + # now and provides valid defined web.topic for the application. # get topic parameter and set the location overriding any # other value derived from the namespace param - my $topic = $request->param('topic') - || $Foswiki::cfg{HomeTopicName}; - my ( $jsrpcWeb, $jsrpcTopic ) = - Foswiki::Func::normalizeWebTopicName( $Foswiki::cfg{UsersWebName}, - $topic ); - $app->request->web($jsrpcWeb); - $app->request->topic($jsrpcTopic); - $this->writeDebug("topic=$topic") if TRACE; + #my $topic = $request->param('topic') + # || $Foswiki::cfg{HomeTopicName}; + #my ( $jsrpcWeb, $jsrpcTopic ) = + # Foswiki::Func::normalizeWebTopicName( $Foswiki::cfg{UsersWebName}, + # $topic ); + #$app->request->web($jsrpcWeb); + #$app->request->topic($jsrpcTopic); + #$this->writeDebug("topic=$topic") if TRACE; + # ---------- OBSOLETE END --------- # get handler for this namespace my $handler = $this->getHandler($request); @@ -197,16 +201,13 @@ sub dispatch { # finally my $redirectto = $request->param('redirectto'); + my $url; + if ( $code == 0 && defined $redirectto ) { - my $url; - if ( $redirectto =~ /^https?:/ ) { - $url = $redirectto; - } - else { - $url = - $app->cfg->getScriptUrl( 1, 'view', $app->request->web, - $redirectto ); - } + $url = $app->redirectto($redirectto); + } + + if ($url) { $app->redirect($url); } else { diff --git a/JsonRpcContrib/lib/Foswiki/Request/JSON.pm b/JsonRpcContrib/lib/Foswiki/Request/JSON.pm index 0cdb1ccd68..8ffee13e67 100644 --- a/JsonRpcContrib/lib/Foswiki/Request/JSON.pm +++ b/JsonRpcContrib/lib/Foswiki/Request/JSON.pm @@ -167,10 +167,14 @@ sub parseJSON { $data = ( ref($foo) eq 'ARRAY' ) ? shift @$foo : $foo; + my $minimal = ($data) ? 0 : 1; $data ||= '{"jsonrpc":"2.0"}'; # Minimal setup $jsondata = $this->initFromString($data); + # If the parse failed, give up here. + return $jsondata if $this->jsonerror; + # some basic checks if this is a proper json-rpc 2.0 request # must have a version tag @@ -183,13 +187,16 @@ sub parseJSON { ); } - # must have a json method + # must have a json method + # SMELL: This error is suppressed for simple query param type json requests. + # It's a processing order issue. The method is there in the query path, but it + # has not been parsed yet. $this->jsonerror( new Foswiki::Contrib::JsonRpcContrib::Error( code => -32600, text => "Invalid JSON-RPC request - no method" ) - ) unless defined $jsondata->{method}; + ) unless $minimal || defined $jsondata->{method}; # must not have any other keys other than these foreach my $key ( keys %{$jsondata} ) { @@ -239,7 +246,7 @@ sub initFromString { =begin TML ----++ private objectMethod _establishAddress() -> n/a +---++ private objectMethod _establishAttributes() -> n/a Used internally by the web() and topic() methods to trigger parsing of the JSON topic parameter or the CGI topic parameer, and set object variables with the results. @@ -262,6 +269,15 @@ around _establishAttributes => sub { $parse->{topic} = ucfirst( $parse->{topic} ) if ( defined $parse->{topic} ); + # SMELL. This isn't working. Test still fails with wrong web + unless ( defined $parse->{web} ) { + if ( defined $this->param('defaultweb') ) { + $parse->{web} = + Foswiki::Sandbox::untaint( $this->param('defaultweb'), + \&Foswiki::Sandbox::validateWebName ); + } + } + # Note that Web can still be undefined. Caller then determines if the # defaultweb query param, or the HomeWeb config parameter should be used. @@ -396,11 +412,11 @@ around _trigger_method => sub { my $this = shift; my ($value) = @_; - if ( defined $value && $this->_hasjsondata && lc($value) ne 'post' ) { + if ( defined $value && $this->has_jsondata && lc($value) ne 'post' ) { $this->jsonerror( new Foswiki::Contrib::JsonRpcContrib::Error( code => -32600, - text => "Method must be POST, not " . $value + text => "Method must be POST, not " . uc($value) ) ); } @@ -442,7 +458,8 @@ sub _establishNamespace { my $namespace = $1; my $method = $2; - if ( defined $method ) { + # Don't set the method if it has already been established by POSTDATA + if ( defined $method && !$this->jsonmethod ) { $this->jsonmethod($method); } diff --git a/JsonRpcContrib/test/unit/JsonRpcContrib/JsonrpcTests.pm b/JsonRpcContrib/test/unit/JsonRpcContrib/JsonrpcTests.pm index 6d6dd25cef..83566a51a3 100644 --- a/JsonRpcContrib/test/unit/JsonRpcContrib/JsonrpcTests.pm +++ b/JsonRpcContrib/test/unit/JsonRpcContrib/JsonrpcTests.pm @@ -11,8 +11,7 @@ use Foswiki::EngineException(); use Carp(); use Try::Tiny; -use Moo; -use namespace::clean; +use Foswiki::Class; extends qw( FoswikiFnTestCase ); use Unit::Request::JSON; @@ -27,6 +26,17 @@ around set_up => sub { return; }; +around loadExtraConfig => sub { + my $orig = shift; + my $this = shift; + + $orig->( $this, @_ ); + + my $cfgData = $this->app->cfg->data; + $cfgData->{PermittedRedirectHostUrls} = 'http://lolcats.com'; + +}; + # A simple REST handler sub json_handler { my ( $app, $request ) = @_; @@ -35,14 +45,15 @@ sub json_handler { die "incorrect jsonmethod()" unless $request->jsonmethod() eq 'trial'; # The old method() was jsonmethod alias. - #die "incorrect method()" unless $request->method() eq 'trial'; - die "incorrect method()" unless $request->method eq 'post'; - die "Incorrect topic" unless $app->request->topic eq 'WebChanges'; - die "Incorrect web" unless $app->request->web eq 'System'; + die "incorrect method()" unless $request->jsonmethod() eq 'trial'; + die "incorrect method()" unless $request->method eq 'post'; + die "Incorrect topic" unless $app->request->topic eq 'WebChanges'; + die "Incorrect web " . $app->request->web + unless $app->request->web eq 'System'; return 'SUCCESS'; } -# A simple REST handler with error +# A simple JSON handler with error sub json_and_be_thankful { my ( $session, $subject, $verb ) = @_; @@ -89,16 +100,8 @@ sub test_simple_postdata { sub { $this->createNewFoswikiApp( - requestParams => { - initializer => { - action => ['jsonrpc'], - uri => '/' . __PACKAGE__ . '/trial', - path_info => '/' . __PACKAGE__ . '/trial', - }, - }, engineParams => { initialAttributes => { - uri => '/' . __PACKAGE__ . "/trial", path_info => '/' . __PACKAGE__ . "/trial", method => 'post', action => 'jsonrpc', @@ -115,8 +118,8 @@ sub test_simple_postdata { ); } catch { - my $e = $_; - if ( ref($e) && $e->isa('Foswiki::EngineException') ) { + my $e = Foswiki::Exception::Fatal->transmute( $_, 0 ); + if ( $e->isa('Foswiki::EngineException') ) { $this->assert_equals( 401, $e->status, $e->stringify ); } else { @@ -127,157 +130,444 @@ sub test_simple_postdata { $this->assert_matches( qr/"result" : "SUCCESS"/, $response ); return; } -1; -__END__ + +# -32600: Invalid Request - The JSON sent is not a valid Request object. +sub test_invalid_request { + my $this = shift; + + #$query->path_info( '/' . __PACKAGE__ . '/saywhat' ); + # + my $response; + + # This first test has a syntax error - missing { + try { + ($response) = $this->capture( + sub { + + $this->createNewFoswikiApp( + engineParams => { + initialAttributes => { + path_info => '/' . __PACKAGE__ . "/trial", + method => 'post', + action => 'jsonrpc', + postData => +'{"jsonrpc":"1.0","method":"trial","params":"wizard":"ScriptHash","method":"verify","keys":"{ScriptUrlPaths}{view}","set":{},"topic":"System.WebChanges","cfgpassword":"xxxxxxx"},"id":"iCall-verify_6"}' + }, + }, + ); + Foswiki::Contrib::JsonRpcContrib::registerMethod( __PACKAGE__, + 'trial', \&json_handler ); + + return $this->app->handleRequest; + }, + ); + } + catch { + my $e = Foswiki::Exception::Fatal->transmute( $_, 0 ); + if ( $e->isa('Foswiki::EngineException') ) { + $this->assert_equals( 401, $e->status, $e->stringify ); + } + else { + $e->rethrow; + } + }; + + $this->assert_matches( qr/"code" : -32700/, $response ); + $this->assert_matches( + qr/"message" : "Parse error - invalid json-rpc request:/, $response ); + + # Now test with an invalid version + try { + ($response) = $this->capture( + sub { + + $this->createNewFoswikiApp( + engineParams => { + initialAttributes => { + path_info => '/' . __PACKAGE__ . "/trial", + method => 'post', + action => 'jsonrpc', + postData => +'{"jsonrpc":"1.0","method":"trial","params":{"wizard":"ScriptHash","method":"verify","keys":"{ScriptUrlPaths}{view}","set":{},"topic":"System.WebChanges","cfgpassword":"xxxxxxx"},"id":"iCall-verify_6"}', + }, + }, + ); + Foswiki::Contrib::JsonRpcContrib::registerMethod( __PACKAGE__, + 'trial', \&json_handler ); + + return $this->app->handleRequest; + }, + ); + } + catch { + my $e = Foswiki::Exception::Fatal->transmute( $_, 0 ); + if ( $e->isa('Foswiki::EngineException') ) { + $this->assert_equals( 401, $e->status, $e->stringify ); + } + else { + $e->rethrow; + } + }; + + $this->assert_matches( qr/"code" : -32600/, $response ); + $this->assert_matches( qr/"message" : "Invalid JSON-RPC request/, + $response ); + + return; +} # Simple jsonrpc, using query params sub test_simple_query_params { my $this = shift; - Foswiki::Contrib::JsonRpcContrib::registerMethod( __PACKAGE__, 'trial', - \&json_handler ); - my $query = Unit::Request::JSON->new( { action => ['jsonrpc'], } ); - $query->path_info( '/' . __PACKAGE__ . '/trial' ); - $query->method('post'); - $query->param( 'topic', 'WebChanges' ); - $query->param( 'defaultweb', 'System' ); - $this->createNewFoswikiSession( $this->{test_user_login}, $query ); - my ( $response, $result, $out, $err ) = - $this->capture( $UI_FN, $this->{session} ); + my $response; + + try { + ($response) = $this->capture( + sub { + + $this->createNewFoswikiApp( + requestParams => { + initializer => { + action => ['jsonrpc'], + uri => '/' . __PACKAGE__ . '/trial', + path_info => '/' . __PACKAGE__ . '/trial', + topic => 'WebChanges', + defaultweb => 'System', + }, + }, + engineParams => { + initialAttributes => { + uri => '/' . __PACKAGE__ . "/trial", + path_info => '/' . __PACKAGE__ . "/trial", + method => 'post', + action => 'jsonrpc', + }, + }, + ); + Foswiki::Contrib::JsonRpcContrib::registerMethod( __PACKAGE__, + 'trial', \&json_handler ); + + return $this->app->handleRequest; + }, + ); + } + catch { + my $e = Foswiki::Exception::Fatal->transmute( $_, 0 ); + if ( $e->isa('Foswiki::EngineException') ) { + $this->assert_equals( 401, $e->status, $e->stringify ); + } + else { + $e->rethrow; + } + }; $this->assert_matches( qr/"result" : "SUCCESS"/, $response ); return; } -# -32601: Method not found - The method does not exist / is not available. -sub test_method_missing { +sub test_post_required { my $this = shift; - Foswiki::Contrib::JsonRpcContrib::registerMethod( __PACKAGE__, 'trial', - \&json_handler ); - my $query = Unit::Request::JSON->new( { action => ['jsonrpc'], } ); - $query->path_info( '/' . __PACKAGE__ . '/saywhat' ); - $query->method('post'); - $query->param( 'topic', 'WebChanges' ); - $query->param( 'defaultweb', 'System' ); - $this->createNewFoswikiSession( $this->{test_user_login}, $query ); - my ( $response, $result, $out, $err ) = - $this->capture( $UI_FN, $this->{session} ); + my $response; - $this->assert_matches( qr/"code" : -32601/, $response ); + try { + ($response) = $this->capture( + sub { + + $this->createNewFoswikiApp( + engineParams => { + initialAttributes => { + path_info => '/' . __PACKAGE__ . "/trial", + method => 'get', + action => 'jsonrpc', + postData => +'{"jsonrpc":"2.0","method":"trial","params":{"wizard":"ScriptHash","method":"verify","keys":"{ScriptUrlPaths}{view}","set":{},"topic":"System.WebChanges","cfgpassword":"xxxxxxx"},"id":"iCall-verify_6"}', + }, + }, + ); + Foswiki::Contrib::JsonRpcContrib::registerMethod( __PACKAGE__, + 'trial', \&json_handler ); + + return $this->app->handleRequest; + }, + ); + } + catch { + my $e = Foswiki::Exception::Fatal->transmute( $_, 0 ); + if ( $e->isa('Foswiki::EngineException') ) { + $this->assert_equals( 401, $e->status, $e->stringify ); + } + else { + $e->rethrow; + } + }; + + $this->assert_matches( qr/"code" : -32600/, $response ); + $this->assert_matches( qr/"message" : "Method must be POST,/, $response ); return; } -# -32600: Invalid Request - The JSON sent is not a valid Request object. -sub test_invalid_request { +# -32601: Method not found - The method does not exist / is not available. +sub test_method_missing { my $this = shift; - Foswiki::Contrib::JsonRpcContrib::registerMethod( __PACKAGE__, 'trial', - \&json_handler ); + my $response; - my $query = Unit::Request::JSON->new( { action => ['jsonrpc'], } ); - $query->path_info( '/' . __PACKAGE__ . '/saywhat' ); - $query->param( 'POSTDATA', -'{"jsonrpc":"2.0","method":"trial","params":"wizard":"ScriptHash","method":"verify","keys":"{ScriptUrlPaths}{view}","set":{},"topic":"System.WebChanges","cfgpassword":"xxxxxxx"},"id":"iCall-verify_6"}' - ); - $this->createNewFoswikiSession( $this->{test_user_login}, $query ); - my ( $response, $result, $out, $err ) = - $this->capture( $UI_FN, $this->{session} ); - $this->assert_matches( qr/"code" : -32600/, $response ); + # Test as part of query path + try { + ($response) = $this->capture( + sub { + + $this->createNewFoswikiApp( + engineParams => { + initialAttributes => { + path_info => '/' . __PACKAGE__ . "/saywhat", + method => 'post', + action => 'jsonrpc', + }, + }, + ); + Foswiki::Contrib::JsonRpcContrib::registerMethod( __PACKAGE__, + 'trial', \&json_handler ); + + return $this->app->handleRequest; + }, + ); + } + catch { + my $e = Foswiki::Exception::Fatal->transmute( $_, 0 ); + if ( $e->isa('Foswiki::EngineException') ) { + $this->assert_equals( 401, $e->status, $e->stringify ); + } + else { + $e->rethrow; + } + }; + + $this->assert_matches( qr/"code" : -32601/, $response ); $this->assert_matches( - qr/"message" : "Invalid JSON-RPC request - must be jsonrpc: '2.0'"/, - $response ); +qr/"message" : "Invalid invocation - unknown handler for JsonrpcTests.saywhat"/, + $response + ); + + # Test as part of json data + try { + ($response) = $this->capture( + sub { + + $this->createNewFoswikiApp( + engineParams => { + initialAttributes => { + path_info => '/' . __PACKAGE__ . "/", + method => 'post', + action => 'jsonrpc', + postData => +'{"jsonrpc":"2.0","method":"saywhat","params":{"wizard":"ScriptHash","method":"verify","keys":"{ScriptUrlPaths}{view}","set":{},"topic":"System.WebChanges","cfgpassword":"xxxxxxx"},"id":"iCall-verify_6"}', + }, + }, + ); + Foswiki::Contrib::JsonRpcContrib::registerMethod( __PACKAGE__, + 'trial', \&json_handler ); + + return $this->app->handleRequest; + }, + ); + } + catch { + my $e = Foswiki::Exception::Fatal->transmute( $_, 0 ); + if ( $e->isa('Foswiki::EngineException') ) { + $this->assert_equals( 401, $e->status, $e->stringify ); + } + else { + $e->rethrow; + } + }; + $this->assert_matches( qr/"code" : -32601/, $response ); + $this->assert_matches( +qr/"message" : "Invalid invocation - unknown handler for JsonrpcTests.saywhat"/, + $response + ); return; } -sub test_post_required { +# Test the handler that dies +sub test_500 { my $this = shift; - Foswiki::Contrib::JsonRpcContrib::registerMethod( __PACKAGE__, 'trial', - \&json_handler ); + my $response; - my $query = Unit::Request::JSON->new( { action => ['jsonrpc'], } ); - $query->method('get'); - $query->path_info( '/' . __PACKAGE__ . '/trial' ); - $query->param( 'POSTDATA', -'{"jsonrpc":"2.0","method":"trial","params":{"wizard":"ScriptHash","method":"verify","keys":"{ScriptUrlPaths}{view}","set":{},"topic":"WebChanges","cfgpassword":"xxxxxxx"},"id":"iCall-verify_6"}' - ); - $this->createNewFoswikiSession( $this->{test_user_login}, $query ); - my ( $response, $result, $out, $err ) = - $this->capture( $UI_FN, $this->{session} ); + try { + ($response) = $this->capture( + sub { - $this->assert_matches( qr/"code" : -32600/, $response ); - $this->assert_matches( qr/"message" : "Method must be POST"/, $response ); + $this->createNewFoswikiApp( + engineParams => { + initialAttributes => { + path_info => '/' . __PACKAGE__ . "/", + method => 'post', + action => 'jsonrpc', + postData => +'{"jsonrpc":"2.0","method":"json_and_be_thankful","params":{"wizard":"ScriptHash","method":"verify","keys":"{ScriptUrlPaths}{view}","set":{},"topic":"System.WebChanges","cfgpassword":"xxxxxxx"},"id":"iCall-verify_6"}', + }, + }, + ); + Foswiki::Contrib::JsonRpcContrib::registerMethod( __PACKAGE__, + 'json_and_be_thankful', \&json_and_be_thankful ); + return $this->app->handleRequest; + }, + ); + } + catch { + my $e = Foswiki::Exception::Fatal->transmute( $_, 0 ); + $e->rethrow; + }; + + $this->assert_matches( qr#^Status: 500#m, $response ); + $this->assert_matches( qr#"code" : 1#, $response ); # Code 1 if handler dies + $this->assert_matches( + qr# "message" : "meistersinger at JsonRpcContrib/JsonrpcTests.pm#, + $response ); return; } -# Test the handler that dies -sub test_500 { +# Test the redirectto parameter with a URL +sub test_redirectto_URL { my $this = shift; - Foswiki::Contrib::JsonRpcContrib::registerMethod( __PACKAGE__, 'trial', - \&json_and_be_thankful ); + my $response; - my $query = Unit::Request::JSON->new( { action => ['jsonrpc'], } ); - $query->path_info( '/' . __PACKAGE__ . '/trial' ); - $query->method('post'); - $this->createNewFoswikiSession( $this->{test_user_login}, $query ); - &$UI_FN( $this->{session} ); - my ($text) = $this->capture( $UI_FN, $this->{session} ); + try { + ($response) = $this->capture( + sub { + + $this->createNewFoswikiApp( + requestParams => { + initializer => { + action => ['jsonrpc'], + uri => '/' . __PACKAGE__ . '/trial', + path_info => '/' . __PACKAGE__ . '/trial', + topic => 'WebChanges', + defaultweb => 'System', + redirectto => + "http://lolcats.com/funny?pussy=cat#MyAnch", + }, + }, + engineParams => { + initialAttributes => { + uri => '/' . __PACKAGE__ . "/trial", + path_info => '/' . __PACKAGE__ . "/trial", + method => 'post', + action => 'jsonrpc', + }, + }, + ); + Foswiki::Contrib::JsonRpcContrib::registerMethod( __PACKAGE__, + 'trial', \&json_handler ); + + return $this->app->handleRequest; + }, + ); + } + catch { + my $e = Foswiki::Exception::Fatal->transmute( $_, 0 ); + if ( $e->isa('Foswiki::EngineException') ) { + $this->assert_equals( 401, $e->status, $e->stringify ); + } + else { + $e->rethrow; + } + }; + + $this->assert_matches( qr#^Status: 302#m, $response ); + $this->assert_matches( + qr#^Location: http://lolcats.com/funny\?pussy=cat\#MyAnch\s*$#m, + $response ); - $this->assert_matches( qr#^Status: 500#m, $text ); - $this->assert_matches( qr#"code" : 1#, $text ); # Code 1 if handler dies return; } # Test the redirectto parameter as a json param -sub test_redirectto { +sub test_redirectto_JSON { my $this = shift; - Foswiki::Contrib::JsonRpcContrib::registerMethod( __PACKAGE__, 'trial', - \&json_handler ); - my $query = Unit::Request::JSON->new( { action => ['jsonrpc'], } ); - $query->path_info( '/' . __PACKAGE__ . '/trial' ); - $query->method('post'); - $query->param( 'POSTDATA', -'{"jsonrpc":"2.0","method":"trial","params":{"wizard":"ScriptHash","method":"verify","redirectto":"' - . "$this->{test_web}/$this->{test_topic}" - . '","set":{},"topic":"System.WebChanges","cfgpassword":"xxxxxxx"},"id":"iCall-verify_6"}' - ); - $this->createNewFoswikiSession( $this->{test_user_login}, $query ); - my ( $response, $result, $out, $err ) = - $this->capture( $UI_FN, $this->{session} ); + my $response; + + try { + ($response) = $this->capture( + sub { + + $this->createNewFoswikiApp( + engineParams => { + initialAttributes => { + path_info => '/' . __PACKAGE__ . "/trial", + method => 'post', + action => 'jsonrpc', + postData => +qq#{"jsonrpc":"2.0","method":"trial","params":{"wizard":"ScriptHash","method":"verify","redirectto":"$this->{test_web}.$this->{test_topic}","keys":"{ScriptUrlPaths}{view}","set":{},"topic":"System.WebChanges","cfgpassword":"xxxxxxx"},"id":"iCall-verify_6"}#, + }, + }, + ); + Foswiki::Contrib::JsonRpcContrib::registerMethod( __PACKAGE__, + 'trial', \&json_handler ); + + return $this->app->handleRequest; + }, + ); + } + catch { + my $e = Foswiki::Exception::Fatal->transmute( $_, 0 ); + $e->rethrow; + }; $this->assert_matches( qr#^Status: 302#m, $response ); $this->assert_matches( qr#^Location:.*$this->{test_web}/$this->{test_topic}\s*$#m, $response ); - return; } # Test the redirectto parameter with anchor -sub future_test_redirectto_Anchor { +sub test_redirectto_Anchor { my $this = shift; + my $response; + try { + ($response) = $this->capture( + sub { + + $this->createNewFoswikiApp( + engineParams => { + initialAttributes => { + path_info => '/' . __PACKAGE__ . "/trial", + method => 'post', + action => 'jsonrpc', + postData => +qq~{"jsonrpc":"2.0","method":"trial","params":{"wizard":"ScriptHash","method":"verify","redirectto":"$this->{test_web}.$this->{test_topic}#MyAnch","keys":"{ScriptUrlPaths}{view}","set":{},"topic":"System.WebChanges","cfgpassword":"xxxxxxx"},"id":"iCall-verify_6"}~, + }, + }, + ); + Foswiki::Contrib::JsonRpcContrib::registerMethod( __PACKAGE__, + 'trial', \&json_handler ); + + return $this->app->handleRequest; + }, + ); + } + catch { + my $e = Foswiki::Exception::Fatal->transmute( $_, 0 ); + $e->rethrow; + }; Foswiki::Func::registerRESTHandler( 'trial', \&json_handler ); - my $query = Unit::Request::JSON->new( - { - action => ['rest'], - redirectto => "$this->{test_web}/$this->{test_topic}#MyAnch", - } - ); - $query->path_info( '/' . __PACKAGE__ . '/trial' ); - $query->method('post'); - $this->createNewFoswikiSession( $this->{test_user_login}, $query ); - my ($text) = $this->capture( $UI_FN, $this->{session} ); - $this->assert_matches( qr#^Status: 302#m, $text ); + $this->assert_matches( qr#^Status: 302#m, $response ); $this->assert_matches( qr#^Location:.*$this->{test_web}/$this->{test_topic}\#MyAnch\s*$#m, - $text ); + $response ); return; } +1; +__END__ ## Test the authentication methods sub future_test_authmethods { my $this = shift; @@ -530,30 +820,6 @@ sub future_test_authenticate { return; } -# Test the redirectto parameter with a URL -sub future_test_redirectto_URL { - my $this = shift; - Foswiki::Func::registerRESTHandler( 'trial', \&json_handler ); - $Foswiki::cfg{PermittedRedirectHostUrls} = 'http://lolcats.com'; - - my $query = Unit::Request::JSON->new( - { - action => ['rest'], - redirectto => "http://lolcats.com/funny?pussy=cat", - } - ); - $query->path_info( '/' . __PACKAGE__ . '/trial' ); - $query->method('post'); - $this->createNewFoswikiSession( $this->{test_user_login}, $query ); - &$UI_FN( $this->{session} ); - my ($text) = $this->capture( $UI_FN, $this->{session} ); - $this->assert_matches( qr#^Status: 302#m, $text ); - $this->assert_matches( - qr#^Location: http://lolcats.com/funny\?pussy=cat\s*$#m, $text ); - - return; -} - # Test the redirectto parameter with a bad URL sub future_test_redirectto_badURL { my $this = shift; diff --git a/ModPerlEngineContrib/lib/Foswiki/Contrib/ModPerlEngineContrib/DEPENDENCIES b/ModPerlEngineContrib/lib/Foswiki/Contrib/ModPerlEngineContrib/DEPENDENCIES index e8d23e7a64..888d4f7dde 100644 --- a/ModPerlEngineContrib/lib/Foswiki/Contrib/ModPerlEngineContrib/DEPENDENCIES +++ b/ModPerlEngineContrib/lib/Foswiki/Contrib/ModPerlEngineContrib/DEPENDENCIES @@ -1,4 +1,2 @@ -mod_perl,>=1.24,cpan,Required if you're using Apache 1.3 and plan to enable mod_perl -mod_perl2,>=2.0,cpan,Required if you're using Apache 2.x and plan to enable mod_perl -Apache::Request,>=1.30,cpan,Optional. Recommended if you're using Apache 1.3 and plan to enable mod_perl -Apache2::Request,>=2.0,cpan,Optional. Recommended if you're using Apache 2.x and plan to enable mod_perl +mod_perl2,>=2.0,cpan,Optional, but required if you're using Apache 2.x and plan to enable mod_perl +Apache2::Request,>=2.0,cpan,Optional, but recommended if you're using Apache 2.x and plan to enable mod_perl diff --git a/UnitTestContrib/lib/Foswiki/Engine/Test.pm b/UnitTestContrib/lib/Foswiki/Engine/Test.pm index 9458b7191b..015880af73 100644 --- a/UnitTestContrib/lib/Foswiki/Engine/Test.pm +++ b/UnitTestContrib/lib/Foswiki/Engine/Test.pm @@ -5,37 +5,116 @@ use v5.14; =begin TML ----+!! package Foswiki::Engine::Test +---+!! Class Foswiki::Engine::Test This is unit tests support engine which is supposed to simulate real-life environment for test cases. -A instance of this class initialize itself using the following sources of data: - - * Key =__foswikiEngineTestInit= on =env= attribute hash. This key must be a - hashref which is passed to the parent constructor as a hash of defaults - alongside with user supplied parameters in =new()= call. User parameters - has preference over the defaults. - * For both defaults and user parameters =setUrl= key of =initialAttributes= - hash may be used to set those parameters which are not set implicitly by - corresponding source. - * =FOSWIKI_TEST_= prefixed =env= attribute hash keys for individual keys of - =*Data= attributes. For example, =$engine->pathData->{path_info}= would be - set from =FOSWIKI_TEST_PATH_INFO=. These are used only when corresponding - =*Data= attribute is not initialized at object construction stage. - * Similar to other engines =FOSWIKI_ACTION= might be used if none of the - above sources provided a value for the =pathData->{action}= key. - * =FOSWIKI_TEST_QUERY_STRING= is used for setting =queryParameters= - attribute. +---++ Object Initialization + +A instance of this class initialize itself using the following sources of data +(in the order from higher to lower priority): + + 1. Constructor stage: + * User supplied parameters. + * =setUrl= key of =initialAttributes= constuctor parameter. + * Key =__foswikiEngineTestInit= on =env= constructor parameter. This key + must be a hashref and it defines the defaults for constructor + parameters. + * =setUrl= key of =initialAttributes= parameter from + =__foswikiEngineTestInit=. + 1. Run-time stage: + * =initialAttributes= object attribute. For example, + =$engine->pathData->{path_info}= would be fetched from + =$engine->initialAttributes->{path_info}=. + * =FOSWIKI_TEST_= prefixed keys of the =env= attribute hash for individual + keys of =*Data= attributes (see =Foswiki::Engine=). For example, + =$engine->pathData->{path_info}= would be set from + =FOSWIKI_TEST_PATH_INFO=. These are used only when corresponding =*Data= + attribute is not initialized at object construction stage. + * Similar to other engines =FOSWIKI_ACTION= might be used if none of the + above sources provides a value for the =$engine->pathData->{action}= + key. + * =FOSWIKI_TEST_QUERY_STRING= is used for setting =queryParameters= + attribute. + +*NOTE* Most of the time constructor parameter and object attribute are the +same thing - see CPAN:Moo. But remember that when 'constructor parameter' +is used it means at this stage attributes are not initialized yet. Or attribute +may differ from constructor parameter value. + +*NOTE* on =__foswikiEngineTestInit=: it is a global default for all newly +created engine objects. So that instead of duplicating a call like this: + + +$engine = Foswiki::Engine::Test->new( + initialAttributes => { + path_info => '/Web/TopicName', + }, +); + + +or in terms of =Unit::FoswikiTestRole= =createNewFoswikiApp= method: + + +# This is way more common throughout the tests code. +$testCase->createNewFoswikiApp( + engineParams => { + initialAttributes => { + path_info => '/Web/TopicName', + }, + }, +); + + +it would be easier to define the parameter once within =set_up= method: + + +around set_up => sub { + my $orig = shift; + my $this = shift; + + $orig->($this, @_); + + $this->app->env->{__foswikiEngineTestInit} = { + initialAttributes => { + path_info => '/Web/TopicName', + }, + }; +}; + + +Since the =env= attribute is cloned by the =createNewFoswikiApp()= method from +previously active application object the key will be propagaded and all newly +created engines will use it as the default. Similar effect would be achieved +by setting =FOSWIKI_TEST_PATH_INFO= key of =env=. But whereas =FOSWIKI_TEST_*= +are used to setup request parameters, =__foswikiEngineTestInit= can define +any engine constructor parameter. For example: + + +$this->app->env->{__foswikiEngineTestInit} = { + simulate => 'cgi', +}; + =cut use Assert; -use Moo; -use namespace::clean; +use Foswiki::Class; extends qw(Foswiki::Engine); +=begin TML + +---++ ObjectAttribute simulate -> [psgi|cgi] + +The only valid values for this attribute are either 'psgi' or 'cgi'. It defines +how this engine communicates with the 'outside' worlds: either in PSGI way by +returning a three element array; or in CGI way by sending HTTP response to +stdout. + +=cut + has simulate => ( is => 'rw', default => 'psgi', @@ -47,6 +126,25 @@ has simulate => ( return $method; }, ); + +=begin TML + +---++ ObjectAttribute initialAttributes + +This is a hashref for initializing =Foswiki::Engine= =*Data= attributes. It is +used by =initFromEnv= method as described in the _Initialization_ section of +this document. + +See =Foswiki::Engine= =pathData=, =connectionData= for the list of of keys. +Additionally the following keys are used: + +| *Key* | *Initialized attribute* | *Comment* | +| =user=, =remote_user= | =user= | =user= takes precedence | +| =postData= | =postData= | What is defined by the key would end up in engine's =postData= attribute as is, with no modification and will be validated by a request object. | +| =headers= | =headers= | Hash of header => value pairs | + +=cut + has initialAttributes => ( is => 'rw', default => sub { { headers => {}, } }, ); around BUILDARGS => sub { @@ -67,8 +165,18 @@ around BUILDARGS => sub { return $orig->( $class, %params ); }; -# Form a data hash using keys either from initialAttributes (higher prio) or -# from env. +=begin TML + +---++ ObjectMethod initFromEnv(@keys) + +Forms a data hash using keys either from initialAttributes (higher prio) or from +=env=. See the _Initialization_ section of this document. Requested keys are +defined by this method arguments. + +Returns a hashref. + +=cut + sub initFromEnv { my $this = shift; my $initAttrs = $this->initialAttributes; @@ -84,7 +192,7 @@ sub initFromEnv { return \%initHash; } -around _preparePath => sub { +around preparePath => sub { my $orig = shift; my $this = shift; @@ -93,7 +201,7 @@ around _preparePath => sub { return $this->initFromEnv(qw(action path_info uri)); }; -around _prepareConnection => sub { +around prepareConnection => sub { my $orig = shift; my $this = shift; @@ -105,7 +213,7 @@ around _prepareConnection => sub { return $connData; }; -around _prepareQueryParameters => sub { +around prepareQueryParameters => sub { my $orig = shift; my $this = shift; @@ -116,7 +224,7 @@ around _prepareQueryParameters => sub { return []; }; -around _prepareHeaders => sub { +around prepareHeaders => sub { my $orig = shift; my $this = shift; @@ -142,20 +250,96 @@ around _prepareHeaders => sub { return $headers; }; -around _prepareUser => sub { +around prepareUser => sub { my $orig = shift; my $this = shift; my $initHash = $this->initFromEnv(qw(user remote_user)); return $initHash->{user} // $initHash->{remote_user}; }; -around _preparePostData => sub { +around preparePostData => sub { my $orig = shift; my $this = shift; my $initHash = $this->initFromEnv(qw(postData)); return $initHash->{postData}; }; +=begin TML + +---++ ObjectMethod mergeAttrs(\%attrs1 [, \%attrs2 [, ...]]) + +Gets a list of hashrefs and deep merges them. The first one is the destination +hash which will contain the result of the merge. + +By deep merge it is meant that any if more than one hash has a key SomeKey at +Nth depth level and the keys in turn contain hashrefs then those hashes are +merged too. + +For example: + + +my %attr1 = ( + A => B, + Lev1 => { + Lev2 => { + A2 => B2, + Lev3 => { + C3 => D3, + } + } + }, +); +my %attr2 = ( + Lev1 => { + A1 => B1, + Lev2 => { + A2 => Bb2, + Lev3 => { + A3 => B3, + } + } + }, +); +my %attr3 = ( + Lev1 => { + A1 => Bb1, + C1 => D1, + Lev2 => { + Lev3 => { + A3 => Bb3, + E3 => F3, + } + } + }, +); + +mergeAttrs(\%attr1, \%attr2, \%attr3); + + +This code will result in the following content in =%attr1=: + +( + A => B, + Lev1 => { + A1 => B1, + C1 => D1, + Lev2 => { + A2 => B2, + Lev3 => { + C3 => D3, + A3 => B3, + E3 => F3, + } + } + } +) + + +Keys =A2= from =%attr2=, =A3= and =A1= from =%attr3= are dropped due to lower +priority of these hashes. + +=cut + sub mergeAttrs { my @hashes = @_; ASSERT( UNIVERSAL::isa( $_, 'HASH' ), @@ -186,6 +370,18 @@ sub mergeAttrs { } } +=begin TML + +---++ ObjectMethod parseURL($queryString) -> \%attrs + +This method parses =$queryString= with complete URL and returns constructor-compatible attribute hash +where =initialAttributes= constructor parameter is initialized from the query string. The following keys +are set: =secure=, =headers= (_Host_ header), =query_string=, and =path_info=. + +Returns attributes hash suitable to be used by the constructor method. + +=cut + sub parseURL { my ($queryString) = @_; diff --git a/UnitTestContrib/lib/Unit/FoswikiTestRole.pm b/UnitTestContrib/lib/Unit/FoswikiTestRole.pm index 4442e859c9..54c5eee6c1 100644 --- a/UnitTestContrib/lib/Unit/FoswikiTestRole.pm +++ b/UnitTestContrib/lib/Unit/FoswikiTestRole.pm @@ -150,6 +150,14 @@ around tear_down => sub { $orig->( $this, @_ ); }; +=begin TML + +---++ ObjectMethod prepareUsersWeb + +Initializer method for =users_web= attribute. + +=cut + sub prepareUsersWeb { return $TEST_WEB_PREFIX . $_[0]->testSuite . 'UsersWeb'; } @@ -443,7 +451,7 @@ s/((\$Foswiki::cfg\{.*?\})\s*=.*?;)(?:\n|$)/push(@moreConfig, $1) unless (eval " =begin TML ---++ ObjectMethod setupDirs -Takes measures as to avoid polluting the base directory with test data and logs. +Takes measures as to avoid polluting the base (=FOSWIKI_HOME=) directory with test data and logs. =cut @@ -555,9 +563,26 @@ sub setupUserRegistration { cleans up the existing Foswiki object, and creates a new one -=%params= are passed directly to the =Foswiki::App= constructor. +=%params= are passed directly to the application object constructor. Note that for most test cases this would be =Unit::TestApp= class instead of its parent =Foswiki::App=. -typically called to force a full re-initialisation either with new preferences, topics, users, groups or CFG +Typically called to force a full re-initialisation with new preferences, topics, users, groups or config. + +A new application is created with environment and config (parameters =env= and +=cfg= respectively) being cloned from the currently active application object +unless corresponding parameters are supplied by user. The new application object +is completely replacing the previously active one by being stored in +=$Foswiki::app= and test case =app= attribute. + +In addition this method traverses down the data structure of the current object +by looking for blessed references and checks if they're suspects to have an +=rwp= =app= attribute. If found then the attribute is set to point to the new +application with =_set_app()= method. + +*NOTE* No responsibility is taken if a reference to the application is stored in +any other attribute or object's hash key. To make sure that a instance of your +class is handled properly by this core apply =Foswiki::AppObject= role. + +See also: =Unit::TestApp= =cut @@ -573,6 +598,7 @@ sub _fixupAppObjects { blessed( $this->{$attr} ) && $this->$attr->isa('Foswiki::Object') && $this->$attr->can('_set_app') + && $this->$attr->can('app') && ( !defined( $this->$attr->app ) || ( $this->$attr->app != $app ) ) ) @@ -591,7 +617,8 @@ sub createNewFoswikiApp { $app->cfg->data->{Store}{Implementation} ||= 'Foswiki::Store::PlainFile'; $params{env} //= $app->cloneEnv; - my $newApp = Unit::TestApp->new( cfg => $app->cfg->clone, %params ); + $params{cfg} //= $app->cfg->clone; + my $newApp = Unit::TestApp->new(%params); $this->app($newApp); $this->_fixupAppObjects; @@ -606,6 +633,7 @@ sub createNewFoswikiApp { } =begin TML + ---++ ObjectMethod testWebName($baseName) -> $webName Returns a standard test web name formed with test suite name and =$baseName=. @@ -733,6 +761,7 @@ was called. =cut +# SMELL Make it support Unit::Leak::Object sub leakDetectCheckpoint { my $this = shift; my ($dumpName) = @_; @@ -762,6 +791,7 @@ Do nothing unless =Unit::TestRunner::CHECKLEAK= is true. =cut +# SMELL Make it support Unit::Leak::Object sub leakDetectDump { my $this = shift; my ($dumpName) = @_; diff --git a/UnitTestContrib/lib/Unit/TestApp.pm b/UnitTestContrib/lib/Unit/TestApp.pm index 7ef5a65271..af4f941cf2 100644 --- a/UnitTestContrib/lib/Unit/TestApp.pm +++ b/UnitTestContrib/lib/Unit/TestApp.pm @@ -3,6 +3,20 @@ package Unit::TestApp; use v5.14; +=begin TML + +---+ Class Unit::TestApp + +This is the default application class for unit tests. It provides additional +functionality to support testing while offloading it from the parent +=Foswiki::App= class to reduce %WIKITOOLNAME% memory footprint and probably +avoid some slowdown by handling cases which are not gonna be met outside of +the testing environment. + +Alongside to this class one must also study =Foswiki::Engine::Test=. + +=cut + use Assert; use Scalar::Util qw(blessed weaken refaddr); @@ -11,37 +25,71 @@ use Try::Tiny; use Foswiki::Class qw(callbacks); extends qw(Foswiki::App); +=begin TML + +---++ Callbacks + +This class defines the following callbacks (see =Foswiki::Aux::Callbacks=): + +| *name* | *Description* | +| =testPreHandleRequest= | Executed before control is passed over to =Foswiki::App= =handleRequest()= method. | +| =testPostHandleRequest= | Executed right after =Foswiki::App= =handleRequest()= method finishes. | + +See [[#AttrCallbacks][=callbacks= attribute]]. + +---+++ Callback testPreHandleRequest + +No =params= are sent to the handler. + +---+++ Callback testPostHandleRequest + +=params= contains one key: =rc= with =handleRequest= method return value. + +=cut + callback_names qw(testPreHandleRequest testPostHandleRequest); -#with qw(Foswiki::Aux::Localize); - -#sub setLocalizableAttributes { -# return -# qw( -# access attach cache cfg env forms -# logger engine heap i18n plugins prefs -# renderer request requestParams response -# search store templates macros context -# ui remoteUser user users zones _dispatcherAttrs -# ) -# ; -#} - -# requestParams hash is used to initialize a new request object. +=begin TML + +---++ ObjectAttribute requestParams -> hash + +This is a hash of parameters to be passed over to =Foswiki::Request= +constructor. + +=cut + has requestParams => ( is => 'rwp', lazy => 1, default => sub { {} }, ); -# engineParams hash is used to initialize a new engine object. +=begin TML + +---++ ObjectAttribute engineParams -> hash + +This is a hash of parameters to be passed over to =Foswiki::Engine= +constructor. + +=cut + has engineParams => ( is => 'rw', lazy => 1, default => sub { {} }, ); -# Hash of the test callbacks to be registered on the app object. +=begin TML + +#AttrCallbacks +---++ ObjectAttribute callbacks -> hash + +A hash of =callback => \&handler= pairs. Handlers are registered for their +respective callbacks. Each handler =data= parameter is a hash whith the only +key =app= containing reference to the application object. + +=cut + has callbacks => ( is => 'rw', lazy => 1, @@ -156,13 +204,82 @@ around handleRequest => sub { return $rc; }; -1; +=begin TML + +---++ Examples + +---+++ A test case code + +This code demonstrates a sample case of testing a request. Take a note that +tests are using =Foswiki::Engine::Test= engine. + +What is demonstrated here is: -__DATA__ + * Handling of application's internal exceptions. Useful for cases when we expect an exception and test success depends on it. It would be then easier to get the exception itself instead of analyzing HTML output. + * Passing of new application parameters via =createNewFoswikiApp= method. + * Defining basic request parameters as engine constructor parameters. + + +sub _cbHRE { + my $obj = shift; + my %args = @_; + $args{params}{exception}->rethrow; +} + +sub test_someTest { + my $this = shift; + + ... + + $this->createNewFoswikiApp( + requestParams => { + initializer => { + templatetopic => $this->test_web . ".TemplateTopic", + }, + }, + engineParams => { + initialAttributes => { + path_info => "/" . $this->test_web . "/" . $this->test_topic, + user => $this->app->cfg->data->{AdminUserLogin}, + action => 'view', + method => 'GET', + }, + simulate => 'psgi', + }, + callbacks => { handleRequestException => \&_cbHRE, }, + ); + + try { + my ($text) = $this->capture( + sub { + return $this->app->handleRequest; + } + ); + } catch { + my $e = Foswiki::Exception::Fatal->transmute($_, 0); + + # Handle any application exception here. + unless ( $e->isa('Foswiki::OopsException') ) { + # Assume that we expected an oops. + $e->rethrow; + } + + ... + } +} + + +---++ See also + +=Unit::FoswikiTestRole=, =Foswiki::Engine::Test= + +=cut + +1; -Author: Crawford Currie, http://c-dot.co.uk +__END__ -Copyright (C) 2007-2016 Foswiki Contributors +Copyright (C) 2016 Foswiki Contributors All Rights Reserved. This program is free software; you can redistribute it and/or diff --git a/UnitTestContrib/lib/Unit/TestCase.pm b/UnitTestContrib/lib/Unit/TestCase.pm index 27966b379c..8bef88bc10 100644 --- a/UnitTestContrib/lib/Unit/TestCase.pm +++ b/UnitTestContrib/lib/Unit/TestCase.pm @@ -20,6 +20,8 @@ use Foswiki::Exception (); use Text::Diff (); require File::Temp; +use Assert; + use Foswiki::Class; extends 'Foswiki::Object'; @@ -269,7 +271,10 @@ sub assert { return 1 if $bool; $mess ||= "Assertion failed"; $mess = join( "\n", @{ $this->annotations } ) . "\n" . $mess; - $mess = Carp::longmess($mess); + + # When DEBUG is on don't use long message format because the exception will + # have the stack trace. + $mess = DEBUG ? $mess : Carp::longmess($mess); Foswiki::Exception::ASSERT->throw( text => $mess, object => $this ); } diff --git a/UnitTestContrib/test/unit/AddressTests.pm b/UnitTestContrib/test/unit/AddressTests.pm index 2487fe9c06..6d4c978a6d 100644 --- a/UnitTestContrib/test/unit/AddressTests.pm +++ b/UnitTestContrib/test/unit/AddressTests.pm @@ -5,8 +5,7 @@ use Data::Dumper; use Benchmark qw(:hireswallclock); use constant TRACE => 0; -use Moo; -use namespace::clean; +use Foswiki::Class; extends 'FoswikiTestCase'; my $test_web = 'Temporary' . __PACKAGE__ . 'TestWeb'; diff --git a/UnitTestContrib/test/unit/NetTests.pm b/UnitTestContrib/test/unit/NetTests.pm index 679ff33ff7..3c30509c96 100644 --- a/UnitTestContrib/test/unit/NetTests.pm +++ b/UnitTestContrib/test/unit/NetTests.pm @@ -4,7 +4,7 @@ use v5.14; use Foswiki::Net; -use Moo; +use Foswiki::Class; extends qw( FoswikiTestCase ); our $expectedHeader; diff --git a/core/lib/Foswiki/AccessControlException.pm b/core/lib/Foswiki/AccessControlException.pm index 508a58b1a4..bbab7c8ca5 100644 --- a/core/lib/Foswiki/AccessControlException.pm +++ b/core/lib/Foswiki/AccessControlException.pm @@ -118,12 +118,13 @@ Generate a summary string. This is mainly for debugging. =cut -around stringify => sub { - my $orig = shift; - my $this = shift; +sub _accessText { + my $this = shift; + my $topic = $this->topic || ''; # Access checks of Web objects causes uninitialized string errors my $web = $this->web // '*UNDEF*'; + return "AccessControlException: Access to " . $this->mode . " " @@ -131,8 +132,20 @@ around stringify => sub { . ".$topic for " . $this->user . " is denied. " - . $this->reason - . ( DEBUG ? $this->stacktrace : "" ); + . $this->reason; +} + +around stringify => sub { + my $orig = shift; + my $this = shift; + return $this->_accessText . ( DEBUG ? $this->stacktrace : "" ); +}; + +around prepareText => sub { + my $orig = shift; + my $this = shift; + + return $this->_accessText; }; 1; diff --git a/core/lib/Foswiki/Config.pm b/core/lib/Foswiki/Config.pm index 8fae337fde..16411d9c9d 100644 --- a/core/lib/Foswiki/Config.pm +++ b/core/lib/Foswiki/Config.pm @@ -117,17 +117,76 @@ Keeps the name of the failed config or spec file. =cut -has failedConfig => ( is => 'rw', ); +has failedConfig => ( is => 'rw', ); + +=begin TML + +---++ ObjectAttribute bootstrapMessage + +If there is something to inform user about bootstrapping stage – the message +will be here. + +=cut + has bootstrapMessage => ( is => 'rw', ); -has noExpand => ( is => 'rw', default => 0, ); -has noSpec => ( is => 'rw', default => 0, ); -has configSpec => ( is => 'rw', default => 0, ); -has noLocal => ( is => 'rw', default => 0, ); + +=begin TML + +---++ ObjectAttribute noExpand -> Bool + +Default for =readConfig()= method =$noExpand= parameter when called by +constructor. Not used otherwise. + +See [[#ObjectMethodNew][constructor new()]]. + +=cut + +has noExpand => ( is => 'rw', default => 0, ); + +=begin TML + +---++ ObjectAttribute noSpec -> Bool + +Default for =readConfig()= method =$noSpec= parameter when called by +constructor. Not used otherwise. + +See [[#ObjectMethodNew][constructor new()]]. + +=cut + +has noSpec => ( is => 'rw', default => 0, ); + +=begin TML + +---++ ObjectAttribute configSpec -> Bool + +Default for =readConfig()= method =$configSpec= parameter when called by +constructor. Not used otherwise. + +See [[#ObjectMethodNew][constructor new()]]. + +=cut + +has configSpec => ( is => 'rw', default => 0, ); + +=begin TML + +---++ ObjectAttribute noLocal -> Bool + +Default for =readConfig()= method =$noLocal= parameter when called by +constructor. Not used otherwise. + +See [[#ObjectMethodNew][constructor new()]]. + +=cut + +has noLocal => ( is => 'rw', default => 0, ); # Configuration shortcut attributes. =begin TML +#ObjectMethodNew ---++ ClassMethod new([noExpand => 0/1][, noSpec => 0/1][, configSpec => 0/1][, noLoad => 0/1]) * =noExpand= - suppress expansion of $Foswiki vars embedded in @@ -232,7 +291,7 @@ around doLocalize => sub { =begin TML ----++ ObjectMethod readConfig +---++ ObjectMethod readConfig( $noExpand, $noSpec, $configSpec, $noLocal ) In normal Foswiki operations as a web server this method is called by the =BEGIN= block of =Foswiki.pm=. However, when benchmarking/debugging it can be diff --git a/core/lib/Foswiki/Contrib/core/DEPENDENCIES b/core/lib/Foswiki/Contrib/core/DEPENDENCIES index 58b42af62f..e3d11f7c17 100755 --- a/core/lib/Foswiki/Contrib/core/DEPENDENCIES +++ b/core/lib/Foswiki/Contrib/core/DEPENDENCIES @@ -16,7 +16,6 @@ DBD::Pg,>=0,cpan,Optional Foswiki Page Cache using PostgreSQL DBD::SQLite,>=0,cpan,Optional Foswiki Page Cache using SQLite DBD::mysql,>=0,cpan,Optional Foswiki Page Cache using MySQL DBI,>=0,cpan,Optional Foswiki Page Cache -Devel::Leak,>=0.03,cpan,Optional for tracing down memory leaks. Digest::SHA,>=0,cpan,Optional, may be required for password encryption. Email::MIME,>=1.903,cpan,Required for correct handling of email MIME structure. Email::Simple,>=2.206,cpan,Required for compatibility with Email::MIME. @@ -35,6 +34,7 @@ Locale::Msgfmt,>=0,cpan,Optional, used to compress the language files in locale Moo,>=2.002004,cpan,Required, the OO framework core. Mozilla::CA,>=20110904,cpan,Optional, SSL host verification for e-mail and other SSL/TLS connections. Plack,>=1.4.200,cpan,Required, PSGI support +Plack::Test,>=0,cpan,Optional, but needed by tests. Socket,>=2.001,cpan,Required, for base Foswiki. Throwable,>=0.2,cpan,Required for exception handling Try::Tiny,>=0.27,cpan,Required for expections handling diff --git a/core/lib/Foswiki/Engine.pm b/core/lib/Foswiki/Engine.pm index 3b8152a388..3ce16d8a7c 100644 --- a/core/lib/Foswiki/Engine.pm +++ b/core/lib/Foswiki/Engine.pm @@ -2,13 +2,14 @@ =begin TML ----+!! package Foswiki::Engine +---+!! Class Foswiki::Engine -The engine class is a singleton that implements details about Foswiki's -execution mode. This is the base class and implements basic behavior. +Engine is a mediator between the 'outside' world (i.e. – user side browser or a test +unit code) and %WIKITOOLNAME% core; in particular – =Foswiki::Request= object. -Each engine should inherits from this and overload methods necessary -to achieve correct behavior. +This is the base class and implements only some basic functionality. Each engine +should inherit from this and overload methods necessary to achieve correct +behavior. =cut @@ -25,11 +26,31 @@ extends qw(Foswiki::Object); use constant HTTP_COMPLIANT => undef; # This is a generic class. +=begin TML + +---++ ObjectAttribute env -> hash + +Hashref of environment variables. Depending on engine might be fetched from +different sources. For example, for PSGI it's application sub argument. + +Default: application object =env= attribute. + +=cut + has env => ( is => 'rw', isa => Foswiki::Object::isaHASH( 'env', noUndef => 1, ), default => sub { $_[0]->app->env }, ); + +=begin TML + +---++ ObjectMethod gzipAccepted -> bool + +True if client accepts gzip compression. + +=cut + has gzipAccepted => ( is => 'ro', lazy => 1, @@ -50,19 +71,23 @@ has gzipAccepted => ( =begin TML ----++ ObjectAttribute pathData +---++ ObjectAttribute pathData -> hash + +pathData attribute is a hash with the following keys: -pathData attribute is a hash with the following keys: =action=, =path_info=, =uri=. + * =action= + * =path_info= + * =uri= The =uri= key can be undef under certain circumstances. =cut -has pathData => ( is => 'rw', lazy => 1, builder => '_preparePath', ); +has pathData => ( is => 'rw', lazy => 1, builder => 'preparePath', ); =begin TML ----++ ObjectAttribute connectionData +---++ ObjectAttribute connectionData -> hash connectionData attribute is a hash with the following keys: @@ -75,7 +100,7 @@ connectionData attribute is a hash with the following keys: =cut has connectionData => - ( is => 'rw', lazy => 1, builder => '_prepareConnection', ); + ( is => 'rw', lazy => 1, builder => 'prepareConnection', ); =begin TML @@ -91,13 +116,13 @@ has queryParameters => ( is => 'rw', lazy => 1, isa => Foswiki::Object::isaARRAY( 'queryParameters', noUndef => 1 ), - builder => '_prepareQueryParameters', + builder => 'prepareQueryParameters', ); has bodyParameters => ( is => 'rw', lazy => 1, isa => Foswiki::Object::isaARRAY( 'bodyParameters', noUndef => 1 ), - builder => '_prepareBodyParameters', + builder => 'prepareBodyParameters', ); =begin TML @@ -111,7 +136,7 @@ Containts raw, non-decoded, POST data. has postData => ( is => 'ro', lazy => 1, - builder => '_preparePostData', + builder => 'preparePostData', ); =begin TML @@ -123,7 +148,7 @@ Hash of =$filename => \%uploadInfo= pairs. =cut has uploads => - ( is => 'rw', lazy => 1, clearer => 1, builder => '_prepareUploads', ); + ( is => 'rw', lazy => 1, clearer => 1, builder => 'prepareUploads', ); =begin TML @@ -152,7 +177,7 @@ Suggested username. =cut -has user => ( is => 'rw', lazy => 1, builder => '_prepareUser', ); +has user => ( is => 'rw', lazy => 1, builder => 'prepareUser', ); =begin TML @@ -162,9 +187,10 @@ Hashref of headers. =cut -has headers => ( is => 'rw', lazy => 1, builder => '_prepareHeaders', ); +has headers => ( is => 'rw', lazy => 1, builder => 'prepareHeaders', ); =begin TML + ---++ ClassMethod start(env => \%env) Determines the type of environment we're running in and creates an instance of @@ -211,142 +237,17 @@ sub start { =begin TML ----++ ClassMethod new() -> $engine - -Constructs an engine object. - -=cut - -=begin TML - ----++ 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 ); -# } -#} - -=begin TML - ----++ 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. - -The actual request object is created in preparePath. - -=cut - -sub __deprecated_prepare { - my $this = shift; - my $req; - - if ( $Foswiki::cfg{Store}{overrideUmask} && $Foswiki::cfg{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) - ( - ( - $Foswiki::cfg{Store}{dirPermission} + 0 | - $Foswiki::cfg{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"; - } - - try { - $req = $this->create('Foswiki::Request'); - $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') - || $e->isa('Foswiki::Exception::Engine') ) - { - my $res = $e->response; - unless ( defined $res ) { - $res = $this->create('Foswiki::Response'); - $res->header( -type => 'text/html', -status => $e->status ); - my $html = CGI::start_html( $e->status . ' Bad Request' ); - $html .= CGI::h1( {}, 'Bad Request' ); - $html .= CGI::p( {}, $e->reason ); - $html .= CGI::end_html(); - $res->print($html); - } - $this->finalizeError( $res, $req ); - return $e->status; - } - else { # Not Foswiki::EngineException - my $res = $this->create('Foswiki::Response'); - my $mess = - $e->can('stringify') - ? $e->stringify() - : 'Unknown ' . ref($e) . ' exception: ' . $@; - $res->header( -type => 'text/plain', -status => '500' ); - if (DEBUG) { - - # output the full message and stacktrace to the browser - $res->print($mess); - } - else { - print STDERR $mess; - - # tell the browser where to look for more help - my $text = -'Foswiki detected an internal error - please check your Foswiki logs and webserver logs for more information.' - . "\n\n"; - $mess =~ s/ at .*$//s; - - # cut out pathnames from public announcement - $mess =~ s#/[\w./]+#path#g; - $text .= $mess; - $res->print($text); - } - $this->finalizeError( $res, $req ); - return 500; # Internal server error - } - }; - return $req; -} - -=begin TML - ----++ ObjectMethod _prepareConnection +---++ ObjectMethod prepareConnection Initializer method of =connectionData= attribute. =cut -sub _prepareConnection { } +# SMELL Must be non-private as well as other initializers. +sub prepareConnection { } # Initializer for queryParameters attribute. -sub _prepareQueryParameters { +sub prepareQueryParameters { my $this = shift; my ($queryString) = @_; @@ -374,17 +275,17 @@ sub _prepareQueryParameters { =begin TML ----++ ObjectMethod _prepareHeaders +---++ ObjectMethod prepareHeaders Abstract initializer for the =headers= object attribute. =cut -sub _prepareHeaders { return {}; } +sub prepareHeaders { return {}; } =begin TML ----++ ObjectMethod _prepareUser +---++ ObjectMethod prepareUser Initializer for the =user= object attribute. @@ -393,33 +294,30 @@ Returns =$req= - Foswiki::Request object, populated with the action and the path =cut -sub _prepareUser { return shift->env->{REMOTE_USER}; } +sub prepareUser { return shift->env->{REMOTE_USER}; } =begin TML ----++ ObjectMethod _preparePostData +---++ ObjectMethod preparePostData Abstract initializer for the =postData= object attribute. =cut -sub _preparePostData { } +sub preparePostData { } =begin TML ----++ ObjectMethod _preparePath( ) +---++ ObjectMethod preparePath( ) Initializer method of =pathData=. =cut -sub _preparePath { } +sub preparePath { } # Abstract initializer for bodyParameters -sub _prepareBodyParameters { return []; } - -# Abstract initializer for uploads -sub _prepareUploads { return []; } +sub prepareBodyParameters { return []; } =begin TML @@ -427,18 +325,18 @@ sub _prepareUploads { return []; } Abstract method, must be defined by inherited classes. -Should fill $req's {uploads} field. This is a hashref whose keys are -upload names and values Foswiki::Request::Upload objects. - Implementations must convert upload names to unicode. =cut -sub prepareUploads { } +# Abstract initializer for uploads +sub prepareUploads { return []; } =begin TML ----++ ObjectMethod stringifyHeaders(\@psgiReturnArray) => $headersText +---++ ObjectMethod stringifyHeaders(\@psgiReturnArray) -> $headersText + +Converts headers from PSGI format to string in HTTP response format. =cut @@ -459,7 +357,7 @@ sub stringifyHeaders { =begin TML ----++ ObjectMethod stringifyBody(\@psgiReturnArray) => $bodyText +---++ ObjectMethod stringifyBody(\@psgiReturnArray) -> $bodyText =cut @@ -501,7 +399,7 @@ sub _writeBody { =begin TML ----++ ObjectMethod finalizeReturn(\@rc) => $rc +---++ ObjectMethod finalizeReturn(\@rc) -> $rc Abstract method, must be defined by inherited classes. diff --git a/core/lib/Foswiki/Engine/CGI.pm b/core/lib/Foswiki/Engine/CGI.pm index d012437853..dc637a3428 100644 --- a/core/lib/Foswiki/Engine/CGI.pm +++ b/core/lib/Foswiki/Engine/CGI.pm @@ -2,7 +2,7 @@ =begin TML ----+!! package Foswiki::Engine::CGI +---+!! Class Foswiki::Engine::CGI Class that implements default CGI behavior. @@ -167,7 +167,7 @@ sub probe { # }; #}; -around _prepareConnection => sub { +around prepareConnection => sub { my $orig = shift; my $this = shift; @@ -186,14 +186,14 @@ around _prepareConnection => sub { # XXX The base class method now fetches QUERY_STRING on its own. So, why wasting # time on extra call? -#around _prepareQueryParameters => sub { +#around prepareQueryParameters => sub { # my $orig = shift; # my ( $this, $req ) = @_; # $orig->( $this, $req, $this->env->{QUERY_STRING} ) # if $this->env->{QUERY_STRING}; #}; -around _prepareHeaders => sub { +around prepareHeaders => sub { my $orig = shift; my $this = shift; my $headers = $orig->($this); @@ -205,13 +205,13 @@ around _prepareHeaders => sub { return $headers; }; -around _prepareUser => sub { +around prepareUser => sub { my $orig = shift; my $this = shift; return $this->env->{REMOTE_USER}; }; -around _preparePath => sub { +around preparePath => sub { my $orig = shift; my ($this) = @_; @@ -280,7 +280,7 @@ around _preparePath => sub { }; }; -sub _prepareCGI { +sub prepareCGI { my ($this) = @_; return unless $this->env->{CONTENT_LENGTH}; @@ -301,7 +301,7 @@ sub _prepareCGI { return $cgi; } -around _prepareBodyParameters => sub { +around prepareBodyParameters => sub { my $orig = shift; my $this = shift; @@ -348,7 +348,7 @@ around prepareUploads => sub { $req->uploads( \%uploads ); }; -around _preparePostData => sub { +around preparePostData => sub { my $orig = shift; my $this = shift; diff --git a/core/lib/Foswiki/Engine/CLI.pm b/core/lib/Foswiki/Engine/CLI.pm index bf9ddc7e5a..a00e1231c5 100644 --- a/core/lib/Foswiki/Engine/CLI.pm +++ b/core/lib/Foswiki/Engine/CLI.pm @@ -2,7 +2,7 @@ =begin TML ----+!! package Foswiki::Engine::CLI +---+!! Class Foswiki::Engine::CLI Class that implements CGI scripts functionality when called from command line or cron job @@ -62,7 +62,7 @@ around BUILDARGS => sub { return $orig->( $class, %params ); }; -around _prepareConnection => sub { +around prepareConnection => sub { my $orig = shift; my $this = shift; return { @@ -71,7 +71,7 @@ around _prepareConnection => sub { }; }; -around _prepareQueryParameters => sub { +around prepareQueryParameters => sub { my $orig = shift; my ( $this, $req ) = @_; my @params; @@ -85,7 +85,7 @@ around _prepareQueryParameters => sub { # This initializer will be called only when no =user= parameter is set by the # object constructor. -around _prepareUser => sub { +around prepareUser => sub { my $orig = shift; my $this = shift; my $user = $orig->($this); @@ -100,7 +100,7 @@ around _prepareUser => sub { return $user; }; -around _preparePath => sub { +around preparePath => sub { my $orig = shift; my ($this) = @_; my $env = $this->env; diff --git a/core/lib/Foswiki/Engine/PSGI.pm b/core/lib/Foswiki/Engine/PSGI.pm index 03a84c8bf0..1355cb80fc 100644 --- a/core/lib/Foswiki/Engine/PSGI.pm +++ b/core/lib/Foswiki/Engine/PSGI.pm @@ -2,7 +2,7 @@ =begin TML ----+!! package Foswiki::Engine::PSGI +---+!! Class Foswiki::Engine::PSGI Class that implements default PSGI behavior. @@ -41,7 +41,7 @@ sub probe { return defined $params{env}{'psgi.version'}; } -around _prepareConnection => sub { +around prepareConnection => sub { my $orig = shift; my $this = shift; @@ -55,7 +55,7 @@ around _prepareConnection => sub { }; }; -around _prepareHeaders => sub { +around prepareHeaders => sub { my $orig = shift; my $this = shift; @@ -65,13 +65,13 @@ around _prepareHeaders => sub { return \%hdrHash; }; -around _prepareUser => sub { +around prepareUser => sub { my $orig = shift; my $this = shift; return $this->psgi->user; }; -around _preparePath => sub { +around preparePath => sub { my $orig = shift; my ($this) = @_; @@ -132,7 +132,7 @@ around _preparePath => sub { }; }; -around _prepareBodyParameters => sub { +around prepareBodyParameters => sub { my $orig = shift; my $this = shift; @@ -154,7 +154,7 @@ around _prepareBodyParameters => sub { return \@params; }; -around _prepareQueryParameters => sub { +around prepareQueryParameters => sub { my $orig = shift; my $this = shift; @@ -174,13 +174,13 @@ around _prepareQueryParameters => sub { return \@params; }; -around _preparePostData => sub { +around preparePostData => sub { my $orig = shift; my $this = shift; return $this->psgi->raw_body; }; -around _prepareUploads => sub { +around prepareUploads => sub { my $orig = shift; my ( $this, $req ) = @_; diff --git a/core/lib/Foswiki/Exception.pm b/core/lib/Foswiki/Exception.pm index d30de90fbb..7d7e8eae2d 100644 --- a/core/lib/Foswiki/Exception.pm +++ b/core/lib/Foswiki/Exception.pm @@ -57,7 +57,7 @@ use Foswiki::Class; extends qw(Foswiki::Object); with 'Throwable'; -use overload '""' => 'stringify'; +use overload '""' => 'to_str'; our $EXCEPTION_TRACE = 0; @@ -185,6 +185,16 @@ sub stringify { ); } +sub to_str { + my $this = shift; + + my $boundary = '-' x 60; + my $msg = join( "\n", + $boundary, map( { " " . $_ } split /\n/, $this->stringify ), + $boundary ); + return $msg; +} + # We must not get into this. But if we do then let's not hide a error but let it # thru to the end user via JsonRPC interfaces. sub TO_JSON { @@ -402,12 +412,6 @@ use Assert; use Foswiki::Class; extends qw(Foswiki::Exception); -sub BUILD { - my $this = shift; - - say STDERR $this->stringify, $this->stacktrace if DEBUG; -} - # To cover perl/system errors. package Foswiki::Exception::ASSERT; diff --git a/core/lib/Foswiki/IncludeHandlers/doc.pm b/core/lib/Foswiki/IncludeHandlers/doc.pm index f642f1b291..ee8197b2da 100644 --- a/core/lib/Foswiki/IncludeHandlers/doc.pm +++ b/core/lib/Foswiki/IncludeHandlers/doc.pm @@ -54,7 +54,7 @@ sub INCLUDE { return '' unless $pmfile; my $PMFILE; - open( $PMFILE, '<', $pmfile ) || return ''; + open( $PMFILE, '<:utf8', $pmfile ) || return ''; my $inPod = 0; my $pod = ''; my $howSmelly = 0; diff --git a/core/lib/Foswiki/Net.pm b/core/lib/Foswiki/Net.pm index 24234e7504..e06810aa38 100644 --- a/core/lib/Foswiki/Net.pm +++ b/core/lib/Foswiki/Net.pm @@ -286,6 +286,8 @@ sub getExternalResource { eval 'require HTTP::Response' unless ($noHTTPResponse); if ( $@ || $noHTTPResponse ) { + Foswiki::load_class('Foswiki::Net::HTTPResponse'); + # Nope, no HTTP::Response, have to do things the hard way :-( $response = Foswiki::Net::HTTPResponse->parse($result); } diff --git a/core/lib/Foswiki/Request.pm b/core/lib/Foswiki/Request.pm index d3e070e77e..7b532ce2c5 100644 --- a/core/lib/Foswiki/Request.pm +++ b/core/lib/Foswiki/Request.pm @@ -1250,9 +1250,15 @@ sub _establishParamList { sub _establishWeb { my $this = shift; - return ( $this->_pathParsed->{web} - || $this->param('defaultweb') - || $this->app->cfg->data->{UsersWebName} ); + + if ( $this->_pathParsed->{web} ) { + return $this->_pathParsed->{web}; + } + elsif ( $this->param('defaultweb') ) { + return Foswiki::Sandbox::untaint( $this->param('defaultweb'), + \&Foswiki::Sandbox::validateWebName ); + } + return $this->app->cfg->data->{UsersWebName}; } sub _establishTopic { @@ -1262,8 +1268,13 @@ sub _establishTopic { } sub _establishMethod { - my $this = shift; - return $this->app->engine->connectionData->{method}; + my $this = shift; + my $method = $this->app->engine->connectionData->{method}; + + # Call it manually because triggers are not called for default/build. + $this->_trigger_method($method); + + return $method; } sub _establishUploads { diff --git a/core/lib/Foswiki/Request/Rest.pm b/core/lib/Foswiki/Request/Rest.pm index 4d5a4fb8d4..cc451fac28 100644 --- a/core/lib/Foswiki/Request/Rest.pm +++ b/core/lib/Foswiki/Request/Rest.pm @@ -49,7 +49,7 @@ has invalidVerb => ( =begin TML ----++ private objectMethod _establishAddress() -> n/a +---++ private objectMethod _establishAttributes() -> n/a Used internally by the web(), topic() and attachment() methods to trigger parsing of the url and/or topic= parameter and set object variables with the results. Attachment requests have to also accommodate redirect requests diff --git a/core/locale/uk.po b/core/locale/uk.po index 5b23e20b3e..92f746ac99 100644 --- a/core/locale/uk.po +++ b/core/locale/uk.po @@ -6,15 +6,15 @@ msgstr "" "Project-Id-Version: Foswiki $ld\n" "Report-Msgid-Bugs-To: foswiki-svn@lists.sourceforge.net\n" "POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2016-09-06 17:51+0200\n" +"PO-Revision-Date: 2016-10-15 01:45+0200\n" "Last-Translator: Vadim Belman \n" "Language-Team: \n" "Language: uk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" -"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=" +"4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "X-Generator: Weblate 2.4\n" "X-Poedit-Language: Ukrainian\n" "X-Poedit-Country: UKRAINE\n" @@ -665,14 +665,12 @@ msgid "Actions" msgstr "Дії" #: core/templates/registermessages.tmpl:91 -#, fuzzy msgid "Activation Failed" -msgstr "Невірний код активації" +msgstr "Невдала активація" #: core/templates/registermessages.tmpl:92 -#, fuzzy msgid "Activation for code %1 failed with an internal error." -msgstr "Операція %1 не вдалась через внутрішню помилку" +msgstr "Внутрішня помилка активації за кодом %1." #: JQueryPlugin/data/System/JQueryButton.txt:29 #: PatternSkin/data/System/PatternSkinElements.txt:452 @@ -4310,13 +4308,12 @@ msgstr "" "цільова тема вже містить додаток з таким ім'ям." #: core/templates/renameattachmentdelete.tmpl:43 -#, fuzzy msgid "" "The attachment may have to be renamed if an attachment called '%1' already " "exists in the target topic." msgstr "" -"Додаток можливо доведеться перейменувати якщо цільова тема вже має додаток з " -"іменем '%FILENAME%'." +"Можливо треба перейменувати додаток якщо цільова тема вже має додаток з " +"іменем '%1'." #: core/data/System/FAQWhyYouAreAskedToConfirm.txt:6 msgid "" @@ -4662,6 +4659,8 @@ msgid "" "This is an internal error. You can try to register again, or contact %1 to " "report the error." msgstr "" +"Це внутрішня помилка. Ви можете спробувати зареєструватись знову або " +"зв'язатись із %1 що повідомити про проблему." #: TopicUserMappingContrib/data/System/UserRegistrationParts.txt:128 msgid "This is not a valid WikiName."