diff --git a/core/bin/run_app b/core/bin/run_app new file mode 100755 index 0000000000..52d0c0e3c9 --- /dev/null +++ b/core/bin/run_app @@ -0,0 +1,5 @@ +#!env perl +use Cwd; +use lib Cwd::abs_path("../lib"); +use Foswiki::App; +exit Foswiki::App->run; diff --git a/core/lib/Foswiki/App.pm b/core/lib/Foswiki/App.pm index 51ac8ebb02..38ba0fc278 100644 --- a/core/lib/Foswiki/App.pm +++ b/core/lib/Foswiki/App.pm @@ -4,7 +4,7 @@ package Foswiki::App; use v5.14; use Cwd; - +use Try::Tiny; use Foswiki::Config; use Moo; @@ -68,11 +68,15 @@ sub BUILD { my $this = shift; my $params = shift; - if ( $this->cfg->{isBOOTSTRAPPING} ) { + $Foswiki::app = $this; - #code + unless ( defined $this->engine ) { + Foswiki::Exception::Fatal->throw( text => "Cannot initialize engine" ); } + unless ( $this->cfg->data->{isVALID} ) { + $this->cfg->bootstrap; + } } =begin TML @@ -106,17 +110,21 @@ sub run { $params{env}{PWD} //= getcwd; try { - $app = $Foswiki::app = Foswiki::App->new(%params); + $app = Foswiki::App->new(%params); $app->handleRequest; } catch { my $e = $_; + unless ( ref($e) && $e->isa('Foswiki::Exception') ) { + $e = Foswiki::Exception->transmute($e); + } + # Low-level report of errors to user. - if ( $app && $app->engine ) { + if ( defined $app && defined $app->engine ) { # TODO Send error output to user using the initialized engine. - ...; + $app->engine->write( $e->stringify ); } else { # Propagade the error using the most primitive way. @@ -126,7 +134,31 @@ sub run { } sub handleRequest { + my $this = shift; + + my $res = Foswiki::UI::handleRequest( $this->request ); + $this->engine->finalize( $res, $this->request ); +} + +=begin TML + +--++ ObjectMethod create($className, %initArgs) + +Similar to =Foswiki::AppObject::create()= method but for the =Foswiki::App= +itself. + +=cut + +sub create { + my $this = shift; + my $class = shift; + + unless ( $class->isa('Foswiki::AppObject') ) { + Foswiki::Exception::Fatal->throw( + text => "Class $class is not a Foswiki::AppObject descendant." ); + } + return $class->new( app => $this, @_ ); } sub _prepareEngine { @@ -136,7 +168,7 @@ sub _prepareEngine { # Foswiki::Engine has to determine what environment are we run within and # return an object of corresponding class. - $engine = Foswiki::Engine->start( env => $env ); + $engine = Foswiki::Engine::start( env => $env ); return $engine; } @@ -149,7 +181,7 @@ sub _prepareRequest { sub _readConfig { my $this = shift; - my $cfg = Foswiki::Config->new( env => $this->env ); + my $cfg = $this->create( 'Foswiki::Config', env => $this->env ); return $cfg; } diff --git a/core/lib/Foswiki/AppObject.pm b/core/lib/Foswiki/AppObject.pm new file mode 100644 index 0000000000..9234df8225 --- /dev/null +++ b/core/lib/Foswiki/AppObject.pm @@ -0,0 +1,68 @@ +# See bottom of file for license and copyright information + +package Foswiki::AppObject; +use v5.14; + +=begin TML + +---+ Class Foswiki::AppObject; + +This is the base class for all classes which cannot be instantiated without +active =Foswiki::App= object. + +=cut + +use Assert; +use Foswiki::Exception; + +use Moo; +use namespace::clean; +extends qw(Foswiki::Object); + +has app => ( + is => 'ro', + weak_ref => 1, + isa => Foswiki::Object::isaCLASS( 'app', 'Foswiki::App', noUndef => 1, ), + required => 1, +); + +=begin TML +---++ ObjectMethod create($className, %initArgs) + +Creates a new object of =Foswiki::AppObject= based class. It's a wrapper to +the =new()= constructor which automatically passes =app= parameter to the newly +created object. + +=cut + +sub create { + my $this = shift; + my $class = shift; + + unless ( $class->isa(__PACKAGE__) ) { + Foswiki::Exception::Fatal->throw( + text => "Class $class is not a " . __PACKAGE__ . " descendant." ); + } + + return $class->new( app => $this->app, @_ ); +} + +1; +__END__ +Foswiki - The Free and Open Source Wiki, http://foswiki.org/ + +Copyright (C) 2013 Foswiki Contributors. Foswiki Contributors +are listed in the AUTHORS file in the root of this distribution. +NOTE: Please extend that file, not this notice. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. For +more details read LICENSE in the root of this distribution. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +As per the GPL, removal of this notice is prohibited. diff --git a/core/lib/Foswiki/Config.pm b/core/lib/Foswiki/Config.pm index b053e8fac8..41927f4031 100644 --- a/core/lib/Foswiki/Config.pm +++ b/core/lib/Foswiki/Config.pm @@ -4,11 +4,18 @@ package Foswiki::Config; use v5.14; use Assert; +use Encode; +use File::Basename; +use File::Spec; +use POSIX qw(locale_h); +use Unicode::Normalize; +use Cwd qw( abs_path ); +use Try::Tiny; use Foswiki (); use Moo; use namespace::clean; -extends qw(Foswiki::Object); +extends qw(Foswiki::AppObject); # Enable to trace auto-configuration (Bootstrap) use constant TRAUTO => 1; @@ -40,6 +47,8 @@ my %remap = ( has data => ( is => 'rw', + lazy => 1, + clearer => 1, default => sub { {} }, ); @@ -49,12 +58,13 @@ has files => ( default => sub { [] }, ); -# failedConfig keeps the name of the failed config or spec file. -has failedConfig => ( 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, ); +# failed keeps the name of the failed config or spec file. +has failedConfig => ( is => 'rw', ); +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 @@ -73,13 +83,16 @@ has noLocal => ( is => 'rw', default => 0, ); sub BUILD { my $this = shift; + my ($params) = @_; # Alias ::cfg for compatibility. Though $app->cfg should be preferred way of # accessing config. *Foswiki::cfg = $this->data; *TWiki::cfg = $this->data; - $this->data->{isVALID} = $this->readConfig; + $this->data->{isVALID} = + $this->readConfig( $this->noExpand, $this->noSpec, $this->configSpec, + $this->noLocal, ); } sub _workOutOS { @@ -143,6 +156,7 @@ provide defaults, and it would be silly to have them in two places anyway. sub readConfig { my $this = shift; + my ( $noExpand, $noSpec, $configSpec, $noLocal ) = @_; # To prevent us from overriding the custom code in test mode return 1 if $this->data->{ConfigurationFinished}; @@ -156,10 +170,10 @@ sub readConfig { # Old configs might not bootstrap the OS settings, so set if needed. $this->_workOutOS unless ( $this->data->{OS} && $this->data->{DetailedOS} ); - unless ( $this->noSpec ) { + unless ($noSpec) { push @{ $this->files }, 'Foswiki.spec'; } - if ( !$this->noSpec && $this->configSpec ) { + if ( !$noSpec && $configSpec ) { foreach my $dir (@INC) { foreach my $subdir ( 'Foswiki/Plugins', 'Foswiki/Contrib' ) { my $d; @@ -179,7 +193,7 @@ sub readConfig { } } } - unless ( $this->noLocal ) { + unless ($noLocal) { push @{ $this->files }, 'LocalSite.cfg'; } @@ -245,7 +259,7 @@ CODE # Expand references to $this->data vars embedded in the values of # other $this->data vars. - $this->expandValue( $this->data ) unless $this->noExpand; + $this->expandValue( $this->data ) unless $noExpand; $this->data->{ConfigurationFinished} = 1; @@ -336,6 +350,329 @@ sub _handleExpand { return ''; } +=begin TML +---++ ObjectMethod bootstrap() + +This method tries to determine mandatory configuration defaults to operate +when no LocalSite.cfg is found. + +=cut + +sub bootstrap { + my $this = shift; + + # Strip off any occasional configuration data which might be a result of + # previously failed readConfig. + $this->clear_data; + + my $env = $this->app->env; + + print STDERR "AUTOCONFIG: Bootstrap Phase 1: " . Data::Dumper::Dumper($env) + if (TRAUTO); + + # Try to create $Foswiki::cfg in a minimal configuration, + # using paths and URLs relative to this request. If URL + # rewriting is happening in the web server this is likely + # to go down in flames, but it gives us the best chance of + # recovering. We need to guess values for all the vars that + + # would trigger "undefined" errors + my $bin; + my $script = ''; + if ( defined $env->{FOSWIKI_SCRIPTS} ) { + $bin = $env->{FOSWIKI_SCRIPTS}; + } + else { + eval('require FindBin'); + Foswiki::Exception::Fatal->throw( text => + "Could not load FindBin to support configuration recovery: $@" ) + if $@; + FindBin::again(); # in case we are under mod_perl or similar + $FindBin::Bin =~ m/^(.*)$/; + $bin = $1; + $FindBin::Script =~ m/^(.*)$/; + $script = $1; + } + + # Can't use Foswiki::decode_utf8 - this is too early in initialization + # SMELL TODO The above must not be true anymore. Yet, why not use + # Encode::decode_utf8? + print STDERR "AUTOCONFIG: Found Bin dir: " + . $bin + . ", Script name: $script using FindBin\n" + if (TRAUTO); + + $this->data->{ScriptSuffix} = ( fileparse( $script, qr/\.[^.]*/ ) )[2]; + $this->data->{ScriptSuffix} = '' + if ( $this->data->{ScriptSuffix} eq '.fcgi' ); + print STDERR "AUTOCONFIG: Found SCRIPT SUFFIX " + . $this->data->{ScriptSuffix} . "\n" + if ( TRAUTO && $this->data->{ScriptSuffix} ); + + my %rel_to_root = ( + DataDir => { dir => 'data', required => 0 }, + LocalesDir => { dir => 'locale', required => 0 }, + PubDir => { dir => 'pub', required => 0 }, + ToolsDir => { dir => 'tools', required => 0 }, + WorkingDir => { + dir => 'working', + required => 1, + validate_file => 'README' + }, + TemplateDir => { + dir => 'templates', + required => 1, + validate_file => 'foswiki.tmpl' + }, + ScriptDir => { + dir => 'bin', + required => 1, + validate_file => 'setlib.cfg' + } + ); + + # Note that we don't resolve x/../y to y, as this might + # confuse soft links + my $root = File::Spec->catdir( $bin, File::Spec->updir() ); + $root =~ s{\\}{/}g; + my $fatal = ''; + my $warn = ''; + while ( my ( $key, $def ) = each %rel_to_root ) { + $this->data->{$key} = File::Spec->rel2abs( $def->{dir}, $root ); + $this->data->{$key} = abs_path( $this->data->{$key} ); + ( $this->data->{$key} ) = $this->data->{$key} =~ m/^(.*)$/; # untaint + + # Need to decode utf8 back to perl characters. The file path operations + # all worked with bytes, but Foswiki needs characters. + $this->data->{$key} = NFC( Encode::decode_utf8( $this->data->{$key} ) ); + + print STDERR "AUTOCONFIG: $key = " + . Encode::encode_utf8( $this->data->{$key} ) . "\n" + if (TRAUTO); + + if ( -d $this->data->{$key} ) { + if ( $def->{validate_file} + && !-e $this->data->{$key} . "/$def->{validate_file}" ) + { + $fatal .= + "\n{$key} (guessed " + . $this->data->{$key} . ") " + . $this->data->{$key} + . "/$def->{validate_file} not found"; + } + } + elsif ( $def->{required} ) { + $fatal .= "\n{$key} (guessed " . $this->data->{$key} . ")"; + } + else { + $warn .= + "\n * Note: {$key} could not be guessed. Set it manually!"; + } + } + + # Bootstrap the Site Locale and CharSet + $this->_bootstrapSiteSettings(); + + # Bootstrap the store related settings. + $this->_bootstrapStoreSettings(); + + if ($fatal) { + Foswiki::Exception::Fatal->throw( text => <readConfig( 0, 0, 1, 1 ); + + $this->_workOutOS(); + print STDERR "AUTOCONFIG: Detected OS " + . $this->data->{OS} + . ": DetailedOS: " + . $this->data->{DetailedOS} . " \n" + if (TRAUTO); + + $this->data->{isVALID} = 1; + $this->setBootstrap(); + + # Note: message is not I18N'd because there is no point; there + # is no localisation in a default cfg derived from Foswiki.spec + my $system_message = <data->{Site}{Locale} = setlocale(LC_CTYPE); + + print STDERR "AUTOCONFIG: Set initial {Site}{Locale} to " + . $this->data->{Site}{Locale} . "\n"; +} + +=begin TML + +---++ ObjectMethod _bootstrapStoreSettings() + +Called by bootstrapConfig. This handles the store specific settings. This in turn +tests each Store Contib to determine if it's capable of bootstrapping. + +=cut + +sub _bootstrapStoreSettings { + my $this = shift; + + # Ask each installed store to bootstrap itself. + + my @stores = Foswiki::Configure::FileUtil::findPackages( + 'Foswiki::Contrib::*StoreContrib'); + + foreach my $store (@stores) { + try { + Foswiki::load_package($store); + } + finally { + unless (@_) { + my $ok; + eval('$ok = $store->can(\'bootstrapStore\')'); + if ($@) { + print STDERR $@; + } + else { + $store->bootstrapStore() if ($ok); + } + } + }; + } + + # Handle the common store settings managed by Core. Important ones + # guessed/checked here include: + # - $Foswiki::cfg{Store}{SearchAlgorithm} + + # Set PurePerl search on Windows, or FastCGI systems. + if ( + ( + $this->data->{Engine} + && $this->data->{Engine} =~ m/(FastCGI|Apache)/ + ) + || $^O eq 'MSWin32' + ) + { + $this->data->{Store}{SearchAlgorithm} = + 'Foswiki::Store::SearchAlgorithms::PurePerl'; + print STDERR +"AUTOCONFIG: Detected FastCGI, mod_perl or MS Windows. {Store}{SearchAlgorithm} set to PurePerl\n" + if (TRAUTO); + } + else { + + # SMELL: The fork to `grep goes into a loop in the unit tests + # Not sure why, for now just default to pure perl bootstrapping + # in the unit tests. + if ( !$Foswiki::inUnitTestMode ) { + + # Untaint PATH so we can check for grep on the path + my $x = $ENV{PATH} || ''; + $x =~ m/^(.*)$/; + $ENV{PATH} = $1; + `grep -V 2>&1`; + if ($!) { + print STDERR +"AUTOCONFIG: Unable to find a valid 'grep' on the path. Forcing PurePerl search\n" + if (TRAUTO); + $this->data->{Store}{SearchAlgorithm} = + 'Foswiki::Store::SearchAlgorithms::PurePerl'; + } + else { + $this->data->{Store}{SearchAlgorithm} = + 'Foswiki::Store::SearchAlgorithms::Forking'; + print STDERR + "AUTOCONFIG: {Store}{SearchAlgorithm} set to Forking\n" + if (TRAUTO); + } + $ENV{PATH} = $x; # re-taint + } + else { + $this->data->{Store}{SearchAlgorithm} = + 'Foswiki::Store::SearchAlgorithms::PurePerl'; + } + } + + # Detect the NFC / NDF normalization of the file system, and set + # NFCNormalizeFilenames if needed. + # SMELL: Really this should be done per web, both in data and pub. + my $nfcok = + Foswiki::Configure::FileUtil::canNfcFilenames( $Foswiki::cfg{DataDir} ); + if ( defined $nfcok && $nfcok == 1 ) { + print STDERR "AUTOCONFIG: Data Storage allows NFC filenames\n" + if (TRAUTO); + $this->data->{NFCNormalizeFilenames} = 0; + } + elsif ( defined($nfcok) && $nfcok == 0 ) { + print STDERR "AUTOCONFIG: Data Storage enforces NFD filenames\n" + if (TRAUTO); + $this->data->{NFCNormalizeFilenames} = 1 + ; #the configure's interface still shows unchecked - so, don't understand.. ;( + } + else { + print STDERR "AUTOCONFIG: WARNING: Unable to detect Normalization.\n"; + $this->data->{NFCNormalizeFilenames} = 1; #enable too - safer as none + } +} + +=begin TML + +---++ ObjectMethod setBootstrap() + +This routine is called to initialize the bootstrap process. It sets the list of +configuration parameters that will need to be set and "protected" during bootstrap. + +If any keys will be set during bootstrap / initial creation of LocalSite.cfg, they +should be added here so that they are preserved when the %Foswiki::cfg hash is +wiped and re-initialized from the Foswiki spec. + +=cut + +sub setBootstrap { + my $this = shift; + + # Bootstrap works out the correct values of these keys + my @BOOTSTRAP = + qw( {DataDir} {DefaultUrlHost} {DetailedOS} {OS} {PubUrlPath} {ToolsDir} {WorkingDir} + {PubDir} {TemplateDir} {ScriptDir} {ScriptUrlPath} {ScriptUrlPaths}{view} + {ScriptSuffix} {LocalesDir} {Store}{Implementation} {NFCNormalizeFilenames} + {Store}{SearchAlgorithm} {Site}{Locale} ); + + $this->data->{isBOOTSTRAPPING} = 1; + push( @{ $this->data->{BOOTSTRAP} }, @BOOTSTRAP ); +} + 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 5f7f0fc3ad..6a3f0616c7 100644 --- a/core/lib/Foswiki/Engine.pm +++ b/core/lib/Foswiki/Engine.pm @@ -53,7 +53,7 @@ sub start { elsif ( $params{env}{'psgi.version'} ) { # We don't have PSGI support yet. - ...; + $engine = 'Foswiki::Engine::PSGI'; } else { $engine = 'Foswiki::Engine::CLI'; diff --git a/core/lib/Foswiki/Exception.pm b/core/lib/Foswiki/Exception.pm index 73c4c59155..43f2777787 100644 --- a/core/lib/Foswiki/Exception.pm +++ b/core/lib/Foswiki/Exception.pm @@ -188,8 +188,8 @@ sub rethrowAs { ---++ ClassMethod transmute($class, $exception) Reinstantiates $exception into $class. "Coerce" would be more correct term for -this operation but it's better be avoded because it is occupied by Moo/Moose for -attribute operation. +this operation but this name better be avoded because it is occupied by +Moo/Moose for an attribute operation. =cut diff --git a/core/lib/Foswiki/Object.pm b/core/lib/Foswiki/Object.pm index 5aebca1cca..f5908e2401 100644 --- a/core/lib/Foswiki/Object.pm +++ b/core/lib/Foswiki/Object.pm @@ -1,10 +1,11 @@ +# See bottom of file for license and copyright information package Foswiki::Object; use v5.14; =begin TML ----+ package Foswiki::Object +---+ Class Foswiki::Object *NOTE:* This document is in draft status and may change as a result of a discussion, raised concerns or reasonable proposals.