API Refactor bootstrap, request handling

See https://github.com/silverstripe/silverstripe-framework/pull/7037
and https://github.com/silverstripe/silverstripe-framework/issues/6681

Squashed commit of the following:

commit 8f65e56532
Author: Ingo Schommer <me@chillu.com>
Date:   Thu Jun 22 22:25:50 2017 +1200

    Fixed upgrade guide spelling

commit 76f95944fa
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Thu Jun 22 16:38:34 2017 +1200

    BUG Fix non-test class manifest including sapphiretest / functionaltest

commit 9379834cb4
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Thu Jun 22 15:50:47 2017 +1200

    BUG Fix nesting bug in Kernel

commit 188ce35d82
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Thu Jun 22 15:14:51 2017 +1200

    BUG fix db bootstrapping issues

commit 7ed4660e7a
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Thu Jun 22 14:49:07 2017 +1200

    BUG Fix issue in DetailedErrorFormatter

commit 738f50c497
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Thu Jun 22 11:49:19 2017 +1200

    Upgrading notes on mysite/_config.php

commit 6279d28e5e
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Thu Jun 22 11:43:28 2017 +1200

    Update developer documentation

commit 5c90d53a84
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Thu Jun 22 10:48:44 2017 +1200

    Update installer to not use global databaseConfig

commit f9b2ba4755
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Wed Jun 21 21:04:39 2017 +1200

    Fix behat issues

commit 5b59a912b6
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Wed Jun 21 17:07:11 2017 +1200

    Move HTTPApplication to SilverStripe\Control namespace

commit e2c4a18f63
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Wed Jun 21 16:29:03 2017 +1200

    More documentation
    Fix up remaining tests
    Refactor temp DB into TempDatabase class so it’s available outside of unit tests.

commit 5d235e64f3
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Wed Jun 21 12:13:15 2017 +1200

    API HTTPRequestBuilder::createFromEnvironment() now cleans up live globals
    BUG Fix issue with SSViewer
    Fix Security / View tests

commit d88d4ed4e4
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Tue Jun 20 16:39:43 2017 +1200

    API Refactor AppKernel into CoreKernel

commit f7946aec33
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Tue Jun 20 16:00:40 2017 +1200

    Docs and minor cleanup

commit 12bd31f936
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Tue Jun 20 15:34:34 2017 +1200

    API Remove OutputMiddleware
    API Move environment / global / ini management into Environment class
    API Move getTempFolder into TempFolder class
    API Implement HTTPRequestBuilder / CLIRequestBuilder
    BUG Restore SS_ALLOWED_HOSTS check in original location
    API CoreKernel now requires $basePath to be passed in
    API Refactor installer.php to use application to bootstrap
    API move memstring conversion globals to Convert
    BUG Fix error in CoreKernel nesting not un-nesting itself properly.

commit bba9791146
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Mon Jun 19 18:07:53 2017 +1200

    API Create HTTPMiddleware and standardise middleware for request handling

commit 2a10c2397b
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Mon Jun 19 17:42:42 2017 +1200

    Fixed ORM tests

commit d75a8d1d93
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Mon Jun 19 17:15:07 2017 +1200

    FIx i18n tests

commit 06364af3c3
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Mon Jun 19 16:59:34 2017 +1200

    Fix controller namespace
    Move states to sub namespace

commit 2a278e2953
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Mon Jun 19 12:49:45 2017 +1200

    Fix forms namespace

commit b65c21241b
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Thu Jun 15 18:56:48 2017 +1200

    Update API usages

commit d1d4375c95
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Thu Jun 15 18:41:44 2017 +1200

    API Refactor $flush into HTPPApplication
    API Enforce health check in Controller::pushCurrent()
    API Better global backup / restore
    Updated Director::test() to use new API

commit b220534f06
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Tue Jun 13 22:05:57 2017 +1200

    Move app nesting to a test state helper

commit 603704165c
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Tue Jun 13 21:46:04 2017 +1200

    Restore kernel stack to fix multi-level nesting

commit 2f6336a15b
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Tue Jun 13 17:23:21 2017 +1200

    API Implement kernel nesting

commit fc7188da7d
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Tue Jun 13 15:43:13 2017 +1200

    Fix core tests

commit a0ae723514
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Tue Jun 13 15:23:52 2017 +1200

    Fix manifest tests

commit ca03395251
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Tue Jun 13 15:00:00 2017 +1200

    API Move extension management into test state

commit c66d433977
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Tue Jun 13 14:10:59 2017 +1200

    API Refactor SapphireTest state management into SapphireTestState
    API Remove Injector::unregisterAllObjects()
    API Remove FakeController

commit f26ae75c6e
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Mon Jun 12 18:04:34 2017 +1200

    Implement basic CLI application object

commit 001d559662
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Mon Jun 12 17:39:38 2017 +1200

    Remove references to SapphireTest::is_running_test()
    Upgrade various code

commit de079c041d
Author: Damian Mooyman <damian@silverstripe.com>
Date:   Wed Jun 7 18:07:33 2017 +1200

    API Implement APP object
    API Refactor of Session
This commit is contained in:
Damian Mooyman 2017-06-22 22:50:45 +12:00 committed by Ingo Schommer
parent 4ad6bdbe7e
commit 3873e4ba00
202 changed files with 7048 additions and 5414 deletions

20
_config/tests.yml Normal file
View File

@ -0,0 +1,20 @@
---
Name: sapphiretest
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Dev\State\SapphireTestState:
properties:
States:
globals: %$SilverStripe\Dev\State\GlobalsTestState
extensions: %$SilverStripe\Dev\State\ExtensionTestState
flushable: %$SilverStripe\Dev\State\FlushableTestState
requirements: %$SilverStripe\View\Dev\RequirementsTestState
---
Name: kerneltest
Before: '*'
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Dev\State\SapphireTestState:
properties:
States:
kernel: %$SilverStripe\Dev\State\KernelTestState

View File

@ -1,152 +1,23 @@
<?php <?php
use SilverStripe\ORM\DB; // CLI specific bootstrapping
use SilverStripe\ORM\DataModel; use SilverStripe\Control\CLIRequestBuilder;
use SilverStripe\Control\Director; use SilverStripe\Control\HTTPApplication;
use SilverStripe\Core\CoreKernel;
require __DIR__ . '/src/includes/autoload.php';
/** // Ensure that people can't access this from a web-server
* File similar to main.php designed for command-line scripts if (!in_array(PHP_SAPI, ["cli", "cgi", "cgi-fcgi"])) {
*
* This file lets you execute SilverStripe requests from the command-line. The URL is passed as the first argument to
* the scripts.
*/
/**
* Ensure that people can't access this from a web-server
*/
if(PHP_SAPI != "cli" && PHP_SAPI != "cgi" && PHP_SAPI != "cgi-fcgi") {
echo "cli-script.php can't be run from a web request, you have to run it on the command-line."; echo "cli-script.php can't be run from a web request, you have to run it on the command-line.";
die(); die();
} }
// We update the $_SERVER variable to contain data consistent with the rest of the application. // Build request and detect flush
$_SERVER = array_merge(array( $request = CLIRequestBuilder::createFromEnvironment();
'SERVER_PROTOCOL' => 'HTTP/1.1',
'HTTP_ACCEPT' => 'text/plain;q=0.5',
'HTTP_ACCEPT_LANGUAGE' => '*;q=0.5',
'HTTP_ACCEPT_ENCODING' => '',
'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1;q=0.5',
'SERVER_SIGNATURE' => 'Command-line PHP/' . phpversion(),
'SERVER_SOFTWARE' => 'PHP/' . phpversion(),
'SERVER_ADDR' => '127.0.0.1',
'REMOTE_ADDR' => '127.0.0.1',
'REQUEST_METHOD' => 'GET',
'HTTP_USER_AGENT' => 'CLI',
), $_SERVER);
/**
* Identify the cli-script.php file and change to its container directory, so that require_once() works
*/
$_SERVER['SCRIPT_FILENAME'] = __FILE__;
chdir(dirname($_SERVER['SCRIPT_FILENAME']));
/**
* Process arguments and load them into the $_GET and $_REQUEST arrays
* For example,
* sake my/url somearg otherarg key=val --otherkey=val third=val&fourth=val
*
* Will result in the following get data:
* args => array('somearg', 'otherarg'),
* key => val
* otherkey => val
* third => val
* fourth => val
*/
if(isset($_SERVER['argv'][2])) {
$args = array_slice($_SERVER['argv'],2);
if(!isset($_GET)) $_GET = array();
if(!isset($_REQUEST)) $_REQUEST = array();
foreach($args as $arg) {
if(strpos($arg,'=') == false) {
$_GET['args'][] = $arg;
} else {
$newItems = array();
parse_str( (substr($arg,0,2) == '--') ? substr($arg,2) : $arg, $newItems );
$_GET = array_merge($_GET, $newItems);
}
}
$_REQUEST = array_merge($_REQUEST, $_GET);
}
// Set 'url' GET parameter
if(isset($_SERVER['argv'][1])) {
$_REQUEST['url'] = $_SERVER['argv'][1];
$_GET['url'] = $_SERVER['argv'][1];
}
// require composers autoloader
if (file_exists($autoloadPath = dirname(__DIR__) . '/vendor/autoload.php')) {
require_once $autoloadPath;
}
else {
echo "Failed to include composer's autoloader, unable to continue\n";
exit(1);
}
/**
* Include SilverStripe's core code
*/
require_once("Core/Core.php");
global $databaseConfig;
// We don't have a session in cli-script, but this prevents errors
$_SESSION = null;
// Connect to database
if(!isset($databaseConfig) || !isset($databaseConfig['database']) || !$databaseConfig['database']) {
echo "\nPlease configure your database connection details. You can do this by creating a file
called .env in " . BASE_PATH;
echo <<<ENVCONTENT
Put the following content into this file:
--------------------------------------------------
# Change this from 'dev' to 'live' for a production environment.
SS_ENVIRONMENT_TYPE="dev"
/* This defines a default database user */
SS_DATABASE_SERVER="localhost"
SS_DATABASE_USERNAME="<user>"
SS_DATABASE_PASSWORD="<password>"
SS_DATABASE_NAME="<database>"
--------------------------------------------------
Once you have done that, run 'composer install' or './framework/sake dev/build' to create
an empty database.
For more information, please read this page in our docs:
http://docs.silverstripe.org/en/getting_started/environment_management/
ENVCONTENT;
exit(1);
}
DB::connect($databaseConfig);
// register_argc_argv: Tells PHP whether to declare the argv & argc variables (that would contain the GET information)
// http://php.net/manual/en/ini.core.php#ini.register-argc-argv
if (!(int)ini_get('register_argc_argv')) {
echo 'register_argc_argv is required to obtain the GET information but is disabled.'
.'Please enable it in your php.ini ('
. php_ini_loaded_file()
.')';
die();
}
// Get the request URL from the querystring arguments
$url = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : null;
if(!$url) {
echo 'Please specify an argument to cli-script.php/sake. For more information, visit'
. ' http://docs.silverstripe.org/en/developer_guides/cli' . "\n";
die();
}
$_SERVER['REQUEST_URI'] = BASE_URL . '/' . $url;
// Direct away - this is the "main" function, that hands control to the apporopriate controller
DataModel::set_inst(new DataModel());
Director::direct($url, DataModel::inst());
// Default application
$kernel = new CoreKernel(BASE_PATH);
$app = new HTTPApplication($kernel);
$response = $app->handle($request);
$response->output();

View File

@ -82,12 +82,12 @@
"SilverStripe\\Framework\\Tests\\Behaviour\\": "tests/behat/src/" "SilverStripe\\Framework\\Tests\\Behaviour\\": "tests/behat/src/"
}, },
"files": [ "files": [
"src/Core/Constants.php", "src/includes/constants.php"
"src/Dev/PhpUnitShim.php"
] ]
}, },
"include-path": [ "include-path": [
"src/", "src/",
"src/includes/",
"thirdparty/" "thirdparty/"
], ],
"scripts": { "scripts": {

View File

@ -24,12 +24,13 @@ Our web-based [PHP installer](installation/) can check if you meet the requireme
* `/dev/urandom` * `/dev/urandom`
* [`mcrypt_create_iv()`](http://php.net/manual/en/function.mcrypt-create-iv.php) * [`mcrypt_create_iv()`](http://php.net/manual/en/function.mcrypt-create-iv.php)
* CAPICOM Utilities (`CAPICOM.Utilities.1`, Windows only) * CAPICOM Utilities (`CAPICOM.Utilities.1`, Windows only)
* Required modules: dom, gd2, fileinfo, hash, iconv, mbstring, mysqli (or other database driver), session, simplexml, tokenizer, xml. * Required modules: ctype, dom, fileinfo, hash, intl, mbstring, session, simplexml, tokenizer, xml.
* At least one from each group of extensions:
* Image library extension (gd2, imagick)
* DB connector library (pdo, mysqli, pgsql)
* Recommended configuration * Recommended configuration
* Dev (local development for running test framework): memory_limit 512MB
safe_mode = Off * Production: memory_limit = 64M
magic_quotes_gpc = Off
memory_limit = 48M
* See [phpinfo()](http://php.net/manual/en/function.phpinfo.php) for more information about your environment * See [phpinfo()](http://php.net/manual/en/function.phpinfo.php) for more information about your environment
* One of the following databases: * One of the following databases:

View File

@ -27,9 +27,9 @@ First we're telling Homebrew about some new repositories to get the PHP installa
brew tap homebrew/dupes brew tap homebrew/dupes
brew tap homebrew/php brew tap homebrew/php
We're installing PHP 5.5 here, with the required `mcrypt` module: We're installing PHP 5.6 here, with the required `mcrypt` module:
brew install php55 php55-mcrypt brew install php56 php56-mcrypt php56-intl php56-apcu
There's a [Homebrew Troubleshooting](https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/Troubleshooting.md) guide if Homebrew doesn't work out as expected (run `brew update` and `brew doctor`). There's a [Homebrew Troubleshooting](https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/Troubleshooting.md) guide if Homebrew doesn't work out as expected (run `brew update` and `brew doctor`).

View File

@ -22,21 +22,26 @@ existing modules or the directories lists in "Core Structure".
| --------- | ----------- | | --------- | ----------- |
| `<mysite>/` | This directory contains all of your code that defines your website. | | `<mysite>/` | This directory contains all of your code that defines your website. |
| `<mysite>/_config` | YAML configuration specific to your application | | `<mysite>/_config` | YAML configuration specific to your application |
| `<mysite>/code` | PHP code for model and controller (subdirectories are optional) | | `<mysite>/src` | PHP code for model and controller (subdirectories are optional) |
| `<mysite>/templates` | HTML [templates](/developer_guides/templates) with *.ss-extension | | `<mysite>/tests` | PHP Unit tests |
| `<mysite>/templates` | HTML [templates](/developer_guides/templates) with *.ss-extension for the `$default` theme |
| `<mysite>/css ` | CSS files | | `<mysite>/css ` | CSS files |
| `<mysite>/images ` | Images used in the HTML templates | | `<mysite>/images ` | Images used in the HTML templates |
| `<mysite>/javascript` | Javascript and other script files | | `<mysite>/javascript` | Javascript and other script files |
| `<mysite>/client` | More complex projects can alternatively contain frontend assets in a common `client` folder | | `<mysite>/client` | More complex projects can alternatively contain frontend assets in a common `client` folder |
| `<mysite>/themes/<yourtheme>` | Custom nested themes (note: theme structure is described below) |
Check our [JavaScript Coding Conventions](javascript_coding_conventions) for more details Check our [JavaScript Coding Conventions](javascript_coding_conventions) for more details
on folder and file naming in SilverStripe core modules. on folder and file naming in SilverStripe core modules.
## Themes Structure ## Themes Structure
| `themes/simple/` | Standard "simple" theme | | Directory | Description |
| ------------------ | --------------------------- | | ------------------ | --------------------------- |
| `themes/yourtheme/` | The themes folder can contain more than one theme - here's your own | | `themes/simple/` | Standard "simple" theme |
| `themes/<yourtheme>/` | Custom theme base directory |
| `themes/<yourtheme>/templates` | Theme templates |
| `themes/<yourtheme>/css` | Theme CSS files |
See [themes](/developer_guides/templates/themes) See [themes](/developer_guides/templates/themes)
@ -77,7 +82,7 @@ Example Forum Documentation:
| `forum/docs/en/` | English documentation | | `forum/docs/en/` | English documentation |
| `forum/docs/en/index.md` | Documentation homepage. Should provide an introduction and links to remaining docs | | `forum/docs/en/index.md` | Documentation homepage. Should provide an introduction and links to remaining docs |
| `forum/docs/en/Getting_Started.md` | Documentation page. Naming convention is Uppercase and underscores. | | `forum/docs/en/Getting_Started.md` | Documentation page. Naming convention is Uppercase and underscores. |
| `forum/docs/en//_images/` | Folder to store any images or media | | `forum/docs/en/_images/` | Folder to store any images or media |
| `forum/docs/en/Some_Topic/` | You can organise documentation into nested folders. Naming convention is Uppercase and underscores. | | `forum/docs/en/Some_Topic/` | You can organise documentation into nested folders. Naming convention is Uppercase and underscores. |
|`forum/docs/en/04_Some_Topic/00_Getting_Started.md`|Structure is created by use of numbered prefixes. This applies to nested folders and documentations pages, index.md should not have a prefix.| |`forum/docs/en/04_Some_Topic/00_Getting_Started.md`|Structure is created by use of numbered prefixes. This applies to nested folders and documentations pages, index.md should not have a prefix.|

View File

@ -19,16 +19,11 @@ resources are required temporarily. In general, we recommend running resource in
[command line](../cli), where configuration defaults for these settings are higher or even unlimited. [command line](../cli), where configuration defaults for these settings are higher or even unlimited.
<div class="info" markdown="1"> <div class="info" markdown="1">
SilverStripe can request more resources through `increase_memory_limit_to()` and `increase_time_limit_to()` functions. SilverStripe can request more resources through `Environment::increaseMemoryLimitTo()` and
`Environment::increaseTimeLimitTo()` functions.
</div> </div>
:::php :::php
function myBigFunction() { public function myBigFunction() {
increase_time_limit_to(400); Environment::increaseTimeLimitTo(400);
// or..
set_increase_time_limit_max();
// ..
} }

View File

@ -0,0 +1,143 @@
title: App Object and Kernel
summary: Provides bootstrapping and entrypoint to the SilverStripe application
# Kernel
The [api:Kernel] object provides a container for the various manifests, services, and components
which a SilverStripe application must have available in order for requests to be executed.
This can be accessed in user code via Injector
:::php
$kernel = Injector::inst()->get(Kernel::class);
echo "Current environment: " . $kernel->getEnvironment();
## Kernel services
Services accessible from this kernel include:
* getContainer() -> Current [api:Injector] service
* getThemeResourceLoader() -> [api:ThemeResourceLoader] Service for loading of discovered templates.
Also used to contain nested theme sets such as the `$default` set for all root module /templates folders.
* getEnvironment() -> String value for the current environment. One of 'dev', 'live' or 'test'
Several meta-services are also available from Kernel (which are themselves containers for
other core services) but are not commonly accessed directly:
* getClassLoader() -> [api:ClassLoader] service which handles the class manifest
* getModuleLoader() -> [api:ModuleLoadel] service which handles module registration
* getConfigLoader() -> [api:ConfigLoader] Service which assists with nesting of [api:Config] instances
* getInjectorLoader() -> [api:InjectorLoader] Service which assists with nesting of [api:Injector] instances
## Kernel nesting
As with Config and Injector the Kernel can be nested to safely modify global application state,
and subsequently restore state. Unlike those classes, however, there is no `::unnest()`. Instead
you should call `->activate()` on the kernel instance you would like to unnest to.
:::php
$oldKernel = Injector::inst()->get(Kernel::class);
try {
// Injector::inst() / Config::inst() are automatically updated to the new kernel
$newKernel = $oldKernel->nest();
Config::modify()->set(Director::class, 'alternate_base_url', '/myurl');
}
finally {
// Any changes to config (or other application state) have now been reverted
$oldKernel->activate();
}
# Application
An application represents a basic excution controller for the top level application entry point.
The role of the application is to:
- Control bootstrapping of a provided kernel instance
- Handle errors raised from an application
- Direct requests to the request handler, and return a valid response
## HTTPApplication
The HTTPApplication provides a specialised application implementation for handling HTTP Requests.
This class provides basic support for HTTP Middleware, such as [api:ErrorControlChainMiddleware].
`main.php` contains the default application implementation.
:::php
<?php
use SilverStripe\Control\HTTPApplication;
use SilverStripe\Control\HTTPRequestBuilder;
use SilverStripe\Core\CoreKernel;
use SilverStripe\Core\Startup\ErrorControlChainMiddleware;
require __DIR__ . '/src/includes/autoload.php';
// Build request and detect flush
$request = HTTPRequestBuilder::createFromEnvironment();
// Default application
$kernel = new CoreKernel(BASE_PATH);
$app = new HTTPApplication($kernel);
$app->addMiddleware(new ErrorControlChainMiddleware($app));
$response = $app->handle($request);
$response->output();
Users can customise their own application by coping the above to a file in `mysite/main.php`, and
updating their `.htaccess` to point to the new file.
:::
<IfModule mod_rewrite.c>
# ...
RewriteCond %{REQUEST_URI} ^(.*)$
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule .* mysite/main.php?url=%1 [QSA]
# ...
</IfModule>
Note: This config must also be duplicated in the below template which provide asset routing:
`silverstripe-assets/templates/SilverStripe/Assets/Flysystem/PublicAssetAdapter_HTAccess.ss`:
:::ss
<IfModule mod_rewrite.c>
# ...
# Non existant files passed to requesthandler
RewriteCond %{REQUEST_URI} ^(.*)$
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule .* ../mysite/main.php?url=%1 [QSA]
</IfModule>
## Custom application actions
If it's necessary to boot a SilverStripe kernel and application, but not do any
request processing, you can use the Application::execute() method to invoke a custom
application entry point.
This may be necessary if using SilverStripe code within the context of a non-SilverStripe
application.
For example, the below will setup a request, session, and current controller,
but will leave the application in a "ready" state without performing any
routing.
:::php
$request = CLIRequestBuilder::createFromEnvironment();
$kernel = new TestKernel(BASE_PATH);
$app = new HTTPApplication($kernel);
$app->execute($request, function (HTTPRequest $request) {
// Start session and execute
$request->getSession()->init();
// Set dummy controller
$controller = Controller::create();
$controller->setRequest($request);
$controller->pushCurrent();
$controller->doInit();
}, true);

View File

@ -78,21 +78,26 @@ can leave sensitive files exposed to public access (the `RewriteRule` conditions
## Bootstrap ## Bootstrap
All requests go through `framework/main.php`, which sets up the execution environment: The `constants.php` file is included automatically in any project which requires silverstripe/framework.
This is included automatically when the composer `vendor/autoload.php` is included, and performs its
tasks silently in the background.
* Tries to locate an `.env` * Tries to locate an `.env`
[configuration file](/getting_started/environment_management) in the webroot. [configuration file](/getting_started/environment_management) in the webroot.
* Sets constants based on the filesystem structure (e.g. `BASE_URL`, `BASE_PATH` and `TEMP_FOLDER`) * Sets constants based on the filesystem structure (e.g. `BASE_URL`, `BASE_PATH` and `TEMP_FOLDER`)
* Normalizes the `url` parameter in preparation for handing it off to `Director`
* Connects to a database, based on information stored in the global `$databaseConfig` variable. All requests go through `framework/main.php`, which sets up the core [api:Kernel] and [api:HTTPApplication]
The configuration is either defined in your `_config.php`, or through `.env` objects. See [/developer_guides/execution_pipeline/app_object_and_kernel] for details on this.
* Sets up [error handlers](../debugging/error_handling) The main process follows:
* Optionally continues a [session](../cookies_and_sessions/sessions) if the request already contains a session identifier
* Loads manifests for PHP classes, templates, as well as any [YAML configuration](../configuration).
* Optionally regenerates these manifests (if a ["flush" query parameter](flushable) is set) * Include `autoload.php`
* Executes all procedural configuration defined through `_config.php` in all discovered modules * Construct [api:HTTPRequest] object from environment.
* Loads the Composer PHP class autoloader * Construct a `Kernel` instance
* Hands control over to [api:Director] * Construct a `HTTPApplication` instance
* Add any necessary middleware to this application
* Pass the request to the application, and request a response
While you usually don't need to modify the bootstrap on this level, some deeper customizations like While you usually don't need to modify the bootstrap on this level, some deeper customizations like
adding your own manifests or a performance-optimized routing might require it. adding your own manifests or a performance-optimized routing might require it.

View File

@ -111,6 +111,16 @@ the `SilverStripe\Forms` namespace.
The below sections deal with upgrades to specific parts of various API. Projects which rely on certain The below sections deal with upgrades to specific parts of various API. Projects which rely on certain
API should be upgraded as appropriate using any of the relevant processes documented below. API should be upgraded as appropriate using any of the relevant processes documented below.
#### Upgrade `mysite/_config.php`
The globals `$database` and `$databaseConfig` are deprecated. You should upgrade your
site _config.php files to use the `.env` configuration (below).
If you need to configure database details in PHP, use the new `DB::setConfig()` api instead.
You should remove any references to `ConfigureFromEnv.php` in your project, as this file
is no longer necessary.
#### Upgrade of `_ss_environment.php` to `.env` configuration #### Upgrade of `_ss_environment.php` to `.env` configuration
The php configuration `_ss_environment.php` file has been replaced in favour of a non-executable The php configuration `_ss_environment.php` file has been replaced in favour of a non-executable
@ -160,6 +170,9 @@ logic early in the bootstrap, this is best placed in the `_config.php` files.
Note also that `$_FILE_TO_URL_MAPPING` has been removed and replaced with `SS_BASE_URL` env var. Note also that `$_FILE_TO_URL_MAPPING` has been removed and replaced with `SS_BASE_URL` env var.
The global values `$database` and `$databaseConfig` have been deprecated, as has `ConfigureFromEnv.php`
which is no longer necessary.
See [Environment Management docs](/getting-started/environment_management/) for full details. See [Environment Management docs](/getting-started/environment_management/) for full details.
#### Replace usages of Object class #### Replace usages of Object class
@ -245,6 +258,30 @@ Extensions
$has = DataObject::has_extension(File::class, Versioned::class); // alternate $has = DataObject::has_extension(File::class, Versioned::class); // alternate
$extensions = DataObject::get_extensions(File::class); $extensions = DataObject::get_extensions(File::class);
#### Upgrade references to Session object
Session object is no longer statically accessible via `Session::inst()`. Instead, Session
is a member of the current request.
Before:
:::php
public function httpSubmission($data, $form, $request) {
Session::set('loggedIn', null);
}
After:
:::php
public function httpSubmission($data, $form, $request) {
$request->getSession()->set('loggedIn', null);
}
In some places it may still be necessary to access the session object where no request is available.
In rare cases it is still possible to access the request of the current controller via
`Controller::curr()->getRequest()` to gain access to the current session.
#### Compatibility with the new front-end building tools #### Compatibility with the new front-end building tools
@ -1248,6 +1285,7 @@ After (`mysite/_config/config.yml`):
#### <a name="overview-general-api"></a>General and Core API Additions / Changes #### <a name="overview-general-api"></a>General and Core API Additions / Changes
* Minimum PHP version raised to 5.6 (with support for PHP 7.x) * Minimum PHP version raised to 5.6 (with support for PHP 7.x)
* Dropped support for PHP safe mode (removed php 5.4).
* Once PHP versions become [unsupported by the PHP Project](http://php.net/supported-versions.php)), * Once PHP versions become [unsupported by the PHP Project](http://php.net/supported-versions.php)),
we drop support for those versions in the [next minor release](/contributing/release-process we drop support for those versions in the [next minor release](/contributing/release-process
This means PHP 5.6 and PHP 7.0 support will become unsupported in Dec 2018. This means PHP 5.6 and PHP 7.0 support will become unsupported in Dec 2018.
@ -1270,6 +1308,9 @@ After (`mysite/_config/config.yml`):
* `Configurable` Provides Config API helper methods * `Configurable` Provides Config API helper methods
* `Injectable` Provides Injector API helper methods * `Injectable` Provides Injector API helper methods
* `Extensible` Allows extensions to be applied * `Extensible` Allows extensions to be applied
* `Convert` class has extra methods for formatting file sizes in php_ini compatible format
* `Convert::memstring2bytes()` will parse a php_ini memory size.
* `Convert::bytes2memstring()` will format the memory size with the appropriate scale.
* `SiteTree.alternatePreviewLink` is deprecated. Use `updatePreviewLink` instead. * `SiteTree.alternatePreviewLink` is deprecated. Use `updatePreviewLink` instead.
* `Injector` dependencies no longer automatically inherit from parent classes. * `Injector` dependencies no longer automatically inherit from parent classes.
* `$action` parameter to `Controller::Link()` method is standardised. * `$action` parameter to `Controller::Link()` method is standardised.
@ -1335,10 +1376,34 @@ After (`mysite/_config/config.yml`):
* `findAnAdministrator` use `DefaultAdminService::findOrCreateDefaultAdmin()` instead * `findAnAdministrator` use `DefaultAdminService::findOrCreateDefaultAdmin()` instead
* `Member` methods deprecated: * `Member` methods deprecated:
* `checkPassword`. Use Authenticator::checkPassword() instead * `checkPassword`. Use Authenticator::checkPassword() instead
* `RequestFilter` changed. $session and $dataModel variables removed from preRequest / postRequest
#### <a name="overview-general-removed"></a>General and Core Removed API #### <a name="overview-general-removed"></a>General and Core Removed API
* Removed `ConfigureFromEnv.php` as it's no longer necessary.
* `Session` object has had significant refactoring. This object no longer is accessed via
`Session::inst()`, but instead should be queried from the current request via `$request->getSession()`.
All static methods have been removed, and the `inst_` prefix removed from all instance members.
Please see the upgrading section on Session object for more details.
* `Director.rules` config no longer support `redirect:<url>` directly via config.
* `Director::get_environment_type()` and `Director::set_environment_type()` are removed.
Get the `Kernel` instance via injector and query `getEnvironment()` instead.
E.g. `$type = Injector::inst()->get(Kernel::class)->getEnvironment();`.
* Many global methods have been refactored into `Environment` or `Convert` class.
* `increase_xdebug_nesting_level_to` removed (functionality has been inlined into `AppKernel`)
* `set_increase_time_limit_max` moved to `Environment::setTimeLimitMax()`
* `get_increase_time_limit_max` moved to `Environment::getTimeLimitMax()`
* `set_increase_memory_limit_max` moved to `Environment::setMemoryLimitMax()`
* `get_increase_memory_limit_max` moved to `Environment::getMemoryLimitMax()`
* `increase_time_limit_to` moved to `Environment::increaseTimeLimitTo()`
* `increase_memory_limit_to` moved to `Environment::increaseMemoryLimitTo()`
* `translate_memstring` moved to `Convert::memstring2bytes`.
* `getTempFolder` moved to `TempFolder::getTempFolder()`
* `getTempParentFolder` removed.
* `getTempFolderUsername` removed.
* `CMSMain::buildbrokenlinks()` action is removed. * `CMSMain::buildbrokenlinks()` action is removed.
* `Injector::unregisterAllObjects()` has been removed. Use `unregisterObjects` to unregister
groups of objects limited by type instead.
* `SS_Log` class has been removed. Use `Injector::inst()->get(LoggerInterface::class)` instead. * `SS_Log` class has been removed. Use `Injector::inst()->get(LoggerInterface::class)` instead.
* Removed `CMSBatchAction_Delete` * Removed `CMSBatchAction_Delete`
* Removed `CMSBatchAction_DeleteFromLive` * Removed `CMSBatchAction_DeleteFromLive`
@ -1496,6 +1561,17 @@ A very small number of methods were chosen for deprecation, and will be removed
* `DBMoney` values are now treated as empty only if `Amount` field is null. If an `Amount` value * `DBMoney` values are now treated as empty only if `Amount` field is null. If an `Amount` value
is provided without a `Currency` specified, it will be formatted as per the current locale. is provided without a `Currency` specified, it will be formatted as per the current locale.
* Removed `DatabaseAdmin#clearAllData()`. Use `DB::get_conn()->clearAllData()` instead * Removed `DatabaseAdmin#clearAllData()`. Use `DB::get_conn()->clearAllData()` instead
* `SapphireTest` temp DB methods have been removed and put into a new `TempDatabase` class.
This allows applications to create temp databases when not running tests.
This class now takes a connection name as a constructor class, and no longer
has static methods.
The following methods have been moved to this new class and renamed as non-static:
* `using_temp_db` -> `isUsed()`
* `kill_temp_db` -> `kill()`
* `empty_temp_db` _> `clearAllData()`
* `create_temp_db` -> `build()`
* `delete_all_temp_dbs` -> `deleteAll()`
* `resetDBSchema` -> `resetSchema()`
The below methods have been added or had their functionality updated to `DBDate`, `DBTime` and `DBDatetime` The below methods have been added or had their functionality updated to `DBDate`, `DBTime` and `DBDatetime`
* `getTimestamp()` added to get the respective date / time as unix timestamp (seconds since 1970-01-01) * `getTimestamp()` added to get the respective date / time as unix timestamp (seconds since 1970-01-01)
@ -1523,6 +1599,8 @@ The below methods have been added or had their functionality updated to `DBDate`
#### <a name="overview-orm-removed"></a>ORM Removed API #### <a name="overview-orm-removed"></a>ORM Removed API
* Deprecated globals `$database` and `$databaseConfig`. Please use `DB::setConfig()` instead.
* `DataModel` removed
* `DataObject::can*` methods no longer accept a member ID. These must now be passed a Member object or left null * `DataObject::can*` methods no longer accept a member ID. These must now be passed a Member object or left null
* `DataObject::db` removed and replaced with `DataObjectSchema::fieldSpec` and `DataObjectSchema::fieldSpecs` * `DataObject::db` removed and replaced with `DataObjectSchema::fieldSpec` and `DataObjectSchema::fieldSpecs`
* `DataObject::manyManyComponent` moved to `DataObjectSchema` * `DataObject::manyManyComponent` moved to `DataObjectSchema`

234
main.php
View File

@ -1,226 +1,18 @@
<?php <?php
use SilverStripe\ORM\DB; use SilverStripe\Control\HTTPApplication;
use SilverStripe\ORM\DataModel; use SilverStripe\Control\HTTPRequestBuilder;
use SilverStripe\Security\Security; use SilverStripe\Core\CoreKernel;
use SilverStripe\Security\Permission; use SilverStripe\Core\Startup\ErrorControlChainMiddleware;
use SilverStripe\Core\Startup\ParameterConfirmationToken;
use SilverStripe\Core\Startup\ErrorControlChain;
use SilverStripe\Control\Session;
use SilverStripe\Control\Director;
require __DIR__ . '/src/includes/autoload.php';
// Build request and detect flush
$request = HTTPRequestBuilder::createFromEnvironment();
// Default application
/************************************************************************************ $kernel = new CoreKernel(BASE_PATH);
************************************************************************************ $app = new HTTPApplication($kernel);
** ** $app->addMiddleware(new ErrorControlChainMiddleware($app));
** If you can read this text in your browser then you don't have PHP installed. ** $response = $app->handle($request);
** Please install PHP 5.5.0 or higher . ** $response->output();
** **
************************************************************************************
************************************************************************************/
if (version_compare(phpversion(), '5.5.0', '<')) {
header($_SERVER['SERVER_PROTOCOL'] . " 500 Server Error");
echo str_replace('$PHPVersion', phpversion(), file_get_contents("Dev/Install/php5-required.html"));
die();
}
/**
* Main file that handles every page request.
*
* The main.php does a number of set-up activities for the request.
*
* - Includes the .env file in your webroot
* - Gets an up-to-date manifest from {@link ManifestBuilder}
* - Sets up error handlers with {@link Debug::loadErrorHandlers()}
* - Calls {@link DB::connect()}, passing it the global variable $databaseConfig that should
* be defined in an _config.php
* - Sets up the default director rules using {@link Director::$rules}
*
* After that, it calls {@link Director::direct()}, which is responsible for doing most of the
* real work.
*
* CONFIGURING THE WEBSERVER
*
* To use SilverStripe, every request that doesn't point directly to a file should be rewritten to
* framework/main.php?url=(url). For example, http://www.example.com/about-us/rss would be rewritten
* to http://www.example.com/framework/main.php?url=about-us/rss
*
* It's important that requests that point directly to a file aren't rewritten; otherwise, visitors
* won't be able to download any CSS, JS, image files, or other downloads.
*
* On Apache, RewriteEngine can be used to do this.
*
* @see Director::direct()
*/
// require composers autoloader, unless it is already installed
if(!class_exists('Composer\\Autoload\\ClassLoader', false)) {
if (file_exists($autoloadPath = dirname(__DIR__) . '/vendor/autoload.php')) {
require_once $autoloadPath;
}
else {
if (!headers_sent()) {
header($_SERVER['SERVER_PROTOCOL'] . " 500 Server Error");
header('Content-Type: text/plain');
}
echo "Failed to include composer's autoloader, unable to continue\n";
exit(1);
}
}
// IIS will sometimes generate this.
if(!empty($_SERVER['HTTP_X_ORIGINAL_URL'])) {
$_SERVER['REQUEST_URI'] = $_SERVER['HTTP_X_ORIGINAL_URL'];
}
// Enable the entity loader to be able to load XML in Zend_Locale_Data
libxml_disable_entity_loader(false);
/**
* Figure out the request URL
*/
global $url;
// Helper to safely parse and load a querystring fragment
$parseQuery = function($query) {
parse_str($query, $_GET);
if ($_GET) $_REQUEST = array_merge((array)$_REQUEST, (array)$_GET);
};
// Apache rewrite rules and IIS use this
if (isset($_GET['url']) && php_sapi_name() !== 'cli-server') {
// Prevent injection of url= querystring argument by prioritising any leading url argument
if(isset($_SERVER['QUERY_STRING']) &&
preg_match('/^(?<url>url=[^&?]*)(?<query>.*[&?]url=.*)$/', $_SERVER['QUERY_STRING'], $results)
) {
$queryString = $results['query'].'&'.$results['url'];
$parseQuery($queryString);
}
$url = $_GET['url'];
// IIS includes get variables in url
$i = strpos($url, '?');
if($i !== false) {
$url = substr($url, 0, $i);
}
// Lighttpd and PHP 5.4's built-in webserver use this
} else {
// Get raw URL -- still needs to be decoded below (after parsing out query string).
$url = $_SERVER['REQUEST_URI'];
// Querystring args need to be explicitly parsed
if(strpos($url,'?') !== false) {
list($url, $query) = explode('?',$url,2);
$parseQuery($query);
}
// Decode URL now that it has been separated from query string.
$url = urldecode($url);
// Pass back to the webserver for files that exist
if(php_sapi_name() === 'cli-server' && file_exists(BASE_PATH . $url) && is_file(BASE_PATH . $url)) {
return false;
}
}
// Remove base folders from the URL if webroot is hosted in a subfolder
if (substr(strtolower($url), 0, strlen(BASE_URL)) == strtolower(BASE_URL)) $url = substr($url, strlen(BASE_URL));
/**
* Include SilverStripe's core code
*/
require_once('Core/Startup/ErrorControlChain.php');
require_once('Core/Startup/ParameterConfirmationToken.php');
// Prepare tokens and execute chain
$reloadToken = ParameterConfirmationToken::prepare_tokens(array('isTest', 'isDev', 'flush'));
$chain = new ErrorControlChain();
$chain
->then(function($chain) use ($reloadToken) {
// If no redirection is necessary then we can disable error supression
if (!$reloadToken) $chain->setSuppression(false);
// Load in core
require_once('Core/Core.php');
// Connect to database
global $databaseConfig;
if ($databaseConfig) DB::connect($databaseConfig);
// Check if a token is requesting a redirect
if (!$reloadToken) return;
// Otherwise, we start up the session if needed
if(!isset($_SESSION) && Session::request_contains_session_id()) {
Session::start();
}
// Next, check if we're in dev mode, or the database doesn't have any security data, or we are admin
if (Director::isDev() || !Security::database_is_ready() || Permission::check('ADMIN')) {
$reloadToken->reloadWithToken();
return;
}
// Fail and redirect the user to the login page
$loginPage = Director::absoluteURL(Security::config()->login_url);
$loginPage .= "?BackURL=" . urlencode($_SERVER['REQUEST_URI']);
header('location: '.$loginPage, true, 302);
die;
})
// Finally if a token was requested but there was an error while figuring out if it's allowed, do it anyway
->thenIfErrored(function() use ($reloadToken){
if ($reloadToken) {
$reloadToken->reloadWithToken();
}
})
->execute();
global $databaseConfig;
// Redirect to the installer if no database is selected
if(!isset($databaseConfig) || !isset($databaseConfig['database']) || !$databaseConfig['database']) {
// Is there an _ss_environment.php file?
if(file_exists(BASE_PATH . '/_ss_environment.php') || file_exists(dirname(BASE_PATH) . '/_ss_environment.php')) {
header($_SERVER['SERVER_PROTOCOL'] . " 500 Server Error");
$dv = new SilverStripe\Dev\DebugView();
echo $dv->renderHeader();
echo $dv->renderInfo(
"Configuraton Error",
Director::absoluteBaseURL()
);
echo $dv->renderParagraph(
'You need to replace your _ss_environment.php file with a .env file, or with environment variables.<br><br>'
. 'See the <a href="https://docs.silverstripe.org/en/4/getting_started/environment_management/">'
. 'Environment Management</a> docs for more information.'
);
echo $dv->renderFooter();
die();
}
if(!file_exists(BASE_PATH . '/install.php')) {
header($_SERVER['SERVER_PROTOCOL'] . " 500 Server Error");
die('SilverStripe Framework requires a $databaseConfig defined.');
}
$host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $_SERVER['SERVER_NAME'];
$s = (isset($_SERVER['SSL']) || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')) ? 's' : '';
$installURL = "http$s://" . $host . BASE_URL . '/install.php';
// The above dirname() will equate to "\" on Windows when installing directly from http://localhost (not using
// a sub-directory), this really messes things up in some browsers. Let's get rid of the backslashes
$installURL = str_replace('\\', '', $installURL);
header("Location: $installURL");
die();
}
// Direct away - this is the "main" function, that hands control to the appropriate controller
DataModel::set_inst(new DataModel());
Director::direct($url, DataModel::inst());

View File

@ -1,9 +0,0 @@
<?php
/**
* @package framework
* @subpackage core
*
* Alternative main.php file for servers that need the php5 extension
*/
include("main.php");
?>

View File

@ -0,0 +1,69 @@
<?php
namespace SilverStripe\Control;
/**
* CLI specific request building logic
*/
class CLIRequestBuilder extends HTTPRequestBuilder
{
public static function cleanEnvironment(array $variables)
{
// Create all blank vars
foreach (['_REQUEST', '_GET', '_POST', '_SESSION', '_SERVER', '_COOKIE', '_ENV', '_FILES'] as $key) {
if (!isset($variables[$key])) {
$variables[$key] = [];
};
}
// We update the $_SERVER variable to contain data consistent with the rest of the application.
$variables['_SERVER'] = array_merge(array(
'SERVER_PROTOCOL' => 'HTTP/1.1',
'HTTP_ACCEPT' => 'text/plain;q=0.5',
'HTTP_ACCEPT_LANGUAGE' => '*;q=0.5',
'HTTP_ACCEPT_ENCODING' => '',
'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1;q=0.5',
'SERVER_SIGNATURE' => 'Command-line PHP/' . phpversion(),
'SERVER_SOFTWARE' => 'PHP/' . phpversion(),
'SERVER_ADDR' => '127.0.0.1',
'REMOTE_ADDR' => '127.0.0.1',
'REQUEST_METHOD' => 'GET',
'HTTP_USER_AGENT' => 'CLI',
), $variables['_SERVER']);
/**
* Process arguments and load them into the $_GET and $_REQUEST arrays
* For example,
* sake my/url somearg otherarg key=val --otherkey=val third=val&fourth=val
*
* Will result in the following get data:
* args => array('somearg', 'otherarg'),
* key => val
* otherkey => val
* third => val
* fourth => val
*/
if (isset($variables['_SERVER']['argv'][2])) {
$args = array_slice($variables['_SERVER']['argv'], 2);
foreach ($args as $arg) {
if (strpos($arg, '=') == false) {
$variables['_GET']['args'][] = $arg;
} else {
$newItems = array();
parse_str((substr($arg, 0, 2) == '--') ? substr($arg, 2) : $arg, $newItems);
$variables['_GET'] = array_merge($variables['_GET'], $newItems);
}
}
$_REQUEST = array_merge($_REQUEST, $variables['_GET']);
}
// Set 'url' GET parameter
if (isset($variables['_SERVER']['argv'][1])) {
$variables['_GET']['url'] = $variables['_SERVER']['argv'][1];
$variables['_SERVER']['REQUEST_URI'] = $variables['_SERVER']['argv'][1];
}
// Parse rest of variables as standard
return parent::cleanEnvironment($variables);
}
}

View File

@ -3,9 +3,7 @@
namespace SilverStripe\Control; namespace SilverStripe\Control;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Debug; use SilverStripe\Dev\Debug;
use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Security\BasicAuth; use SilverStripe\Security\BasicAuth;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
@ -47,13 +45,6 @@ class Controller extends RequestHandler implements TemplateGlobalProvider
*/ */
protected $action; protected $action;
/**
* The {@link Session} object for this controller.
*
* @var Session
*/
protected $session;
/** /**
* Stack of current controllers. Controller::$controller_stack[0] is the current controller. * Stack of current controllers. Controller::$controller_stack[0] is the current controller.
* *
@ -61,6 +52,14 @@ class Controller extends RequestHandler implements TemplateGlobalProvider
*/ */
protected static $controller_stack = array(); protected static $controller_stack = array();
/**
* Assign templates for this controller.
* Map of action => template name
*
* @var array
*/
protected $templates = [];
/** /**
* @var bool * @var bool
*/ */
@ -152,16 +151,14 @@ class Controller extends RequestHandler implements TemplateGlobalProvider
* @todo setDataModel and setRequest are redundantly called in parent::handleRequest() - sort this out * @todo setDataModel and setRequest are redundantly called in parent::handleRequest() - sort this out
* *
* @param HTTPRequest $request * @param HTTPRequest $request
* @param DataModel $model
*/ */
protected function beforeHandleRequest(HTTPRequest $request, DataModel $model) protected function beforeHandleRequest(HTTPRequest $request)
{ {
//Set up the internal dependencies (request, response)
$this->setRequest($request);
//Push the current controller to protect against weird session issues //Push the current controller to protect against weird session issues
$this->pushCurrent(); $this->pushCurrent();
//Set up the internal dependencies (request, response, datamodel)
$this->setRequest($request);
$this->setResponse(new HTTPResponse()); $this->setResponse(new HTTPResponse());
$this->setDataModel($model);
//kick off the init functionality //kick off the init functionality
$this->doInit(); $this->doInit();
} }
@ -192,24 +189,22 @@ class Controller extends RequestHandler implements TemplateGlobalProvider
* and end the method with $this->afterHandleRequest() * and end the method with $this->afterHandleRequest()
* *
* @param HTTPRequest $request * @param HTTPRequest $request
* @param DataModel $model
*
* @return HTTPResponse * @return HTTPResponse
*/ */
public function handleRequest(HTTPRequest $request, DataModel $model) public function handleRequest(HTTPRequest $request)
{ {
if (!$request) { if (!$request) {
user_error("Controller::handleRequest() not passed a request!", E_USER_ERROR); user_error("Controller::handleRequest() not passed a request!", E_USER_ERROR);
} }
//set up the controller for the incoming request //set up the controller for the incoming request
$this->beforeHandleRequest($request, $model); $this->beforeHandleRequest($request);
//if the before handler manipulated the response in a way that we shouldn't proceed, then skip our request //if the before handler manipulated the response in a way that we shouldn't proceed, then skip our request
// handling // handling
if (!$this->getResponse()->isFinished()) { if (!$this->getResponse()->isFinished()) {
//retrieve the response for the request //retrieve the response for the request
$response = parent::handleRequest($request, $model); $response = parent::handleRequest($request);
//prepare the response (we can receive an assortment of response types (strings/objects/HTTPResponses) //prepare the response (we can receive an assortment of response types (strings/objects/HTTPResponses)
$this->prepareResponse($response); $this->prepareResponse($response);
@ -520,7 +515,7 @@ class Controller extends RequestHandler implements TemplateGlobalProvider
$template = $this->getViewer($this->getAction()); $template = $this->getViewer($this->getAction());
// if the object is already customised (e.g. through Controller->run()), use it // if the object is already customised (e.g. through Controller->run()), use it
$obj = ($this->customisedObj) ? $this->customisedObj : $this; $obj = $this->getCustomisedObj() ?: $this;
if ($params) { if ($params) {
$obj = $this->customise($params); $obj = $this->customise($params);
@ -593,18 +588,15 @@ class Controller extends RequestHandler implements TemplateGlobalProvider
* Pushes this controller onto the stack of current controllers. This means that any redirection, * Pushes this controller onto the stack of current controllers. This means that any redirection,
* session setting, or other things that rely on Controller::curr() will now write to this * session setting, or other things that rely on Controller::curr() will now write to this
* controller object. * controller object.
*
* Note: Ensure this controller is assigned a request with a valid session before pushing
* it to the stack.
*/ */
public function pushCurrent() public function pushCurrent()
{ {
// Ensure this controller has a valid session
$this->getRequest()->getSession();
array_unshift(self::$controller_stack, $this); array_unshift(self::$controller_stack, $this);
// Create a new session object
if (!$this->session) {
if (isset(self::$controller_stack[1])) {
$this->session = self::$controller_stack[1]->getSession();
} else {
$this->session = Injector::inst()->create('SilverStripe\\Control\\Session', array());
}
}
} }
/** /**
@ -653,26 +645,6 @@ class Controller extends RequestHandler implements TemplateGlobalProvider
return $this->getResponse() && $this->getResponse()->getHeader('Location'); return $this->getResponse() && $this->getResponse()->getHeader('Location');
} }
/**
* Get the Session object representing this Controller's session.
*
* @return Session
*/
public function getSession()
{
return $this->session;
}
/**
* Set the Session object.
*
* @param Session $session
*/
public function setSession(Session $session)
{
$this->session = $session;
}
/** /**
* Joins two or more link segments together, putting a slash between them if necessary. Use this * Joins two or more link segments together, putting a slash between them if necessary. Use this
* for building the results of {@link Link()} methods. If either of the links have query strings, * for building the results of {@link Link()} methods. If either of the links have query strings,

View File

@ -2,15 +2,12 @@
namespace SilverStripe\Control; namespace SilverStripe\Control;
use InvalidArgumentException;
use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Environment;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Kernel;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\ArrayLib;
use SilverStripe\ORM\DataModel;
use SilverStripe\Versioned\Versioned; use SilverStripe\Versioned\Versioned;
use SilverStripe\View\Requirements; use SilverStripe\View\Requirements;
use SilverStripe\View\Requirements_Backend; use SilverStripe\View\Requirements_Backend;
@ -120,112 +117,40 @@ class Director implements TemplateGlobalProvider
* *
* @uses handleRequest() rule-lookup logic is handled by this. * @uses handleRequest() rule-lookup logic is handled by this.
* @uses TestController::handleRequest() This handles the page logic for a Director::direct() call. * @uses TestController::handleRequest() This handles the page logic for a Director::direct() call.
* @param string $url * @param HTTPRequest $request
* @param DataModel $model * @return HTTPResponse
* @throws HTTPResponse_Exception * @throws HTTPResponse_Exception
*/ */
public static function direct($url, DataModel $model) public static function direct(HTTPRequest $request)
{ {
// check allowed hosts // check allowed hosts
if (getenv('SS_ALLOWED_HOSTS') && !Director::is_cli()) { if (getenv('SS_ALLOWED_HOSTS') && !static::is_cli()) {
$all_allowed_hosts = explode(',', getenv('SS_ALLOWED_HOSTS')); $allowedHosts = explode(',', getenv('SS_ALLOWED_HOSTS'));
if (!in_array(static::host(), $all_allowed_hosts)) { if (!in_array(static::host(), $allowedHosts)) {
throw new HTTPResponse_Exception('Invalid Host', 400); return new HTTPResponse('Invalid Host', 400);
} }
} }
// Pre-request
// Validate $_FILES array before merging it with $_POST $output = RequestProcessor::singleton()->preRequest($request);
foreach ($_FILES as $k => $v) {
if (is_array($v['tmp_name'])) {
$v = ArrayLib::array_values_recursive($v['tmp_name']);
foreach ($v as $tmpFile) {
if ($tmpFile && !is_uploaded_file($tmpFile)) {
user_error("File upload '$k' doesn't appear to be a valid upload", E_USER_ERROR);
}
}
} else {
if ($v['tmp_name'] && !is_uploaded_file($v['tmp_name'])) {
user_error("File upload '$k' doesn't appear to be a valid upload", E_USER_ERROR);
}
}
}
$req = new HTTPRequest(
(isset($_SERVER['X-HTTP-Method-Override']))
? $_SERVER['X-HTTP-Method-Override']
: $_SERVER['REQUEST_METHOD'],
$url,
$_GET,
ArrayLib::array_merge_recursive((array) $_POST, (array) $_FILES),
@file_get_contents('php://input')
);
$headers = self::extract_request_headers($_SERVER);
foreach ($headers as $header => $value) {
$req->addHeader($header, $value);
}
// Initiate an empty session - doesn't initialize an actual PHP session until saved (see below)
$session = Session::create(isset($_SESSION) ? $_SESSION : array());
// Only resume a session if its not started already, and a session identifier exists
if (!isset($_SESSION) && Session::request_contains_session_id()) {
$session->inst_start();
}
$output = RequestProcessor::singleton()->preRequest($req, $session, $model);
if ($output === false) { if ($output === false) {
// @TODO Need to NOT proceed with the request in an elegant manner return new HTTPResponse(_t(__CLASS__.'.INVALID_REQUEST', 'Invalid request'), 400);
throw new HTTPResponse_Exception(_t('SilverStripe\\Control\\Director.INVALID_REQUEST', 'Invalid request'), 400);
} }
$result = Director::handleRequest($req, $session, $model); // Generate output
$result = static::handleRequest($request);
// Save session data. Note that inst_save() will start/resume the session if required. // Save session data. Note that save() will start/resume the session if required.
$session->inst_save(); $request->getSession()->save();
// Return code for a redirection request // Post-request handling
if (is_string($result) && substr($result, 0, 9) == 'redirect:') { $postRequest = RequestProcessor::singleton()->postRequest($request, $result);
$url = substr($result, 9); if ($postRequest === false) {
return new HTTPResponse(_t(__CLASS__ . '.REQUEST_ABORTED', 'Request aborted'), 500);
if (Director::is_cli()) {
// on cli, follow SilverStripe redirects automatically
Director::direct(
str_replace(Director::absoluteBaseURL(), '', $url),
DataModel::inst()
);
return;
} else {
$response = new HTTPResponse();
$response->redirect($url);
$res = RequestProcessor::singleton()->postRequest($req, $response, $model);
if ($res !== false) {
$response->output();
}
}
// Handle a controller
} elseif ($result) {
if ($result instanceof HTTPResponse) {
$response = $result;
} else {
$response = new HTTPResponse();
$response->setBody($result);
} }
$res = RequestProcessor::singleton()->postRequest($req, $response, $model); // Return
if ($res !== false) { return $result;
$response->output();
} else {
// @TODO Proper response here.
throw new HTTPResponse_Exception("Invalid response");
}
//$controllerObj->getSession()->inst_save();
}
} }
/** /**
@ -253,7 +178,7 @@ class Director implements TemplateGlobalProvider
*/ */
public static function test( public static function test(
$url, $url,
$postVars = null, $postVars = [],
$session = array(), $session = array(),
$httpMethod = null, $httpMethod = null,
$body = null, $body = null,
@ -261,132 +186,157 @@ class Director implements TemplateGlobalProvider
$cookies = array(), $cookies = array(),
&$request = null &$request = null
) { ) {
return static::mockRequest(
function (HTTPRequest $request) {
return static::direct($request);
},
$url,
$postVars,
$session,
$httpMethod,
$body,
$headers,
$cookies,
$request
);
}
Config::nest(); /**
Injector::nest(); * Mock a request, passing this to the given callback, before resetting.
*
* @param callable $callback Action to pass the HTTPRequst object
* @param string $url The URL to build
* @param array $postVars The $_POST & $_FILES variables.
* @param array|Session $session The {@link Session} object representing the current session.
* By passing the same object to multiple calls of Director::test(), you can simulate a persisted
* session.
* @param string $httpMethod The HTTP method, such as GET or POST. It will default to POST if
* postVars is set, GET otherwise. Overwritten by $postVars['_method'] if present.
* @param string $body The HTTP body.
* @param array $headers HTTP headers with key-value pairs.
* @param array|Cookie_Backend $cookies to populate $_COOKIE.
* @param HTTPRequest $request The {@see SS_HTTP_Request} object generated as a part of this request.
* @return mixed Result of callback
*/
public static function mockRequest(
$callback,
$url,
$postVars = [],
$session = [],
$httpMethod = null,
$body = null,
$headers = [],
$cookies = [],
&$request = null
) {
// Build list of cleanup promises
$finally = [];
/** @var Kernel $kernel */
$kernel = Injector::inst()->get(Kernel::class);
$kernel->nest();
$finally[] = function () use ($kernel) {
$kernel->activate();
};
// backup existing vars, and create new vars
$existingVars = Environment::getVariables();
$finally[] = function () use ($existingVars) {
Environment::setVariables($existingVars);
};
$newVars = $existingVars;
// These are needed so that calling Director::test() does not muck with whoever is calling it. // These are needed so that calling Director::test() does not muck with whoever is calling it.
// Really, it's some inappropriate coupling and should be resolved by making less use of statics. // Really, it's some inappropriate coupling and should be resolved by making less use of statics.
$oldReadingMode = null;
if (class_exists(Versioned::class)) { if (class_exists(Versioned::class)) {
$oldReadingMode = Versioned::get_reading_mode(); $oldReadingMode = Versioned::get_reading_mode();
} $finally[] = function () use ($oldReadingMode) {
$getVars = array(); Versioned::set_reading_mode($oldReadingMode);
};
if (!$httpMethod) {
$httpMethod = ($postVars || is_array($postVars)) ? "POST" : "GET";
} }
if (!$session) { // Default httpMethod
$session = Session::create([]); $newVars['_SERVER']['REQUEST_METHOD'] = $httpMethod ?: ($postVars ? "POST" : "GET");
$newVars['_POST'] = (array)$postVars;
// Setup session
if ($session instanceof Session) {
// Note: If passing $session as object, ensure that changes are written back
// This is important for classes such as FunctionalTest which emulate cross-request persistence
$newVars['_SESSION'] = $session->getAll();
$finally[] = function () use ($session) {
if (isset($_SESSION)) {
foreach ($_SESSION as $key => $value) {
$session->set($key, $value);
} }
}
};
} else {
$newVars['_SESSION'] = $session ?: [];
}
// Setup cookies
$cookieJar = $cookies instanceof Cookie_Backend $cookieJar = $cookies instanceof Cookie_Backend
? $cookies ? $cookies
: Injector::inst()->createWithArgs(Cookie_Backend::class, array($cookies ?: [])); : Injector::inst()->createWithArgs(Cookie_Backend::class, array($cookies ?: []));
$newVars['_COOKIE'] = $cookieJar->getAll(false);
// Back up the current values of the superglobals
$existingRequestVars = isset($_REQUEST) ? $_REQUEST : array();
$existingGetVars = isset($_GET) ? $_GET : array();
$existingPostVars = isset($_POST) ? $_POST : array();
$existingSessionVars = isset($_SESSION) ? $_SESSION : array();
$existingCookies = isset($_COOKIE) ? $_COOKIE : array();
$existingServer = isset($_SERVER) ? $_SERVER : array();
$existingRequirementsBackend = Requirements::backend();
Cookie::config()->update('report_errors', false); Cookie::config()->update('report_errors', false);
Requirements::set_backend(Requirements_Backend::create()); Injector::inst()->registerService($cookieJar, Cookie_Backend::class);
if (strpos($url, '#') !== false) { // Backup requirements
$url = substr($url, 0, strpos($url, '#')); $existingRequirementsBackend = Requirements::backend();
} Requirements::set_backend(Requirements_Backend::create());
$finally[] = function () use ($existingRequirementsBackend) {
Requirements::set_backend($existingRequirementsBackend);
};
// Strip any hash
$url = strtok($url, '#');
// Handle absolute URLs // Handle absolute URLs
if (parse_url($url, PHP_URL_HOST)) { if (parse_url($url, PHP_URL_HOST)) {
$bits = parse_url($url); $bits = parse_url($url);
// If a port is mentioned in the absolute URL, be sure to add that into the HTTP host // If a port is mentioned in the absolute URL, be sure to add that into the HTTP host
if (isset($bits['port'])) { $newVars['_SERVER']['HTTP_HOST'] = isset($bits['port'])
$_SERVER['HTTP_HOST'] = $bits['host'].':'.$bits['port']; ? $bits['host'].':'.$bits['port']
} else { : $bits['host'];
$_SERVER['HTTP_HOST'] = $bits['host'];
}
} }
// Ensure URL is properly made relative. // Ensure URL is properly made relative.
// Example: url passed is "/ss31/my-page" (prefixed with BASE_URL), this should be changed to "my-page" // Example: url passed is "/ss31/my-page" (prefixed with BASE_URL), this should be changed to "my-page"
$url = self::makeRelative($url); $url = self::makeRelative($url);
$urlWithQuerystring = $url;
if (strpos($url, '?') !== false) { if (strpos($url, '?') !== false) {
list($url, $getVarsEncoded) = explode('?', $url, 2); list($url, $getVarsEncoded) = explode('?', $url, 2);
parse_str($getVarsEncoded, $getVars); parse_str($getVarsEncoded, $newVars['_GET']);
} else {
$newVars['_GET'] = [];
} }
$newVars['_SERVER']['REQUEST_URI'] = Director::baseURL() . $url;
$newVars['_REQUEST'] = array_merge($newVars['_GET'], $newVars['_POST']);
// Replace the super globals with appropriate test values // Normalise vars
$_REQUEST = ArrayLib::array_merge_recursive((array) $getVars, (array) $postVars); $newVars = HTTPRequestBuilder::cleanEnvironment($newVars);
$_GET = (array) $getVars;
$_POST = (array) $postVars;
$_SESSION = $session ? $session->inst_getAll() : array();
$_COOKIE = $cookieJar->getAll(false);
Injector::inst()->registerService($cookieJar, Cookie_Backend::class);
$_SERVER['REQUEST_URI'] = Director::baseURL() . $urlWithQuerystring;
$request = new HTTPRequest($httpMethod, $url, $getVars, $postVars, $body); // Create new request
$request = HTTPRequestBuilder::createFromVariables($newVars, $body);
if ($headers) { if ($headers) {
foreach ($headers as $k => $v) { foreach ($headers as $k => $v) {
$request->addHeader($k, $v); $request->addHeader($k, $v);
} }
} }
// Apply new vars to environment
Environment::setVariables($newVars);
try { try {
// Pre-request filtering // Normal request handling
$model = DataModel::inst(); return call_user_func($callback, $request);
$requestProcessor = Injector::inst()->get(RequestProcessor::class);
$output = $requestProcessor->preRequest($request, $session, $model);
if ($output === false) {
throw new HTTPResponse_Exception(_t('SilverStripe\\Control\\Director.INVALID_REQUEST', 'Invalid request'), 400);
}
// Process request
$result = Director::handleRequest($request, $session, $model);
// Ensure that the result is an HTTPResponse object
if (is_string($result)) {
if (substr($result, 0, 9) == 'redirect:') {
$response = new HTTPResponse();
$response->redirect(substr($result, 9));
$result = $response;
} else {
$result = new HTTPResponse($result);
}
}
$output = $requestProcessor->postRequest($request, $result, $model);
if ($output === false) {
throw new HTTPResponse_Exception("Invalid response");
}
// Return valid response
return $result;
} finally { } finally {
// Restore the super globals // Restore state in reverse order to assignment
$_REQUEST = $existingRequestVars; foreach (array_reverse($finally) as $callback) {
$_GET = $existingGetVars; call_user_func($callback);
$_POST = $existingPostVars;
$_SESSION = $existingSessionVars;
$_COOKIE = $existingCookies;
$_SERVER = $existingServer;
Requirements::set_backend($existingRequirementsBackend);
// These are needed so that calling Director::test() does not muck with whoever is calling it.
// Really, it's some inappropriate coupling and should be resolved by making less use of statics
if (class_exists(Versioned::class)) {
Versioned::set_reading_mode($oldReadingMode);
} }
Injector::unnest(); // Restore old CookieJar, etc
Config::unnest();
} }
} }
@ -395,15 +345,14 @@ class Director implements TemplateGlobalProvider
* *
* @skipUpgrade * @skipUpgrade
* @param HTTPRequest $request * @param HTTPRequest $request
* @param Session $session * @return HTTPResponse
* @param DataModel $model
* @return HTTPResponse|string
*/ */
protected static function handleRequest(HTTPRequest $request, Session $session, DataModel $model) protected static function handleRequest(HTTPRequest $request)
{ {
$rules = Director::config()->uninherited('rules'); $rules = Director::config()->uninherited('rules');
foreach ($rules as $pattern => $controllerOptions) { foreach ($rules as $pattern => $controllerOptions) {
// Normalise route rule
if (is_string($controllerOptions)) { if (is_string($controllerOptions)) {
if (substr($controllerOptions, 0, 2) == '->') { if (substr($controllerOptions, 0, 2) == '->') {
$controllerOptions = array('Redirect' => substr($controllerOptions, 2)); $controllerOptions = array('Redirect' => substr($controllerOptions, 2));
@ -412,7 +361,9 @@ class Director implements TemplateGlobalProvider
} }
} }
if (($arguments = $request->match($pattern, true)) !== false) { // Match pattern
$arguments = $request->match($pattern, true);
if ($arguments !== false) {
$request->setRouteParams($controllerOptions); $request->setRouteParams($controllerOptions);
// controllerOptions provide some default arguments // controllerOptions provide some default arguments
$arguments = array_merge($controllerOptions, $arguments); $arguments = array_merge($controllerOptions, $arguments);
@ -424,24 +375,20 @@ class Director implements TemplateGlobalProvider
// Handle redirection // Handle redirection
if (isset($arguments['Redirect'])) { if (isset($arguments['Redirect'])) {
return "redirect:" . Director::absoluteURL($arguments['Redirect'], true); // Redirection
} else { $response = new HTTPResponse();
$response->redirect(static::absoluteURL($arguments['Redirect']));
return $response;
}
// Find the controller name // Find the controller name
$controller = $arguments['Controller']; $controller = $arguments['Controller'];
$controllerObj = Injector::inst()->create($controller); $controllerObj = Injector::inst()->create($controller);
$controllerObj->setSession($session);
try { try {
$result = $controllerObj->handleRequest($request, $model); return $controllerObj->handleRequest($request);
} catch (HTTPResponse_Exception $responseException) { } catch (HTTPResponse_Exception $responseException) {
$result = $responseException->getResponse(); return $responseException->getResponse();
}
if (!is_object($result) || $result instanceof HTTPResponse) {
return $result;
}
user_error("Bad result from url " . $request->getURL() . " handled by " .
get_class($controllerObj)." controller: ".get_class($result), E_USER_WARNING);
} }
} }
} }
@ -852,36 +799,6 @@ class Director implements TemplateGlobalProvider
} }
} }
/**
* Takes a $_SERVER data array and extracts HTTP request headers.
*
* @param array $server
*
* @return array
*/
public static function extract_request_headers(array $server)
{
$headers = array();
foreach ($server as $key => $value) {
if (substr($key, 0, 5) == 'HTTP_') {
$key = substr($key, 5);
$key = strtolower(str_replace('_', ' ', $key));
$key = str_replace(' ', '-', ucwords($key));
$headers[$key] = $value;
}
}
if (isset($server['CONTENT_TYPE'])) {
$headers['Content-Type'] = $server['CONTENT_TYPE'];
}
if (isset($server['CONTENT_LENGTH'])) {
$headers['Content-Length'] = $server['CONTENT_LENGTH'];
}
return $headers;
}
/** /**
* Given a filesystem reference relative to the site root, return the full file-system path. * Given a filesystem reference relative to the site root, return the full file-system path.
* *
@ -942,17 +859,15 @@ class Director implements TemplateGlobalProvider
* Skip any further processing and immediately respond with a redirect to the passed URL. * Skip any further processing and immediately respond with a redirect to the passed URL.
* *
* @param string $destURL * @param string $destURL
* @throws HTTPResponse_Exception
*/ */
protected static function force_redirect($destURL) protected static function force_redirect($destURL)
{ {
// Redirect to installer
$response = new HTTPResponse(); $response = new HTTPResponse();
$response->redirect($destURL, 301); $response->redirect($destURL, 301);
HTTP::add_cache_headers($response); HTTP::add_cache_headers($response);
throw new HTTPResponse_Exception($response);
// TODO: Use an exception - ATM we can be called from _config.php, before Director#handleRequest's try block
$response->output();
die;
} }
/** /**
@ -983,19 +898,23 @@ class Director implements TemplateGlobalProvider
* *
* @param array $patterns Array of regex patterns to match URLs that should be HTTPS. * @param array $patterns Array of regex patterns to match URLs that should be HTTPS.
* @param string $secureDomain Secure domain to redirect to. Defaults to the current domain. * @param string $secureDomain Secure domain to redirect to. Defaults to the current domain.
* * @return bool true if already on SSL, false if doesn't match patterns (or cannot redirect)
* @return bool|string String of URL when unit tests running, boolean FALSE if patterns don't match request URI. * @throws HTTPResponse_Exception Throws exception with redirect, if successful
*/ */
public static function forceSSL($patterns = null, $secureDomain = null) public static function forceSSL($patterns = null, $secureDomain = null)
{ {
// Calling from the command-line? // Already on SSL
if (static::is_https()) {
return true;
}
// Can't redirect without a url
if (!isset($_SERVER['REQUEST_URI'])) { if (!isset($_SERVER['REQUEST_URI'])) {
return false; return false;
} }
$matched = false;
if ($patterns) { if ($patterns) {
$matched = false;
$relativeURL = self::makeRelative(Director::absoluteURL($_SERVER['REQUEST_URI'])); $relativeURL = self::makeRelative(Director::absoluteURL($_SERVER['REQUEST_URI']));
// protect portions of the site based on the pattern // protect portions of the site based on the pattern
@ -1005,33 +924,22 @@ class Director implements TemplateGlobalProvider
break; break;
} }
} }
} else { if (!$matched) {
// protect the entire site
$matched = true;
}
if ($matched && !self::is_https()) {
// if an domain is specified, redirect to that instead of the current domain
if ($secureDomain) {
$url = 'https://' . $secureDomain . $_SERVER['REQUEST_URI'];
} else {
$url = $_SERVER['REQUEST_URI'];
}
$destURL = str_replace('http:', 'https:', Director::absoluteURL($url));
// This coupling to SapphireTest is necessary to test the destination URL and to not interfere with tests
if (class_exists('SilverStripe\\Dev\\SapphireTest', false) && SapphireTest::is_running_test()) {
return $destURL;
} else {
self::force_redirect($destURL);
return true;
}
} else {
return false; return false;
} }
} }
// if an domain is specified, redirect to that instead of the current domain
if (!$secureDomain) {
$secureDomain = static::host();
}
$url = 'https://' . $secureDomain . $_SERVER['REQUEST_URI'];
// Force redirect
self::force_redirect($url);
return true;
}
/** /**
* Force a redirect to a domain starting with "www." * Force a redirect to a domain starting with "www."
*/ */
@ -1073,47 +981,7 @@ class Director implements TemplateGlobalProvider
*/ */
public static function is_cli() public static function is_cli()
{ {
return (php_sapi_name() == "cli"); return php_sapi_name() === "cli";
}
/**
* Set the environment type of the current site.
*
* Typically, a SilverStripe site have a number of environments:
* - Development environments, such a copy on your local machine.
* - Test sites, such as the one you show the client before going live.
* - The live site itself.
*
* The behaviour of these environments often varies slightly. For example, development sites may
* have errors dumped to the screen, and order confirmation emails might be sent to the developer
* instead of the client.
*
* To help with this, SilverStripe supports the notion of an environment type. The environment
* type can be dev, test, or live.
*
* Dev mode can also be forced by putting ?isDev=1 in your URL, which will ask you to log in and
* then push the site into dev mode for the remainder of the session. Putting ?isDev=0 onto the URL
* can turn it back.
*
* Test mode can also be forced by putting ?isTest=1 in your URL, which will ask you to log in and
* then push the site into test mode for the remainder of the session. Putting ?isTest=0 onto the URL
* can turn it back.
*
* Generally speaking, these methods will be called from your _config.php file.
*
* Once the environment type is set, it can be checked with {@link Director::isDev()},
* {@link Director::isTest()}, and {@link Director::isLive()}.
*
* @param string $environment
*/
public static function set_environment_type($environment)
{
if (!in_array($environment, ['dev', 'test', 'live'])) {
throw new InvalidArgumentException(
"Director::set_environment_type passed '$environment'. It should be passed dev, test, or live"
);
}
self::$environment_type = $environment;
} }
/** /**
@ -1124,22 +992,9 @@ class Director implements TemplateGlobalProvider
*/ */
public static function get_environment_type() public static function get_environment_type()
{ {
// Check saved session /** @var Kernel $kernel */
if ($env = self::session_environment()) { $kernel = Injector::inst()->get(Kernel::class);
return $env; return $kernel->getEnvironment();
}
// Check set
if (self::$environment_type) {
return self::$environment_type;
}
// Check getenv
if ($env = getenv('SS_ENVIRONMENT_TYPE')) {
return $env;
}
return 'live';
} }
/** /**
@ -1175,37 +1030,6 @@ class Director implements TemplateGlobalProvider
return self::get_environment_type() === 'test'; return self::get_environment_type() === 'test';
} }
/**
* Check or update any temporary environment specified in the session.
*
* @return null|string
*/
public static function session_environment()
{
// Set session from querystring
if (isset($_GET['isDev'])) {
if (isset($_SESSION)) {
unset($_SESSION['isTest']); // In case we are changing from test mode
$_SESSION['isDev'] = $_GET['isDev'];
}
return 'dev';
} elseif (isset($_GET['isTest'])) {
if (isset($_SESSION)) {
unset($_SESSION['isDev']); // In case we are changing from dev mode
$_SESSION['isTest'] = $_GET['isTest'];
}
return 'test';
}
// Check session
if (isset($_SESSION['isDev']) && $_SESSION['isDev']) {
return 'dev';
} elseif (isset($_SESSION['isTest']) && $_SESSION['isTest']) {
return 'test';
} else {
return null;
}
}
/** /**
* Returns an array of strings of the method names of methods on the call that should be exposed * Returns an array of strings of the method names of methods on the call that should be exposed
* as global variables in the templates. * as global variables in the templates.

View File

@ -96,6 +96,72 @@ class Email extends ViewableData
return \Swift_Validate::email($address); return \Swift_Validate::email($address);
} }
/**
* Get send_all_emails_to
*
* @return array Keys are addresses, values are names
*/
public static function getSendAllEmailsTo()
{
return static::mergeConfiguredEmails('send_all_emails_to', 'SS_SEND_ALL_EMAILS_TO');
}
/**
* Get cc_all_emails_to
*
* @return array
*/
public static function getCCAllEmailsTo()
{
return static::mergeConfiguredEmails('cc_all_emails_to', 'SS_CC_ALL_EMAILS_TO');
}
/**
* Get bcc_all_emails_to
*
* @return array
*/
public static function getBCCAllEmailsTo()
{
return static::mergeConfiguredEmails('bcc_all_emails_to', 'SS_BCC_ALL_EMAILS_TO');
}
/**
* Get send_all_emails_from
*
* @return array
*/
public static function getSendAllEmailsFrom()
{
return static::mergeConfiguredEmails('send_all_emails_from', 'SS_SEND_ALL_EMAILS_FROM');
}
/**
* Normalise email list from config merged with env vars
*
* @param string $config Config key
* @param string $env Env variable key
* @return array Array of email addresses
*/
protected static function mergeConfiguredEmails($config, $env)
{
// Normalise config list
$normalised = [];
$source = (array)static::config()->get($config);
foreach ($source as $address => $name) {
if ($address && !is_numeric($address)) {
$normalised[$address] = $name;
} elseif ($name) {
$normalised[$name] = null;
}
}
$extra = getenv($env);
if ($extra) {
$normalised[$extra] = null;
}
return $normalised;
}
/** /**
* Encode an email-address to protect it from spambots. * Encode an email-address to protect it from spambots.
* At the moment only simple string substitutions, * At the moment only simple string substitutions,
@ -197,7 +263,7 @@ class Email extends ViewableData
public function setSwiftMessage($swiftMessage) public function setSwiftMessage($swiftMessage)
{ {
$swiftMessage->setDate(DBDatetime::now()->getTimestamp()); $swiftMessage->setDate(DBDatetime::now()->getTimestamp());
if (!$swiftMessage->getFrom() && ($defaultFrom = $this->config()->admin_email)) { if (!$swiftMessage->getFrom() && ($defaultFrom = $this->config()->get('admin_email'))) {
$swiftMessage->setFrom($defaultFrom); $swiftMessage->setFrom($defaultFrom);
} }
$this->swiftMessage = $swiftMessage; $this->swiftMessage = $swiftMessage;
@ -238,7 +304,7 @@ class Email extends ViewableData
} }
/** /**
* @return array * @return string
*/ */
public function getSender() public function getSender()
{ {

View File

@ -11,36 +11,29 @@ class SwiftPlugin implements \Swift_Events_SendListener
*/ */
public function beforeSendPerformed(\Swift_Events_SendEvent $evt) public function beforeSendPerformed(\Swift_Events_SendEvent $evt)
{ {
/** @var \Swift_Message $message */ /** @var \Swift_Message $message */
$message = $evt->getMessage(); $message = $evt->getMessage();
$sendAllTo = Email::config()->send_all_emails_to;
$ccAllTo = Email::config()->cc_all_emails_to;
$bccAllTo = Email::config()->bcc_all_emails_to;
$sendAllFrom = Email::config()->send_all_emails_from;
$sendAllTo = Email::getSendAllEmailsTo();
if (!empty($sendAllTo)) { if (!empty($sendAllTo)) {
$this->setTo($message, $sendAllTo); $this->setTo($message, $sendAllTo);
} }
$ccAllTo = Email::getCCAllEmailsTo();
if (!empty($ccAllTo)) { if (!empty($ccAllTo)) {
if (!is_array($ccAllTo)) {
$ccAllTo = array($ccAllTo => null);
}
foreach ($ccAllTo as $address => $name) { foreach ($ccAllTo as $address => $name) {
$message->addCc($address, $name); $message->addCc($address, $name);
} }
} }
$bccAllTo = Email::getBCCAllEmailsTo();
if (!empty($bccAllTo)) { if (!empty($bccAllTo)) {
if (!is_array($bccAllTo)) {
$bccAllTo = array($bccAllTo => null);
}
foreach ($bccAllTo as $address => $name) { foreach ($bccAllTo as $address => $name) {
$message->addBcc($address, $name); $message->addBcc($address, $name);
} }
} }
$sendAllFrom = Email::getSendAllEmailsFrom();
if (!empty($sendAllFrom)) { if (!empty($sendAllFrom)) {
$this->setFrom($message, $sendAllFrom); $this->setFrom($message, $sendAllFrom);
} }
@ -48,7 +41,7 @@ class SwiftPlugin implements \Swift_Events_SendListener
/** /**
* @param \Swift_Mime_Message $message * @param \Swift_Mime_Message $message
* @param string $to * @param array|string $to
*/ */
protected function setTo($message, $to) protected function setTo($message, $to)
{ {
@ -70,7 +63,7 @@ class SwiftPlugin implements \Swift_Events_SendListener
/** /**
* @param \Swift_Mime_Message $message * @param \Swift_Mime_Message $message
* @param string $from * @param array|string $from
*/ */
protected function setFrom($message, $from) protected function setFrom($message, $from)
{ {

View File

@ -3,7 +3,6 @@
namespace SilverStripe\Control; namespace SilverStripe\Control;
use SilverStripe\Core\Flushable; use SilverStripe\Core\Flushable;
use SilverStripe\ORM\DataModel;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
/** /**
@ -11,37 +10,17 @@ use SilverStripe\Core\ClassInfo;
*/ */
class FlushRequestFilter implements RequestFilter class FlushRequestFilter implements RequestFilter
{ {
public function preRequest(HTTPRequest $request)
/**
* @inheritdoc
*
* @param HTTPRequest $request
* @param Session $session
* @param DataModel $model
*
* @return bool
*/
public function preRequest(HTTPRequest $request, Session $session, DataModel $model)
{ {
if (array_key_exists('flush', $request->getVars())) { if (array_key_exists('flush', $request->getVars())) {
foreach (ClassInfo::implementorsOf(Flushable::class) as $class) { foreach (ClassInfo::implementorsOf(Flushable::class) as $class) {
$class::flush(); $class::flush();
} }
} }
return true; return true;
} }
/** public function postRequest(HTTPRequest $request, HTTPResponse $response)
* @inheritdoc
*
* @param HTTPRequest $request
* @param HTTPResponse $response
* @param DataModel $model
*
* @return bool
*/
public function postRequest(HTTPRequest $request, HTTPResponse $response, DataModel $model)
{ {
return true; return true;
} }

View File

@ -70,7 +70,7 @@ class HTTP
*/ */
public static function absoluteURLs($html) public static function absoluteURLs($html)
{ {
$html = str_replace('$CurrentPageURL', $_SERVER['REQUEST_URI'], $html); $html = str_replace('$CurrentPageURL', Controller::curr()->getRequest()->getURL(), $html);
return HTTP::urlRewriter($html, function ($url) { return HTTP::urlRewriter($html, function ($url) {
//no need to rewrite, if uri has a protocol (determined here by existence of reserved URI character ":") //no need to rewrite, if uri has a protocol (determined here by existence of reserved URI character ":")
if (preg_match('/^\w+:/', $url)) { if (preg_match('/^\w+:/', $url)) {
@ -163,14 +163,17 @@ class HTTP
* *
* @param string $varname * @param string $varname
* @param string $varvalue * @param string $varvalue
* @param string $currentURL Relative or absolute URL. * @param string|null $currentURL Relative or absolute URL, or HTTPRequest to get url from
* @param string $separator Separator for http_build_query(). * @param string $separator Separator for http_build_query().
*
* @return string * @return string
*/ */
public static function setGetVar($varname, $varvalue, $currentURL = null, $separator = '&amp;') public static function setGetVar($varname, $varvalue, $currentURL = null, $separator = '&amp;')
{ {
$uri = $currentURL ? $currentURL : Director::makeRelative($_SERVER['REQUEST_URI']); if (!isset($currentURL)) {
$request = Controller::curr()->getRequest();
$currentURL = $request->getURL(true);
}
$uri = $currentURL;
$isRelative = false; $isRelative = false;
// We need absolute URLs for parse_url() // We need absolute URLs for parse_url()

View File

@ -0,0 +1,127 @@
<?php
namespace SilverStripe\Control;
use SilverStripe\Core\Application;
use SilverStripe\Control\HTTPMiddleware;
use SilverStripe\Core\Kernel;
/**
* Invokes the HTTP application within an ErrorControlChain
*/
class HTTPApplication implements Application
{
/**
* @var HTTPMiddleware[]
*/
protected $middlewares = [];
/**
* @var Kernel
*/
protected $kernel;
public function __construct(Kernel $kernel)
{
$this->kernel = $kernel;
}
/**
* @return HTTPMiddleware[]
*/
public function getMiddlewares()
{
return $this->middlewares;
}
/**
* @param HTTPMiddleware[] $middlewares
* @return $this
*/
public function setMiddlewares($middlewares)
{
$this->middlewares = $middlewares;
return $this;
}
/**
* @param HTTPMiddleware $middleware
* @return $this
*/
public function addMiddleware(HTTPMiddleware $middleware)
{
$this->middlewares[] = $middleware;
return $this;
}
/**
* Call middleware
*
* @param HTTPRequest $request
* @param callable $last Last config to call
* @return HTTPResponse
*/
protected function callMiddleware(HTTPRequest $request, $last)
{
// Reverse middlewares
$next = $last;
/** @var HTTPMiddleware $middleware */
foreach (array_reverse($this->getMiddlewares()) as $middleware) {
$next = function ($request) use ($middleware, $next) {
return $middleware->process($request, $next);
};
}
return call_user_func($next, $request);
}
/**
* Get the kernel for this application
*
* @return Kernel
*/
public function getKernel()
{
return $this->kernel;
}
/**
* Handle the given HTTP request
*
* @param HTTPRequest $request
* @return HTTPResponse
*/
public function handle(HTTPRequest $request)
{
$flush = $request->getVar('flush') || strpos($request->getURL(), 'dev/build') === 0;
// Ensure boot is invoked
return $this->execute($request, function (HTTPRequest $request) {
// Start session and execute
$request->getSession()->init();
return Director::direct($request);
}, $flush);
}
/**
* Safely boot the application and execute the given main action
*
* @param HTTPRequest $request
* @param callable $callback
* @param bool $flush
* @return HTTPResponse
*/
public function execute(HTTPRequest $request, callable $callback, $flush = false)
{
try {
return $this->callMiddleware($request, function ($request) use ($callback, $flush) {
// Pre-request boot
$this->getKernel()->boot($flush);
return call_user_func($callback, $request);
});
} catch (HTTPResponse_Exception $ex) {
return $ex->getResponse();
} finally {
$this->getKernel()->shutdown();
}
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace SilverStripe\Control;
/**
* HTTP Request middleware
* Based on https://github.com/php-fig/fig-standards/blob/master/proposed/http-middleware/middleware.md#21-psrhttpservermiddlewaremiddlewareinterface
*/
interface HTTPMiddleware
{
/**
* Generate response for the given request
*
* @param HTTPRequest $request
* @param callable $delegate
* @return HTTPResponse
*/
public function process(HTTPRequest $request, callable $delegate);
}

View File

@ -2,9 +2,10 @@
namespace SilverStripe\Control; namespace SilverStripe\Control;
use ArrayAccess;
use BadMethodCallException;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\ORM\ArrayLib; use SilverStripe\ORM\ArrayLib;
use ArrayAccess;
/** /**
* Represents a HTTP-request, including a URL that is tokenised for parsing, and a request method * Represents a HTTP-request, including a URL that is tokenised for parsing, and a request method
@ -22,7 +23,6 @@ use ArrayAccess;
*/ */
class HTTPRequest implements ArrayAccess class HTTPRequest implements ArrayAccess
{ {
/** /**
* @var string * @var string
*/ */
@ -125,6 +125,11 @@ class HTTPRequest implements ArrayAccess
*/ */
protected $unshiftedButParsedParts = 0; protected $unshiftedButParsedParts = 0;
/**
* @var Session
*/
protected $session;
/** /**
* Construct a HTTPRequest from a URL relative to the site root. * Construct a HTTPRequest from a URL relative to the site root.
* *
@ -138,7 +143,6 @@ class HTTPRequest implements ArrayAccess
{ {
$this->httpMethod = strtoupper(self::detect_method($httpMethod, $postVars)); $this->httpMethod = strtoupper(self::detect_method($httpMethod, $postVars));
$this->setUrl($url); $this->setUrl($url);
$this->getVars = (array) $getVars; $this->getVars = (array) $getVars;
$this->postVars = (array) $postVars; $this->postVars = (array) $postVars;
$this->body = $body; $this->body = $body;
@ -435,21 +439,15 @@ class HTTPRequest implements ArrayAccess
return $this->requestVar($offset); return $this->requestVar($offset);
} }
/**
* @ignore
* @param string $offset
* @param mixed $value
*/
public function offsetSet($offset, $value) public function offsetSet($offset, $value)
{ {
$this->getVars[$offset] = $value;
} }
/**
* @ignore
* @param mixed $offset
*/
public function offsetUnset($offset) public function offsetUnset($offset)
{ {
unset($this->getVars[$offset]);
unset($this->postVars[$offset]);
} }
/** /**
@ -866,4 +864,25 @@ class HTTPRequest implements ArrayAccess
return $origMethod; return $origMethod;
} }
} }
/**
* @return Session
*/
public function getSession()
{
if (empty($this->session)) {
throw new BadMethodCallException("No session available for this HTTPRequest");
}
return $this->session;
}
/**
* @param Session $session
* @return $this
*/
public function setSession(Session $session)
{
$this->session = $session;
return $this;
}
} }

View File

@ -0,0 +1,148 @@
<?php
namespace SilverStripe\Control;
use SilverStripe\Core\Environment;
class HTTPRequestBuilder
{
/**
* Create HTTPRequest instance from the current environment variables.
* May throw errors if request is invalid.
*
* @throws HTTPResponse_Exception
* @return HTTPRequest
*/
public static function createFromEnvironment()
{
// Clean and update live global variables
$variables = static::cleanEnvironment(Environment::getVariables());
Environment::setVariables($variables); // Currently necessary for SSViewer, etc to work
// Health-check prior to creating environment
return static::createFromVariables($variables, @file_get_contents('php://input'));
}
/**
* Build HTTPRequest from given variables
*
* @param array $variables
* @param string $input Request body
* @return HTTPRequest
*/
public static function createFromVariables(array $variables, $input)
{
// Strip `url` out of querystring
$url = $variables['_GET']['url'];
unset($variables['_GET']['url']);
// Build request
$request = new HTTPRequest(
$variables['_SERVER']['REQUEST_METHOD'],
$url,
$variables['_GET'],
$variables['_POST'],
$input
);
// Add headers
$headers = static::extractRequestHeaders($variables['_SERVER']);
foreach ($headers as $header => $value) {
$request->addHeader($header, $value);
}
// Initiate an empty session - doesn't initialize an actual PHP session (see HTTPApplication)
$session = new Session(isset($variables['_SESSION']) ? $variables['_SESSION'] : null);
$request->setSession($session);
return $request;
}
/**
* Takes a $_SERVER data array and extracts HTTP request headers.
*
* @param array $server
*
* @return array
*/
public static function extractRequestHeaders(array $server)
{
$headers = array();
foreach ($server as $key => $value) {
if (substr($key, 0, 5) == 'HTTP_') {
$key = substr($key, 5);
$key = strtolower(str_replace('_', ' ', $key));
$key = str_replace(' ', '-', ucwords($key));
$headers[$key] = $value;
}
}
if (isset($server['CONTENT_TYPE'])) {
$headers['Content-Type'] = $server['CONTENT_TYPE'];
}
if (isset($server['CONTENT_LENGTH'])) {
$headers['Content-Length'] = $server['CONTENT_LENGTH'];
}
return $headers;
}
/**
* Clean up HTTP global vars for $_GET / $_REQUEST prior to bootstrapping
* Will also populate the $_GET['url'] var safely
*
* @param array $variables
* @return array Cleaned variables
*/
public static function cleanEnvironment(array $variables)
{
// IIS will sometimes generate this.
if (!empty($variables['_SERVER']['HTTP_X_ORIGINAL_URL'])) {
$variables['_SERVER']['REQUEST_URI'] = $variables['_SERVER']['HTTP_X_ORIGINAL_URL'];
}
// Override REQUEST_METHOD
if (isset($variables['_SERVER']['X-HTTP-Method-Override'])) {
$variables['_SERVER']['REQUEST_METHOD'] = $variables['_SERVER']['X-HTTP-Method-Override'];
}
// Prevent injection of url= querystring argument by prioritising any leading url argument
if (isset($variables['_SERVER']['QUERY_STRING']) &&
preg_match('/^(?<url>url=[^&?]*)(?<query>.*[&?]url=.*)$/', $variables['_SERVER']['QUERY_STRING'], $results)
) {
$queryString = $results['query'].'&'.$results['url'];
parse_str($queryString, $variables['_GET']);
}
// Decode url from REQUEST_URI if not passed via $_GET['url']
if (!isset($variables['_GET']['url'])) {
$url = $variables['_SERVER']['REQUEST_URI'];
// Querystring args need to be explicitly parsed
if (strpos($url, '?') !== false) {
list($url, $queryString) = explode('?', $url, 2);
parse_str($queryString);
}
// Ensure $_GET['url'] is set
$variables['_GET']['url'] = urldecode($url);
}
// Remove base folders from the URL if webroot is hosted in a subfolder
if (substr(strtolower($variables['_GET']['url']), 0, strlen(BASE_URL)) === strtolower(BASE_URL)) {
$variables['_GET']['url'] = substr($variables['_GET']['url'], strlen(BASE_URL));
}
// Merge $_FILES into $_POST
$variables['_POST'] = array_merge((array)$variables['_POST'], (array)$variables['_FILES']);
// Merge $_POST, $_GET, and $_COOKIE into $_REQUEST
$variables['_REQUEST'] = array_merge(
(array)$variables['_GET'],
(array)$variables['_POST'],
(array)$variables['_COOKIE']
);
return $variables;
}
}

View File

@ -2,8 +2,6 @@
namespace SilverStripe\Control; namespace SilverStripe\Control;
use SilverStripe\ORM\DataModel;
/** /**
* A request filter is an object that's executed before and after a * A request filter is an object that's executed before and after a
* request occurs. By returning 'false' from the preRequest method, * request occurs. By returning 'false' from the preRequest method,
@ -14,24 +12,20 @@ use SilverStripe\ORM\DataModel;
*/ */
interface RequestFilter interface RequestFilter
{ {
/** /**
* Filter executed before a request processes * Filter executed before a request processes
* *
* @param HTTPRequest $request Request container object * @param HTTPRequest $request Request container object
* @param Session $session Request session
* @param DataModel $model Current DataModel
* @return boolean Whether to continue processing other filters. Null or true will continue processing (optional) * @return boolean Whether to continue processing other filters. Null or true will continue processing (optional)
*/ */
public function preRequest(HTTPRequest $request, Session $session, DataModel $model); public function preRequest(HTTPRequest $request);
/** /**
* Filter executed AFTER a request * Filter executed AFTER a request
* *
* @param HTTPRequest $request Request container object * @param HTTPRequest $request Request container object
* @param HTTPResponse $response Response output object * @param HTTPResponse $response
* @param DataModel $model Current DataModel * @return bool Whether to continue processing other filters. Null or true will continue processing (optional)
* @return boolean Whether to continue processing other filters. Null or true will continue processing (optional)
*/ */
public function postRequest(HTTPRequest $request, HTTPResponse $response, DataModel $model); public function postRequest(HTTPRequest $request, HTTPResponse $response);
} }

View File

@ -6,7 +6,6 @@ use InvalidArgumentException;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\Debug; use SilverStripe\Dev\Debug;
use SilverStripe\ORM\DataModel;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Security\PermissionFailureException; use SilverStripe\Security\PermissionFailureException;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
@ -123,22 +122,9 @@ class RequestHandler extends ViewableData
$this->setRequest(new NullHTTPRequest()); $this->setRequest(new NullHTTPRequest());
// This will prevent bugs if setDataModel() isn't called.
$this->model = DataModel::inst();
parent::__construct(); parent::__construct();
} }
/**
* Set the DataModel for this request.
*
* @param DataModel $model
*/
public function setDataModel($model)
{
$this->model = $model;
}
/** /**
* Handles URL requests. * Handles URL requests.
* *
@ -156,10 +142,9 @@ class RequestHandler extends ViewableData
* customise the controller. * customise the controller.
* *
* @param HTTPRequest $request The object that is reponsible for distributing URL parsing * @param HTTPRequest $request The object that is reponsible for distributing URL parsing
* @param DataModel $model
* @return HTTPResponse|RequestHandler|string|array * @return HTTPResponse|RequestHandler|string|array
*/ */
public function handleRequest(HTTPRequest $request, DataModel $model) public function handleRequest(HTTPRequest $request)
{ {
// $handlerClass is used to step up the class hierarchy to implement url_handlers inheritance // $handlerClass is used to step up the class hierarchy to implement url_handlers inheritance
if ($this->brokenOnConstruct) { if ($this->brokenOnConstruct) {
@ -170,7 +155,6 @@ class RequestHandler extends ViewableData
} }
$this->setRequest($request); $this->setRequest($request);
$this->setDataModel($model);
$match = $this->findAction($request); $match = $this->findAction($request);
@ -237,7 +221,7 @@ class RequestHandler extends ViewableData
if ($result instanceof HasRequestHandler) { if ($result instanceof HasRequestHandler) {
$result = $result->getRequestHandler(); $result = $result->getRequestHandler();
} }
$returnValue = $result->handleRequest($request, $model); $returnValue = $result->handleRequest($request);
// Array results can be used to handle // Array results can be used to handle
if (is_array($returnValue)) { if (is_array($returnValue)) {

View File

@ -3,7 +3,6 @@
namespace SilverStripe\Control; namespace SilverStripe\Control;
use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injectable;
use SilverStripe\ORM\DataModel;
/** /**
* Represents a request processer that delegates pre and post request handling to nested request filters * Represents a request processer that delegates pre and post request handling to nested request filters
@ -34,10 +33,10 @@ class RequestProcessor implements RequestFilter
$this->filters = $filters; $this->filters = $filters;
} }
public function preRequest(HTTPRequest $request, Session $session, DataModel $model) public function preRequest(HTTPRequest $request)
{ {
foreach ($this->filters as $filter) { foreach ($this->filters as $filter) {
$res = $filter->preRequest($request, $session, $model); $res = $filter->preRequest($request);
if ($res === false) { if ($res === false) {
return false; return false;
} }
@ -45,10 +44,10 @@ class RequestProcessor implements RequestFilter
return null; return null;
} }
public function postRequest(HTTPRequest $request, HTTPResponse $response, DataModel $model) public function postRequest(HTTPRequest $request, HTTPResponse $response)
{ {
foreach ($this->filters as $filter) { foreach ($this->filters as $filter) {
$res = $filter->postRequest($request, $response, $model); $res = $filter->postRequest($request, $response);
if ($res === false) { if ($res === false) {
return false; return false;
} }

View File

@ -2,10 +2,8 @@
namespace SilverStripe\Control; namespace SilverStripe\Control;
use SilverStripe\Core\Config\Config; use BadMethodCallException;
use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation;
/** /**
* Handles all manipulation of the session. * Handles all manipulation of the session.
@ -89,7 +87,7 @@ use SilverStripe\Dev\Deprecation;
*/ */
class Session class Session
{ {
use Injectable; use Configurable;
/** /**
* Set session timeout in seconds. * Set session timeout in seconds.
@ -130,12 +128,23 @@ class Session
private static $cookie_secure = false; private static $cookie_secure = false;
/** /**
* Session data * Session data.
* Will be null if session has not been started
*
* @var array|null
*/ */
protected $data = array(); protected $data = null;
/**
* @var array
*/
protected $changedData = array(); protected $changedData = array();
/**
* Get user agent for this request
*
* @return string
*/
protected function userAgent() protected function userAgent()
{ {
if (isset($_SERVER['HTTP_USER_AGENT'])) { if (isset($_SERVER['HTTP_USER_AGENT'])) {
@ -148,123 +157,75 @@ class Session
/** /**
* Start PHP session, then create a new Session object with the given start data. * Start PHP session, then create a new Session object with the given start data.
* *
* @param $data array|Session Can be an array of data (such as $_SESSION) or another Session object to clone. * @param array|null|Session $data Can be an array of data (such as $_SESSION) or another Session object to clone.
* If null, this session is treated as unstarted.
*/ */
public function __construct($data) public function __construct($data)
{ {
if ($data instanceof Session) { if ($data instanceof Session) {
$data = $data->inst_getAll(); $data = $data->getAll();
} }
$this->data = $data; $this->data = $data;
}
/**
* Init this session instance before usage
*/
public function init()
{
if (!$this->isStarted()) {
$this->start();
}
if (isset($this->data['HTTP_USER_AGENT'])) {
if ($this->data['HTTP_USER_AGENT'] != $this->userAgent()) {
// Funny business detected! // Funny business detected!
$this->inst_clearAll(); if (isset($this->data['HTTP_USER_AGENT'])) {
$this->inst_destroy(); if ($this->data['HTTP_USER_AGENT'] !== $this->userAgent()) {
$this->inst_start(); $this->clearAll();
$this->destroy();
$this->start();
} }
} }
} }
/** /**
* Add a value to a specific key in the session array * Destroy existing session and restart
*/
public function restart()
{
$this->destroy();
$this->init();
}
/**
* Determine if this session has started
* *
* @param string $name * @return bool
* @param mixed $val
*/ */
public static function add_to_array($name, $val) public function isStarted()
{ {
return self::current_session()->inst_addToArray($name, $val); return isset($this->data);
} }
/** /**
* Set a key/value pair in the session * Begin session
* *
* @param string $name Key * @param string $sid
* @param string|array $val Value
*/ */
public static function set($name, $val) public function start($sid = null)
{ {
return self::current_session()->inst_set($name, $val); if ($this->isStarted()) {
throw new BadMethodCallException("Session has already started");
} }
/** $path = $this->config()->get('cookie_path');
* Return a specific value by session key
*
* @param string $name Key to lookup
* @return mixed
*/
public static function get($name)
{
return self::current_session()->inst_get($name);
}
/**
* Return all the values in session
*
* @return array
*/
public static function get_all()
{
return self::current_session()->inst_getAll();
}
/**
* Clear a given session key, value pair.
*
* @param string $name Key to lookup
*/
public static function clear($name)
{
return self::current_session()->inst_clear($name);
}
/**
* Clear all the values
*
* @return void
*/
public static function clear_all()
{
self::current_session()->inst_clearAll();
self::$default_session = null;
}
/**
* Save all the values in our session to $_SESSION
*/
public static function save()
{
return self::current_session()->inst_save();
}
protected static $default_session = null;
protected static function current_session()
{
if (Controller::has_curr()) {
return Controller::curr()->getSession();
} else {
if (!self::$default_session) {
self::$default_session = Injector::inst()->create('SilverStripe\\Control\\Session', isset($_SESSION) ? $_SESSION : array());
}
return self::$default_session;
}
}
public function inst_start($sid = null)
{
$path = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_path');
if (!$path) { if (!$path) {
$path = Director::baseURL(); $path = Director::baseURL();
} }
$domain = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_domain'); $domain = $this->config()->get('cookie_domain');
$secure = Director::is_https() && Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_secure'); $secure = Director::is_https() && $this->config()->get('cookie_secure');
$session_path = Config::inst()->get('SilverStripe\\Control\\Session', 'session_store_path'); $session_path = $this->config()->get('session_store_path');
$timeout = Config::inst()->get('SilverStripe\\Control\\Session', 'timeout'); $timeout = $this->config()->get('timeout');
// Director::baseURL can return absolute domain names - this extracts the relevant parts // Director::baseURL can return absolute domain names - this extracts the relevant parts
// for the session otherwise we can get broken session cookies // for the session otherwise we can get broken session cookies
@ -300,6 +261,8 @@ class Session
session_start(); session_start();
$this->data = isset($_SESSION) ? $_SESSION : array(); $this->data = isset($_SESSION) ? $_SESSION : array();
} else {
$this->data = [];
} }
// Modify the timeout behaviour so it's the *inactive* time before the session expires. // Modify the timeout behaviour so it's the *inactive* time before the session expires.
@ -310,28 +273,41 @@ class Session
} }
} }
public function inst_destroy($removeCookie = true) /**
* Destroy this session
*
* @param bool $removeCookie
*/
public function destroy($removeCookie = true)
{ {
if (session_id()) { if (session_id()) {
if ($removeCookie) { if ($removeCookie) {
$path = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_path') ?: Director::baseURL(); $path = $this->config()->get('cookie_path') ?: Director::baseURL();
$domain = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_domain'); $domain = $this->config()->get('cookie_domain');
$secure = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_secure'); $secure = $this->config()->get('cookie_secure');
Cookie::force_expiry(session_name(), $path, $domain, $secure, true); Cookie::force_expiry(session_name(), $path, $domain, $secure, true);
} }
session_destroy(); session_destroy();
}
// Clean up the superglobal - session_destroy does not do it. // Clean up the superglobal - session_destroy does not do it.
// http://nz1.php.net/manual/en/function.session-destroy.php // http://nz1.php.net/manual/en/function.session-destroy.php
unset($_SESSION); unset($_SESSION);
$this->data = array(); $this->data = null;
}
} }
public function inst_set($name, $val) /**
* Set session value
*
* @param string $name
* @param mixed $val
* @return $this
*/
public function set($name, $val)
{ {
if (!$this->isStarted()) {
throw new BadMethodCallException("Session cannot be modified until it's started");
}
// Quicker execution path for "."-free names // Quicker execution path for "."-free names
if (strpos($name, '.') === false) { if (strpos($name, '.') === false) {
$this->data[$name] = $val; $this->data[$name] = $val;
@ -360,10 +336,21 @@ class Session
$diffVar = $val; $diffVar = $val;
} }
} }
return $this;
} }
public function inst_addToArray($name, $val) /**
* Merge value with array
*
* @param string $name
* @param mixed $val
*/
public function addToArray($name, $val)
{ {
if (!$this->isStarted()) {
throw new BadMethodCallException("Session cannot be modified until it's started");
}
$names = explode('.', $name); $names = explode('.', $name);
// We still want to do this even if we have strict path checking for legacy code // We still want to do this even if we have strict path checking for legacy code
@ -379,8 +366,18 @@ class Session
$diffVar[sizeof($var)-1] = $val; $diffVar[sizeof($var)-1] = $val;
} }
public function inst_get($name) /**
* Get session value
*
* @param string $name
* @return mixed
*/
public function get($name)
{ {
if (!$this->isStarted()) {
throw new BadMethodCallException("Session cannot be accessed until it's started");
}
// Quicker execution path for "."-free names // Quicker execution path for "."-free names
if (strpos($name, '.') === false) { if (strpos($name, '.') === false) {
if (isset($this->data[$name])) { if (isset($this->data[$name])) {
@ -407,8 +404,18 @@ class Session
} }
} }
public function inst_clear($name) /**
* Clear session value
*
* @param string $name
* @return $this
*/
public function clear($name)
{ {
if (!$this->isStarted()) {
throw new BadMethodCallException("Session cannot be modified until it's started");
}
$names = explode('.', $name); $names = explode('.', $name);
// We still want to do this even if we have strict path checking for legacy code // We still want to do this even if we have strict path checking for legacy code
@ -418,7 +425,7 @@ class Session
foreach ($names as $n) { foreach ($names as $n) {
// don't clear a record that doesn't exist // don't clear a record that doesn't exist
if (!isset($var[$n])) { if (!isset($var[$n])) {
return; return $this;
} }
$var = &$var[$n]; $var = &$var[$n];
} }
@ -432,38 +439,54 @@ class Session
$var = null; $var = null;
$diffVar = null; $diffVar = null;
} }
return $this;
} }
public function inst_clearAll() /**
* Clear all values
*/
public function clearAll()
{ {
if (!$this->isStarted()) {
throw new BadMethodCallException("Session cannot be modified until it's started");
}
if ($this->data && is_array($this->data)) { if ($this->data && is_array($this->data)) {
foreach (array_keys($this->data) as $key) { foreach (array_keys($this->data) as $key) {
$this->inst_clear($key); $this->clear($key);
} }
} }
} }
public function inst_getAll() /**
* Get all values
*
* @return array|null
*/
public function getAll()
{ {
return $this->data; return $this->data;
} }
public function inst_finalize() /**
* Set user agent key
*/
public function finalize()
{ {
$this->inst_set('HTTP_USER_AGENT', $this->userAgent()); $this->set('HTTP_USER_AGENT', $this->userAgent());
} }
/** /**
* Save data to session * Save data to session
* Only save the changes, so that anyone manipulating $_SESSION directly doesn't get burned. * Only save the changes, so that anyone manipulating $_SESSION directly doesn't get burned.
*/ */
public function inst_save() public function save()
{ {
if ($this->changedData) { if ($this->changedData) {
$this->inst_finalize(); $this->finalize();
if (!isset($_SESSION)) { if (!$this->isStarted()) {
$this->inst_start(); $this->start();
} }
$this->recursivelyApply($this->changedData, $_SESSION); $this->recursivelyApply($this->changedData, $_SESSION);
@ -493,55 +516,11 @@ class Session
/** /**
* Return the changed data, for debugging purposes. * Return the changed data, for debugging purposes.
*
* @return array * @return array
*/ */
public function inst_changedData() public function changedData()
{ {
return $this->changedData; return $this->changedData;
} }
/**
* Sets the appropriate form message in session, with type. This will be shown once,
* for the form specified.
*
* @param string $formname the form name you wish to use ( usually $form->FormName() )
* @param string $message the message you wish to add to it
* @param string $type the type of message
*/
public static function setFormMessage($formname, $message, $type)
{
Session::set("FormInfo.$formname.formError.message", $message);
Session::set("FormInfo.$formname.formError.type", $type);
}
/**
* Is there a session ID in the request?
* @return bool
*/
public static function request_contains_session_id()
{
$secure = Director::is_https() && Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_secure');
$name = $secure ? 'SECSESSID' : session_name();
return isset($_COOKIE[$name]) || isset($_REQUEST[$name]);
}
/**
* Initialize session.
*
* @param string $sid Start the session with a specific ID
*/
public static function start($sid = null)
{
self::current_session()->inst_start($sid);
}
/**
* Destroy the active session.
*
* @param bool $removeCookie If set to TRUE, removes the user's cookie, FALSE does not remove
*/
public static function destroy($removeCookie = true)
{
self::current_session()->inst_destroy($removeCookie);
}
} }

29
src/Core/Application.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace SilverStripe\Core;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
/**
* Identifies a class as a root silverstripe application
*/
interface Application
{
/**
* Get the kernel for this application
*
* @return Kernel
*/
public function getKernel();
/**
* Safely boot the application and execute the given main action
*
* @param HTTPRequest $request
* @param callable $callback
* @param bool $flush
* @return HTTPResponse
*/
public function execute(HTTPRequest $request, callable $callback, $flush = false);
}

View File

@ -87,7 +87,7 @@ abstract class Config
{ {
// Unnest unless we would be left at 0 manifests // Unnest unless we would be left at 0 manifests
$loader = ConfigLoader::inst(); $loader = ConfigLoader::inst();
if ($loader->countManifests() < 2) { if ($loader->countManifests() <= 1) {
user_error( user_error(
"Unable to unnest root Config, please make sure you don't have mis-matched nest/unnest", "Unable to unnest root Config, please make sure you don't have mis-matched nest/unnest",
E_USER_WARNING E_USER_WARNING

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Core\Config; namespace SilverStripe\Core\Config;
use BadMethodCallException;
use SilverStripe\Config\Collections\ConfigCollectionInterface; use SilverStripe\Config\Collections\ConfigCollectionInterface;
/** /**
@ -25,7 +26,7 @@ class ConfigLoader
*/ */
public static function inst() public static function inst()
{ {
return self::$instance ? self::$instance : self::$instance = new self(); return self::$instance ? self::$instance : self::$instance = new static();
} }
/** /**
@ -36,6 +37,14 @@ class ConfigLoader
*/ */
public function getManifest() public function getManifest()
{ {
if ($this !== self::$instance) {
throw new BadMethodCallException(
"Non-current config manifest cannot be accessed. Please call ->activate() first"
);
}
if (empty($this->manifests)) {
throw new BadMethodCallException("No config manifests available");
}
return $this->manifests[count($this->manifests) - 1]; return $this->manifests[count($this->manifests) - 1];
} }
@ -78,14 +87,31 @@ class ConfigLoader
} }
/** /**
* Nest the current manifest * Nest the config loader and activates it
* *
* @return ConfigCollectionInterface * @return static
*/ */
public function nest() public function nest()
{ {
$manifest = $this->getManifest()->nest(); // Nest config
$this->pushManifest($manifest); $manifest = clone $this->getManifest();
return $manifest;
// Create new blank loader with new stack (top level nesting)
$newLoader = new static;
$newLoader->pushManifest($manifest);
// Activate new loader
return $newLoader->activate();
}
/**
* Mark this instance as the current instance
*
* @return $this
*/
public function activate()
{
static::$instance = $this;
return $this;
} }
} }

View File

@ -2,22 +2,18 @@
namespace SilverStripe\Core\Config; namespace SilverStripe\Core\Config;
use Monolog\Handler\ErrorLogHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
use SilverStripe\Config\Collections\CachedConfigCollection; use SilverStripe\Config\Collections\CachedConfigCollection;
use SilverStripe\Config\Collections\MemoryConfigCollection; use SilverStripe\Config\Collections\MemoryConfigCollection;
use SilverStripe\Config\Transformer\PrivateStaticTransformer; use SilverStripe\Config\Transformer\PrivateStaticTransformer;
use SilverStripe\Config\Transformer\YamlTransformer; use SilverStripe\Config\Transformer\YamlTransformer;
use SilverStripe\Control\Director;
use SilverStripe\Core\Cache\CacheFactory; use SilverStripe\Core\Cache\CacheFactory;
use SilverStripe\Core\Config\Middleware\ExtensionMiddleware; use SilverStripe\Core\Config\Middleware\ExtensionMiddleware;
use SilverStripe\Core\Config\Middleware\InheritanceMiddleware; use SilverStripe\Core\Config\Middleware\InheritanceMiddleware;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Kernel;
use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\Core\Manifest\ModuleLoader;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;
/** /**
@ -26,19 +22,18 @@ use Symfony\Component\Finder\Finder;
class CoreConfigFactory class CoreConfigFactory
{ {
/** /**
* @var static * @var CacheFactory
*/ */
protected static $inst = null; protected $cacheFactory = null;
/** /**
* @return static * Create factory
*
* @param CacheFactory $cacheFactory
*/ */
public static function inst() public function __construct(CacheFactory $cacheFactory = null)
{ {
if (!self::$inst) { $this->cacheFactory = $cacheFactory;
self::$inst = new static();
}
return self::$inst;
} }
/** /**
@ -46,20 +41,19 @@ class CoreConfigFactory
* This will be an immutable cached config, * This will be an immutable cached config,
* which conditionally generates a nested "core" config. * which conditionally generates a nested "core" config.
* *
* @param bool $flush
* @param CacheFactory $cacheFactory
* @return CachedConfigCollection * @return CachedConfigCollection
*/ */
public function createRoot($flush, CacheFactory $cacheFactory) public function createRoot()
{ {
$instance = new CachedConfigCollection(); $instance = new CachedConfigCollection();
// Create config cache // Create config cache
$cache = $cacheFactory->create(CacheInterface::class.'.configcache', [ if ($this->cacheFactory) {
$cache = $this->cacheFactory->create(CacheInterface::class . '.configcache', [
'namespace' => 'configcache' 'namespace' => 'configcache'
]); ]);
$instance->setCache($cache); $instance->setCache($cache);
$instance->setFlush($flush); }
// Set collection creator // Set collection creator
$instance->setCollectionCreator(function () { $instance->setCollectionCreator(function () {
@ -171,8 +165,11 @@ class CoreConfigFactory
} }
) )
->addRule('environment', function ($env) { ->addRule('environment', function ($env) {
$current = Director::get_environment_type(); // Note: The below relies on direct assignment of kernel to injector instance,
return strtolower($current) === strtolower($env); // and will fail if failing back to config service locator
/** @var Kernel $kernel */
$kernel = Injector::inst()->get(Kernel::class);
return strtolower($kernel->getEnvironment()) === strtolower($env);
}) })
->addRule('moduleexists', function ($module) { ->addRule('moduleexists', function ($module) {
return ModuleLoader::inst()->getManifest()->moduleExists($module); return ModuleLoader::inst()->getManifest()->moduleExists($module);

View File

@ -550,4 +550,47 @@ class Convert
return $return; return $return;
} }
/**
* Turn a memory string, such as 512M into an actual number of bytes.
*
* @param string $memString A memory limit string, such as "64M"
* @return float
*/
public static function memstring2bytes($memString)
{
switch (strtolower(substr($memString, -1))) {
case "b":
return round(substr($memString, 0, -1));
case "k":
return round(substr($memString, 0, -1) * 1024);
case "m":
return round(substr($memString, 0, -1) * 1024 * 1024);
case "g":
return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
default:
return round($memString);
}
}
/**
* @param float $bytes
* @param int $decimal decimal precision
* @return string
*/
public static function bytes2memstring($bytes, $decimal = 0)
{
$scales = ['B','K','M','G'];
// Get scale
$scale = (int)floor(log($bytes, 1024));
if (!isset($scales[$scale])) {
$scale = 2;
}
// Size
$num = round($bytes / pow(1024, $scale), $decimal);
return $num . $scales[$scale];
}
} }

View File

@ -1,324 +0,0 @@
<?php
use SilverStripe\Core\Cache\ManifestCacheFactory;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\CoreConfigFactory;
use SilverStripe\Core\Config\ConfigLoader;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Injector\SilverStripeServiceConfigurationLocator;
use SilverStripe\Core\Manifest\ClassManifest;
use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Control\Director;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Core\Manifest\ModuleManifest;
use SilverStripe\i18n\i18n;
use SilverStripe\Logging\ErrorHandler;
/**
* This file is the Framework bootstrap. It will get your environment ready to call Director::direct().
*
* It takes care of:
* - Checking of PHP memory limit
* - Including all the files needed to get the manifest built
* - Building and including the manifest
*
* @todo This file currently contains a lot of bits and pieces, and its various responsibilities should probably be
* moved into different subsystems.
* @todo A lot of this stuff is very order-dependent. This could be decoupled.
*/
/**
* All errors are reported, including E_STRICT by default *unless* the site is in
* live mode, where reporting is limited to fatal errors and warnings (see later in this file)
*/
error_reporting(E_ALL | E_STRICT);
global $_increase_time_limit_max;
$_increase_time_limit_max = -1;
/**
* Ensure we have enough memory
*/
increase_memory_limit_to('64M');
/**
* Ensure we don't run into xdebug's fairly conservative infinite recursion protection limit
*/
increase_xdebug_nesting_level_to(200);
/**
* Set default encoding
*/
mb_http_output('UTF-8');
mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');
/**
* Enable better garbage collection
*/
gc_enable();
// Initialise the dependency injector as soon as possible, as it is
// subsequently used by some of the following code
$injector = new Injector(array('locator' => SilverStripeServiceConfigurationLocator::class));
Injector::set_inst($injector);
///////////////////////////////////////////////////////////////////////////////
// MANIFEST
// Regenerate the manifest if ?flush is set, or if the database is being built.
// The coupling is a hack, but it removes an annoying bug where new classes
// referenced in _config.php files can be referenced during the build process.
$requestURL = isset($_REQUEST['url']) ? trim($_REQUEST['url'], '/') : false;
$flush = (isset($_GET['flush']) || $requestURL === trim(BASE_URL . '/dev/build', '/'));
// Manifest cache factory
$manifestCacheFactory = new ManifestCacheFactory([
'namespace' => 'manifestcache',
'directory' => getTempFolder(),
]);
// Build class manifest
$manifest = new ClassManifest(BASE_PATH, false, $flush, $manifestCacheFactory);
// Register SilverStripe's class map autoload
$loader = ClassLoader::inst();
$loader->registerAutoloader();
$loader->pushManifest($manifest);
// Init module manifest
$moduleManifest = new ModuleManifest(BASE_PATH, false, $flush, $manifestCacheFactory);
ModuleLoader::inst()->pushManifest($moduleManifest);
// Build config manifest
$configManifest = CoreConfigFactory::inst()->createRoot($flush, $manifestCacheFactory);
ConfigLoader::inst()->pushManifest($configManifest);
// After loading config, boot _config.php files
ModuleLoader::inst()->getManifest()->activateConfig();
// Load template manifest
SilverStripe\View\ThemeResourceLoader::inst()->addSet('$default', new SilverStripe\View\ThemeManifest(
BASE_PATH,
project(),
false,
$flush,
$manifestCacheFactory
));
// If in live mode, ensure deprecation, strict and notices are not reported
if (Director::isLive()) {
error_reporting(E_ALL & ~(E_DEPRECATED | E_STRICT | E_NOTICE));
}
///////////////////////////////////////////////////////////////////////////////
// POST-MANIFEST COMMANDS
/**
* Load error handlers
*/
$errorHandler = Injector::inst()->get(ErrorHandler::class);
$errorHandler->start();
///////////////////////////////////////////////////////////////////////////////
// HELPER FUNCTIONS
/**
* Creates a class instance by the "singleton" design pattern.
* It will always return the same instance for this class,
* which can be used for performance reasons and as a simple
* way to access instance methods which don't rely on instance
* data (e.g. the custom SilverStripe static handling).
*
* @param string $className
* @return mixed
*/
function singleton($className)
{
if ($className === Config::class) {
throw new InvalidArgumentException("Don't pass Config to singleton()");
}
if (!isset($className)) {
throw new InvalidArgumentException("singleton() Called without a class");
}
if (!is_string($className)) {
throw new InvalidArgumentException(
"singleton() passed bad class_name: " . var_export($className, true)
);
}
return Injector::inst()->get($className);
}
function project()
{
global $project;
return $project;
}
/**
* This is the main translator function. Returns the string defined by $entity according to the
* currently set locale.
*
* Also supports pluralisation of strings. Pass in a `count` argument, as well as a
* default value with `|` pipe-delimited options for each plural form.
*
* @param string $entity Entity that identifies the string. It must be in the form
* "Namespace.Entity" where Namespace will be usually the class name where this
* string is used and Entity identifies the string inside the namespace.
* @param mixed $arg,... Additional arguments are parsed as such:
* - Next string argument is a default. Pass in a `|` pipe-delimeted value with `{count}`
* to do pluralisation.
* - Any other string argument after default is context for i18nTextCollector
* - Any array argument in any order is an injection parameter list. Pass in a `count`
* injection parameter to pluralise.
* @return string
*/
function _t($entity, $arg = null)
{
// Pass args directly to handle deprecation
return call_user_func_array([i18n::class, '_t'], func_get_args());
}
/**
* Increase the memory limit to the given level if it's currently too low.
* Only increases up to the maximum defined in {@link set_increase_memory_limit_max()},
* and defaults to the 'memory_limit' setting in the PHP configuration.
*
* @param string|int $memoryLimit A memory limit string, such as "64M". If omitted, unlimited memory will be set.
* @return Boolean TRUE indicates a successful change, FALSE a denied change.
*/
function increase_memory_limit_to($memoryLimit = -1)
{
$curLimit = ini_get('memory_limit');
// Can't go higher than infinite
if ($curLimit == -1) {
return true;
}
// Check hard maximums
$max = get_increase_memory_limit_max();
if ($max && $max != -1 && translate_memstring($memoryLimit) > translate_memstring($max)) {
return false;
}
// Increase the memory limit if it's too low
if ($memoryLimit == -1 || translate_memstring($memoryLimit) > translate_memstring($curLimit)) {
ini_set('memory_limit', $memoryLimit);
}
return true;
}
$_increase_memory_limit_max = ini_get('memory_limit');
/**
* Set the maximum allowed value for {@link increase_memory_limit_to()}.
* The same result can also be achieved through 'suhosin.memory_limit'
* if PHP is running with the Suhosin system.
*
* @param string $memoryLimit Memory limit string
*/
function set_increase_memory_limit_max($memoryLimit)
{
global $_increase_memory_limit_max;
$_increase_memory_limit_max = $memoryLimit;
}
/**
* @return string Memory limit string
*/
function get_increase_memory_limit_max()
{
global $_increase_memory_limit_max;
return $_increase_memory_limit_max;
}
/**
* Increases the XDebug parameter max_nesting_level, which limits how deep recursion can go.
* Only does anything if (a) xdebug is installed and (b) the new limit is higher than the existing limit
*
* @param int $limit - The new limit to increase to
*/
function increase_xdebug_nesting_level_to($limit)
{
if (function_exists('xdebug_enable')) {
$current = ini_get('xdebug.max_nesting_level');
if ((int)$current < $limit) {
ini_set('xdebug.max_nesting_level', $limit);
}
}
}
/**
* Turn a memory string, such as 512M into an actual number of bytes.
*
* @param string $memString A memory limit string, such as "64M"
* @return float
*/
function translate_memstring($memString)
{
switch (strtolower(substr($memString, -1))) {
case "k":
return round(substr($memString, 0, -1)*1024);
case "m":
return round(substr($memString, 0, -1)*1024*1024);
case "g":
return round(substr($memString, 0, -1)*1024*1024*1024);
default:
return round($memString);
}
}
/**
* Increase the time limit of this script. By default, the time will be unlimited.
* Only works if 'safe_mode' is off in the PHP configuration.
* Only values up to {@link get_increase_time_limit_max()} are allowed.
*
* @param int $timeLimit The time limit in seconds. If omitted, no time limit will be set.
* @return Boolean TRUE indicates a successful change, FALSE a denied change.
*/
function increase_time_limit_to($timeLimit = null)
{
$max = get_increase_time_limit_max();
if ($max != -1 && $max != null && $timeLimit > $max) {
return false;
}
if (!ini_get('safe_mode')) {
if (!$timeLimit) {
set_time_limit(0);
return true;
} else {
$currTimeLimit = ini_get('max_execution_time');
// Only increase if its smaller
if ($currTimeLimit && $currTimeLimit < $timeLimit) {
set_time_limit($timeLimit);
}
return true;
}
} else {
return false;
}
}
/**
* Set the maximum allowed value for {@link increase_timeLimit_to()};
*
* @param int $timeLimit Limit in seconds
*/
function set_increase_time_limit_max($timeLimit)
{
global $_increase_time_limit_max;
$_increase_time_limit_max = $timeLimit;
}
/**
* @return Int Limit in seconds
*/
function get_increase_time_limit_max()
{
global $_increase_time_limit_max;
return $_increase_time_limit_max;
}

580
src/Core/CoreKernel.php Normal file
View File

@ -0,0 +1,580 @@
<?php
namespace SilverStripe\Core;
use InvalidArgumentException;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use SilverStripe\Config\Collections\CachedConfigCollection;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Core\Cache\ManifestCacheFactory;
use SilverStripe\Core\Config\ConfigLoader;
use SilverStripe\Core\Config\CoreConfigFactory;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Injector\InjectorLoader;
use SilverStripe\Core\Injector\SilverStripeServiceConfigurationLocator;
use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\ClassManifest;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Core\Manifest\ModuleManifest;
use SilverStripe\Dev\DebugView;
use SilverStripe\Dev\Install\DatabaseAdapterRegistry;
use SilverStripe\Logging\ErrorHandler;
use SilverStripe\ORM\DB;
use SilverStripe\View\ThemeManifest;
use SilverStripe\View\ThemeResourceLoader;
/**
* Simple Kernel container
*/
class CoreKernel implements Kernel
{
/**
* @var Kernel
*/
protected $nestedFrom = null;
/**
* @var Injector
*/
protected $container = null;
/**
* @var string
*/
protected $enviroment = null;
/**
* @var ClassLoader
*/
protected $classLoader = null;
/**
* @var ModuleLoader
*/
protected $moduleLoader = null;
/**
* @var ConfigLoader
*/
protected $configLoader = null;
/**
* @var InjectorLoader
*/
protected $injectorLoader = null;
/**
* @var ThemeResourceLoader
*/
protected $themeResourceLoader = null;
protected $basePath = null;
/**
* Create a new kernel for this application
*
* @param string $basePath Path to base dir for this application
*/
public function __construct($basePath)
{
$this->basePath = $basePath;
// Initialise the dependency injector as soon as possible, as it is
// subsequently used by some of the following code
$injectorLoader = InjectorLoader::inst();
$injector = new Injector(array('locator' => SilverStripeServiceConfigurationLocator::class));
$injectorLoader->pushManifest($injector);
$this->setInjectorLoader($injectorLoader);
// Manifest cache factory
$manifestCacheFactory = $this->buildManifestCacheFactory();
// Class loader
$classLoader = ClassLoader::inst();
$classLoader->pushManifest(new ClassManifest($basePath, $manifestCacheFactory));
$this->setClassLoader($classLoader);
// Module loader
$moduleLoader = ModuleLoader::inst();
$moduleManifest = new ModuleManifest($basePath, $manifestCacheFactory);
$moduleLoader->pushManifest($moduleManifest);
$this->setModuleLoader($moduleLoader);
// Config loader
// @todo refactor CoreConfigFactory
$configFactory = new CoreConfigFactory($manifestCacheFactory);
$configManifest = $configFactory->createRoot();
$configLoader = ConfigLoader::inst();
$configLoader->pushManifest($configManifest);
$this->setConfigLoader($configLoader);
// Load template manifest
$themeResourceLoader = ThemeResourceLoader::inst();
$themeResourceLoader->addSet('$default', new ThemeManifest(
$basePath,
project(),
$manifestCacheFactory
));
$this->setThemeResourceLoader($themeResourceLoader);
}
public function getEnvironment()
{
// Check set
if ($this->enviroment) {
return $this->enviroment;
}
// Check saved session
$env = $this->sessionEnvironment();
if ($env) {
return $env;
}
// Check getenv
if ($env = getenv('SS_ENVIRONMENT_TYPE')) {
return $env;
}
return self::LIVE;
}
/**
* Check or update any temporary environment specified in the session.
*
* @return null|string
*/
protected function sessionEnvironment()
{
// Check isDev in querystring
if (isset($_GET['isDev'])) {
if (isset($_SESSION)) {
unset($_SESSION['isTest']); // In case we are changing from test mode
$_SESSION['isDev'] = $_GET['isDev'];
}
return self::DEV;
}
// Check isTest in querystring
if (isset($_GET['isTest'])) {
if (isset($_SESSION)) {
unset($_SESSION['isDev']); // In case we are changing from dev mode
$_SESSION['isTest'] = $_GET['isTest'];
}
return self::TEST;
}
// Check session
if (!empty($_SESSION['isDev'])) {
return self::DEV;
}
if (!empty($_SESSION['isTest'])) {
return self::TEST;
}
// no session environment
return null;
}
public function boot($flush = false)
{
$this->bootPHP();
$this->bootManifests($flush);
$this->bootErrorHandling();
$this->bootDatabase();
$this->bootConfigs();
}
/**
* Include all _config.php files
*/
protected function bootConfigs()
{
// After loading all other app manifests, include _config.php files
$this->getModuleLoader()->getManifest()->activateConfig();
}
/**
* Configure database
*
* @throws HTTPResponse_Exception
*/
protected function bootDatabase()
{
// Check if a DB is named
$name = $this->getDatabaseName();
// Gracefully fail if no DB is configured
if (empty($name)) {
$this->detectLegacyEnvironment();
$this->redirectToInstaller();
}
// Set default database config
$databaseConfig = $this->getDatabaseConfig();
$databaseConfig['database'] = $this->getDatabaseName();
DB::setConfig($databaseConfig);
}
/**
* Check if there's a legacy _ss_environment.php file
*
* @throws HTTPResponse_Exception
*/
protected function detectLegacyEnvironment()
{
// Is there an _ss_environment.php file?
if (!file_exists($this->basePath . '/_ss_environment.php') &&
!file_exists(dirname($this->basePath) . '/_ss_environment.php')
) {
return;
}
// Build error response
$dv = new DebugView();
$body =
$dv->renderHeader() .
$dv->renderInfo(
"Configuraton Error",
Director::absoluteBaseURL()
) .
$dv->renderParagraph(
'You need to replace your _ss_environment.php file with a .env file, or with environment variables.<br><br>'
. 'See the <a href="https://docs.silverstripe.org/en/4/getting_started/environment_management/">'
. 'Environment Management</a> docs for more information.'
) .
$dv->renderFooter();
// Raise error
$response = new HTTPResponse($body, 500);
throw new HTTPResponse_Exception($response);
}
/**
* If missing configuration, redirect to install.php
*/
protected function redirectToInstaller()
{
// Error if installer not available
if (!file_exists($this->basePath . '/install.php')) {
throw new HTTPResponse_Exception(
'SilverStripe Framework requires a $databaseConfig defined.',
500
);
}
// Redirect to installer
$response = new HTTPResponse();
$response->redirect(Director::absoluteURL('install.php'));
throw new HTTPResponse_Exception($response);
}
/**
* Load database config from environment
*
* @return array
*/
protected function getDatabaseConfig()
{
// Check global config
global $databaseConfig;
if (!empty($databaseConfig)) {
return $databaseConfig;
}
/** @skipUpgrade */
$databaseConfig = [
"type" => getenv('SS_DATABASE_CLASS') ?: 'MySQLDatabase',
"server" => getenv('SS_DATABASE_SERVER') ?: 'localhost',
"username" => getenv('SS_DATABASE_USERNAME') ?: null,
"password" => getenv('SS_DATABASE_PASSWORD') ?: null,
];
// Set the port if called for
$dbPort = getenv('SS_DATABASE_PORT');
if ($dbPort) {
$databaseConfig['port'] = $dbPort;
}
// Set the timezone if called for
$dbTZ = getenv('SS_DATABASE_TIMEZONE');
if ($dbTZ) {
$databaseConfig['timezone'] = $dbTZ;
}
// For schema enabled drivers:
$dbSchema = getenv('SS_DATABASE_SCHEMA');
if ($dbSchema) {
$databaseConfig["schema"] = $dbSchema;
}
// For SQlite3 memory databases (mainly for testing purposes)
$dbMemory = getenv('SS_DATABASE_MEMORY');
if ($dbMemory) {
$databaseConfig["memory"] = $dbMemory;
}
// Allow database adapters to handle their own configuration
DatabaseAdapterRegistry::autoconfigure();
return $databaseConfig;
}
/**
* Get name of database
*
* @return string
*/
protected function getDatabaseName()
{
$prefix = getenv('SS_DATABASE_PREFIX') ?: 'SS_';
// Check globals
global $database;
if (!empty($database)) {
return $prefix.$database;
}
global $databaseConfig;
if (!empty($databaseConfig['database'])) {
return $databaseConfig['database']; // Note: Already includes prefix
}
// Check environment
$database = getenv('SS_DATABASE_NAME');
if ($database) {
return $prefix.$database;
}
// Auto-detect name
$chooseName = getenv('SS_DATABASE_CHOOSE_NAME');
if ($chooseName) {
// Find directory to build name from
$loopCount = (int)$chooseName;
$databaseDir = $this->basePath;
for ($i = 0; $i < $loopCount-1; $i++) {
$databaseDir = dirname($databaseDir);
}
// Build name
$database = str_replace('.', '', basename($databaseDir));
return $prefix.$database;
}
// no DB name (may be optional for some connectors)
return null;
}
/**
* Initialise PHP with default variables
*/
protected function bootPHP()
{
if ($this->getEnvironment() === self::LIVE) {
// limited to fatal errors and warnings in live mode
error_reporting(E_ALL & ~(E_DEPRECATED | E_STRICT | E_NOTICE));
} else {
// Report all errors in dev / test mode
error_reporting(E_ALL | E_STRICT);
}
/**
* Ensure we have enough memory
*/
Environment::increaseMemoryLimitTo('64M');
// Ensure we don't run into xdebug's fairly conservative infinite recursion protection limit
if (function_exists('xdebug_enable')) {
$current = ini_get('xdebug.max_nesting_level');
if ((int)$current < 200) {
ini_set('xdebug.max_nesting_level', 200);
}
}
/**
* Set default encoding
*/
mb_http_output('UTF-8');
mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');
/**
* Enable better garbage collection
*/
gc_enable();
}
/**
* @return ManifestCacheFactory
*/
protected function buildManifestCacheFactory()
{
return new ManifestCacheFactory([
'namespace' => 'manifestcache',
'directory' => TempFolder::getTempFolder($this->basePath),
]);
}
/**
* @return bool
*/
protected function getIncludeTests()
{
return false;
}
/**
* Boot all manifests
*
* @param bool $flush
*/
protected function bootManifests($flush)
{
// Setup autoloader
$this->getClassLoader()->init($this->getIncludeTests(), $flush);
// Find modules
$this->getModuleLoader()->init($this->getIncludeTests(), $flush);
// Flush config
if ($flush) {
$config = $this->getConfigLoader()->getManifest();
if ($config instanceof CachedConfigCollection) {
$config->setFlush(true);
}
}
// Find default templates
$defaultSet = $this->getThemeResourceLoader()->getSet('$default');
if ($defaultSet instanceof ThemeManifest) {
$defaultSet->init($this->getIncludeTests(), $flush);
}
}
/**
* Turn on error handling
*/
protected function bootErrorHandling()
{
// Register error handler
$errorHandler = Injector::inst()->get(ErrorHandler::class);
$errorHandler->start();
// Register error log file
$errorLog = getenv('SS_ERROR_LOG');
if ($errorLog) {
$logger = Injector::inst()->get(LoggerInterface::class);
if ($logger instanceof Logger) {
$logger->pushHandler(new StreamHandler($this->basePath . '/' . $errorLog, Logger::WARNING));
} else {
user_error("SS_ERROR_LOG setting only works with Monolog, you are using another logger", E_USER_WARNING);
}
}
}
public function shutdown()
{
}
public function nest()
{
// Clone this kernel, nesting config / injector manifest containers
$kernel = clone $this;
$kernel->setConfigLoader($this->configLoader->nest());
$kernel->setInjectorLoader($this->injectorLoader->nest());
return $kernel;
}
public function activate()
{
$this->configLoader->activate();
$this->injectorLoader->activate();
// Self register
$this->getInjectorLoader()
->getManifest()
->registerService($this, Kernel::class);
return $this;
}
public function getNestedFrom()
{
return $this->nestedFrom;
}
public function getContainer()
{
return $this->getInjectorLoader()->getManifest();
}
public function setInjectorLoader(InjectorLoader $injectorLoader)
{
$this->injectorLoader = $injectorLoader;
$injectorLoader
->getManifest()
->registerService($this, Kernel::class);
return $this;
}
public function getInjectorLoader()
{
return $this->injectorLoader;
}
public function getClassLoader()
{
return $this->classLoader;
}
public function setClassLoader(ClassLoader $classLoader)
{
$this->classLoader = $classLoader;
return $this;
}
public function getModuleLoader()
{
return $this->moduleLoader;
}
public function setModuleLoader(ModuleLoader $moduleLoader)
{
$this->moduleLoader = $moduleLoader;
return $this;
}
public function setEnvironment($environment)
{
if (!in_array($environment, [self::DEV, self::TEST, self::LIVE, null])) {
throw new InvalidArgumentException(
"Director::set_environment_type passed '$environment'. It should be passed dev, test, or live"
);
}
$this->enviroment = $environment;
return $this;
}
public function getConfigLoader()
{
return $this->configLoader;
}
public function setConfigLoader($configLoader)
{
$this->configLoader = $configLoader;
return $this;
}
public function getThemeResourceLoader()
{
return $this->themeResourceLoader;
}
public function setThemeResourceLoader($themeResourceLoader)
{
$this->themeResourceLoader = $themeResourceLoader;
return $this;
}
}

154
src/Core/Environment.php Normal file
View File

@ -0,0 +1,154 @@
<?php
namespace SilverStripe\Core;
/**
* Consolidates access and modification of PHP global variables and settings.
* This class should be used sparingly, and only if information cannot be obtained
* from a current {@link HTTPRequest} object.
*/
class Environment
{
/**
* Set maximum limit allowed for increaseMemoryLimit
*
* @var float|null
*/
protected static $memoryLimitMax = null;
/**
* Set maximum limited allowed for increaseTimeLimit
*
* @var int|null
*/
protected static $timeLimitMax = null;
/**
* Extract env vars prior to modification
*
* @return array List of all super globals
*/
public static function getVariables()
{
// Suppress return by-ref
return array_merge($GLOBALS, []);
}
/**
* Restore a backed up or modified list of vars to $globals
*
* @param array $vars
*/
public static function setVariables(array $vars)
{
foreach ($vars as $key => $value) {
$GLOBALS[$key] = $value;
}
}
/**
* Increase the memory limit to the given level if it's currently too low.
* Only increases up to the maximum defined in {@link setMemoryLimitMax()},
* and defaults to the 'memory_limit' setting in the PHP configuration.
*
* @param string|float|int $memoryLimit A memory limit string, such as "64M". If omitted, unlimited memory will be set.
* @return bool true indicates a successful change, false a denied change.
*/
public static function increaseMemoryLimitTo($memoryLimit = -1)
{
$memoryLimit = Convert::memstring2bytes($memoryLimit);
$curLimit = Convert::memstring2bytes(ini_get('memory_limit'));
// Can't go higher than infinite
if ($curLimit < 0) {
return true;
}
// Check hard maximums
$max = static::getMemoryLimitMax();
if ($max > 0 && ($memoryLimit < 0 || $memoryLimit > $max)) {
$memoryLimit = $max;
}
// Increase the memory limit if it's too low
if ($memoryLimit < 0) {
ini_set('memory_limit', '-1');
} elseif ($memoryLimit > $curLimit) {
ini_set('memory_limit', Convert::bytes2memstring($memoryLimit));
}
return true;
}
/**
* Set the maximum allowed value for {@link increaseMemoryLimitTo()}.
* The same result can also be achieved through 'suhosin.memory_limit'
* if PHP is running with the Suhosin system.
*
* @param string|float $memoryLimit Memory limit string or float value
*/
static function setMemoryLimitMax($memoryLimit)
{
if (isset($memoryLimit) && !is_numeric($memoryLimit)) {
$memoryLimit = Convert::memstring2bytes($memoryLimit);
}
static::$memoryLimitMax = $memoryLimit;
}
/**
* @return int Memory limit in bytes
*/
public static function getMemoryLimitMax()
{
if (static::$memoryLimitMax === null) {
return Convert::memstring2bytes(ini_get('memory_limit'));
}
return static::$memoryLimitMax;
}
/**
* Increase the time limit of this script. By default, the time will be unlimited.
* Only works if 'safe_mode' is off in the PHP configuration.
* Only values up to {@link getTimeLimitMax()} are allowed.
*
* @param int $timeLimit The time limit in seconds. If omitted, no time limit will be set.
* @return Boolean TRUE indicates a successful change, FALSE a denied change.
*/
public static function increaseTimeLimitTo($timeLimit = null)
{
// Check vs max limit
$max = static::getTimeLimitMax();
if ($max > 0 && $timeLimit > $max) {
return false;
}
if (!$timeLimit) {
set_time_limit(0);
} else {
$currTimeLimit = ini_get('max_execution_time');
// Only increase if its smaller
if ($currTimeLimit > 0 && $currTimeLimit < $timeLimit) {
set_time_limit($timeLimit);
}
}
return true;
}
/**
* Set the maximum allowed value for {@link increaseTimeLimitTo()};
*
* @param int $timeLimit Limit in seconds
*/
public static function setTimeLimitMax($timeLimit)
{
static::$timeLimitMax = $timeLimit;
}
/**
* @return Int Limit in seconds
*/
public static function getTimeLimitMax()
{
return static::$timeLimitMax;
}
}

View File

@ -6,6 +6,7 @@ use InvalidArgumentException;
use SilverStripe\Control\RequestHandler; use SilverStripe\Control\RequestHandler;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\DataObject;
use SilverStripe\View\ViewableData; use SilverStripe\View\ViewableData;
/** /**
@ -271,8 +272,8 @@ trait Extensible
} }
Config::modify()->set($class, 'extensions', $config); Config::modify()->set($class, 'extensions', $config);
// unset singletons to avoid side-effects // Unset singletons
Injector::inst()->unregisterAllObjects(); Injector::inst()->unregisterObjects($class);
// unset some caches // unset some caches
$subclasses = ClassInfo::subclassesFor($class); $subclasses = ClassInfo::subclassesFor($class);

View File

@ -2,14 +2,15 @@
namespace SilverStripe\Core\Injector; namespace SilverStripe\Core\Injector;
use ArrayObject;
use InvalidArgumentException;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface; use Psr\Container\NotFoundExceptionInterface;
use ReflectionMethod;
use ReflectionObject;
use ReflectionProperty;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use ReflectionProperty;
use ArrayObject;
use ReflectionObject;
use ReflectionMethod;
use Psr\Container\ContainerInterface;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
/** /**
@ -204,19 +205,16 @@ class Injector implements ContainerInterface
$this->serviceCache = array( $this->serviceCache = array(
'Injector' => $this, 'Injector' => $this,
); );
$this->specs = array( $this->specs = [
'Injector' => array('class' => 'SilverStripe\\Core\\Injector\\Injector') 'Injector' => ['class' => static::class]
); ];
$this->autoProperties = array(); $this->autoProperties = array();
$creatorClass = isset($config['creator']) $creatorClass = isset($config['creator'])
? $config['creator'] ? $config['creator']
: 'SilverStripe\\Core\\Injector\\InjectionCreator'; : InjectionCreator::class;
$locatorClass = isset($config['locator']) $locatorClass = isset($config['locator'])
? $config['locator'] ? $config['locator']
: 'SilverStripe\\Core\\Injector\\SilverStripeServiceConfigurationLocator'; : SilverStripeServiceConfigurationLocator::class;
$this->objectCreator = new $creatorClass; $this->objectCreator = new $creatorClass;
$this->configLocator = new $locatorClass; $this->configLocator = new $locatorClass;
@ -234,28 +232,11 @@ class Injector implements ContainerInterface
protected $nestedFrom = null; protected $nestedFrom = null;
/** /**
* If a user wants to use the injector as a static reference
*
* @param array $config
* @return Injector * @return Injector
*/ */
public static function inst($config = null) public static function inst()
{ {
if (!self::$instance) { return InjectorLoader::inst()->getManifest();
self::$instance = new Injector($config);
}
return self::$instance;
}
/**
* Sets the default global injector instance.
*
* @param Injector $instance
* @return Injector Reference to new active Injector instance
*/
public static function set_inst(Injector $instance)
{
return self::$instance = $instance;
} }
/** /**
@ -270,11 +251,10 @@ class Injector implements ContainerInterface
*/ */
public static function nest() public static function nest()
{ {
$current = self::$instance; // Clone current injector and nest
$new = clone self::inst();
$new = clone $current; InjectorLoader::inst()->pushManifest($new);
$new->nestedFrom = $current; return $new;
return self::set_inst($new);
} }
/** /**
@ -285,15 +265,17 @@ class Injector implements ContainerInterface
*/ */
public static function unnest() public static function unnest()
{ {
if (self::inst()->nestedFrom) { // Unnest unless we would be left at 0 manifests
self::set_inst(self::inst()->nestedFrom); $loader = InjectorLoader::inst();
} else { if ($loader->countManifests() <= 1) {
user_error( user_error(
"Unable to unnest root Injector, please make sure you don't have mis-matched nest/unnest", "Unable to unnest root Injector, please make sure you don't have mis-matched nest/unnest",
E_USER_WARNING E_USER_WARNING
); );
} else {
$loader->popManifest();
} }
return self::inst(); return static::inst();
} }
/** /**
@ -407,7 +389,7 @@ class Injector implements ContainerInterface
// make sure the class is set... // make sure the class is set...
if (empty($class)) { if (empty($class)) {
throw new \InvalidArgumentException('Missing spec class'); throw new InvalidArgumentException('Missing spec class');
} }
$spec['class'] = $class; $spec['class'] = $class;
@ -654,21 +636,21 @@ class Injector implements ContainerInterface
// Format validation // Format validation
if (!is_array($method) || !isset($method[0]) || isset($method[2])) { if (!is_array($method) || !isset($method[0]) || isset($method[2])) {
throw new \InvalidArgumentException( throw new InvalidArgumentException(
"'calls' entries in service definition should be 1 or 2 element arrays." "'calls' entries in service definition should be 1 or 2 element arrays."
); );
} }
if (!is_string($method[0])) { if (!is_string($method[0])) {
throw new \InvalidArgumentException("1st element of a 'calls' entry should be a string"); throw new InvalidArgumentException("1st element of a 'calls' entry should be a string");
} }
if (isset($method[1]) && !is_array($method[1])) { if (isset($method[1]) && !is_array($method[1])) {
throw new \InvalidArgumentException("2nd element of a 'calls' entry should an arguments array"); throw new InvalidArgumentException("2nd element of a 'calls' entry should an arguments array");
} }
// Check that the method exists and is callable // Check that the method exists and is callable
$objectMethod = array($object, $method[0]); $objectMethod = array($object, $method[0]);
if (!is_callable($objectMethod)) { if (!is_callable($objectMethod)) {
throw new \InvalidArgumentException("'$method[0]' in 'calls' entry is not a public method"); throw new InvalidArgumentException("'$method[0]' in 'calls' entry is not a public method");
} }
// Call it // Call it
@ -850,17 +832,18 @@ class Injector implements ContainerInterface
* @param object $service The object to register * @param object $service The object to register
* @param string $replace The name of the object to replace (if different to the * @param string $replace The name of the object to replace (if different to the
* class name of the object to register) * class name of the object to register)
* @return $this
*/ */
public function registerService($service, $replace = null) public function registerService($service, $replace = null)
{ {
$registerAt = get_class($service); $registerAt = get_class($service);
if ($replace != null) { if ($replace !== null) {
$registerAt = $replace; $registerAt = $replace;
} }
$this->specs[$registerAt] = array('class' => get_class($service)); $this->specs[$registerAt] = array('class' => get_class($service));
$this->serviceCache[$registerAt] = $service; $this->serviceCache[$registerAt] = $service;
$this->inject($service); return $this;
} }
/** /**
@ -868,18 +851,40 @@ class Injector implements ContainerInterface
* by the inject * by the inject
* *
* @param string $name The name to unregister * @param string $name The name to unregister
* @return $this
*/ */
public function unregisterNamedObject($name) public function unregisterNamedObject($name)
{ {
unset($this->serviceCache[$name]); unset($this->serviceCache[$name]);
return $this;
} }
/** /**
* Clear out all objects that are managed by the injetor. * Clear out objects of one or more types that are managed by the injetor.
*
* @param array|string $types Base class of object (not service name) to remove
* @return $this
*/ */
public function unregisterAllObjects() public function unregisterObjects($types)
{ {
$this->serviceCache = array('Injector' => $this); if (!is_array($types)) {
$types = [ $types ];
}
// Filter all objects
foreach ($this->serviceCache as $key => $object) {
foreach ($types as $filterClass) {
// Prevent destructive flushing
if (strcasecmp($filterClass, 'object') === 0) {
throw new InvalidArgumentException("Global unregistration is not allowed");
}
if ($object instanceof $filterClass) {
unset($this->serviceCache[$key]);
break;
}
}
}
return $this;
} }
/** /**

View File

@ -0,0 +1,117 @@
<?php
namespace SilverStripe\Core\Injector;
use BadMethodCallException;
/**
* Registers chained injectors
*/
class InjectorLoader
{
/**
* @internal
* @var self
*/
private static $instance;
/**
* @var Injector[] map of injector instances
*/
protected $manifests = array();
/**
* @return self
*/
public static function inst()
{
return self::$instance ? self::$instance : self::$instance = new static();
}
/**
* Returns the currently active class manifest instance that is used for
* loading classes.
*
* @return Injector
*/
public function getManifest()
{
if ($this !== self::$instance) {
throw new BadMethodCallException(
"Non-current injector manifest cannot be accessed. Please call ->activate() first"
);
}
if (empty($this->manifests)) {
throw new BadMethodCallException("No injector manifests available");
}
return $this->manifests[count($this->manifests) - 1];
}
/**
* Returns true if this class loader has a manifest.
*
* @return bool
*/
public function hasManifest()
{
return (bool)$this->manifests;
}
/**
* Pushes a class manifest instance onto the top of the stack.
*
* @param Injector $manifest
*/
public function pushManifest(Injector $manifest)
{
$this->manifests[] = $manifest;
}
/**
* @return Injector
*/
public function popManifest()
{
return array_pop($this->manifests);
}
/**
* Check number of manifests
*
* @return int
*/
public function countManifests()
{
return count($this->manifests);
}
/**
* Nest the config loader
*
* @return static
*/
public function nest()
{
// Nest injector (note: Don't call getManifest()->nest() since that self-pushes a new manifest)
$manifest = clone $this->getManifest();
// Create new blank loader with new stack (top level nesting)
$newLoader = new static;
$newLoader->pushManifest($manifest);
// Activate new loader
$newLoader->activate();
return $newLoader;
}
/**
* Mark this instance as the current instance
*
* @return $this
*/
public function activate()
{
static::$instance = $this;
return $this;
}
}

View File

@ -51,7 +51,7 @@ class SilverStripeServiceConfigurationLocator implements ServiceConfigurationLoc
return $this->configs[$name]; return $this->configs[$name];
} }
$config = Config::inst()->get('SilverStripe\\Core\\Injector\\Injector', $name); $config = Config::inst()->get(Injector::class, $name);
$this->configs[$name] = $config; $this->configs[$name] = $config;
return $config; return $config;
} }

135
src/Core/Kernel.php Normal file
View File

@ -0,0 +1,135 @@
<?php
namespace SilverStripe\Core;
use SilverStripe\Core\Config\ConfigLoader;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Injector\InjectorLoader;
use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\View\ThemeResourceLoader;
/**
* Represents the core state of a SilverStripe application
* Based loosely on symfony/http-kernel's KernelInterface component
*/
interface Kernel
{
/**
* Test environment
*/
const TEST = 'test';
/**
* Dev environment
*/
const DEV = 'dev';
/**
* Live (default) environment
*/
const LIVE = 'live';
/*
* Boots the current kernel
*
* @param bool $flush
*/
public function boot($flush = false);
/**
* Shutdowns the kernel.
*/
public function shutdown();
/**
* Nests this kernel, all components, and returns the nested value.
*
* @return Kernel
*/
public function nest();
/**
* Ensures this kernel is the active kernel after or during nesting
*
* @return $this
*/
public function activate();
/**
* @return Injector
*/
public function getContainer();
/**
* @return ClassLoader
*/
public function getClassLoader();
/**
* @param ClassLoader $classLoader
* @return $this
*/
public function setClassLoader(ClassLoader $classLoader);
/**
* Get loader for injector instance
*
* @return InjectorLoader
*/
public function getInjectorLoader();
/**
* @param InjectorLoader $injectorLoader
* @return $this
*/
public function setInjectorLoader(InjectorLoader $injectorLoader);
/**
* @return ModuleLoader
*/
public function getModuleLoader();
/**
* @param ModuleLoader $moduleLoader
* @return $this
*/
public function setModuleLoader(ModuleLoader $moduleLoader);
/**
* @return ConfigLoader
*/
public function getConfigLoader();
/**
* @param ConfigLoader $configLoader
* @return $this
*/
public function setConfigLoader($configLoader);
/**
* @return ThemeResourceLoader
*/
public function getThemeResourceLoader();
/**
* @param ThemeResourceLoader $themeResourceLoader
* @return $this
*/
public function setThemeResourceLoader($themeResourceLoader);
/**
* One of dev, live, or test
*
* @return string
*/
public function getEnvironment();
/**
* Sets new environment
*
* @param string $environment
* @return $this
*/
public function setEnvironment($environment);
}

View File

@ -19,7 +19,9 @@ class ClassLoader
private static $instance; private static $instance;
/** /**
* @var array Map of 'instance' (ClassManifest) and other options. * Map of 'instance' (ClassManifest) and other options.
*
* @var array
*/ */
protected $manifests = array(); protected $manifests = array();
@ -28,7 +30,7 @@ class ClassLoader
*/ */
public static function inst() public static function inst()
{ {
return self::$instance ? self::$instance : self::$instance = new self(); return self::$instance ? self::$instance : self::$instance = new static();
} }
/** /**
@ -113,6 +115,23 @@ class ClassLoader
return false; return false;
} }
/**
* Initialise the class loader
*
* @param bool $includeTests
* @param bool $forceRegen
*/
public function init($includeTests = false, $forceRegen = false)
{
foreach ($this->manifests as $manifest) {
/** @var ClassManifest $instance */
$instance = $manifest['instance'];
$instance->init($includeTests, $forceRegen);
}
$this->registerAutoloader();
}
/** /**
* Returns true if a class or interface name exists in the manifest. * Returns true if a class or interface name exists in the manifest.
* *

View File

@ -29,12 +29,11 @@ class ClassManifest
protected $base; protected $base;
/** /**
* Set if including test classes * Used to build cache during boot
* *
* @see TestOnly * @var CacheFactory
* @var bool
*/ */
protected $tests; protected $cacheFactory;
/** /**
* Cache to use, if caching. * Cache to use, if caching.
@ -122,27 +121,30 @@ class ClassManifest
* from the cache or re-scanning for classes. * from the cache or re-scanning for classes.
* *
* @param string $base The manifest base path. * @param string $base The manifest base path.
* @param bool $includeTests Include the contents of "tests" directories.
* @param bool $forceRegen Force the manifest to be regenerated.
* @param CacheFactory $cacheFactory Optional cache to use. Set to null to not cache. * @param CacheFactory $cacheFactory Optional cache to use. Set to null to not cache.
*/ */
public function __construct( public function __construct($base, CacheFactory $cacheFactory = null)
$base, {
$includeTests = false,
$forceRegen = false,
CacheFactory $cacheFactory = null
) {
$this->base = $base; $this->base = $base;
$this->tests = $includeTests; $this->cacheFactory = $cacheFactory;
$this->cacheKey = 'manifest';
}
/**
* Initialise the class manifest
*
* @param bool $includeTests
* @param bool $forceRegen
*/
public function init($includeTests = false, $forceRegen = false)
{
// build cache from factory // build cache from factory
if ($cacheFactory) { if ($this->cacheFactory) {
$this->cache = $cacheFactory->create( $this->cache = $this->cacheFactory->create(
CacheInterface::class.'.classmanifest', CacheInterface::class.'.classmanifest',
[ 'namespace' => 'classmanifest' . ($includeTests ? '_tests' : '') ] [ 'namespace' => 'classmanifest' . ($includeTests ? '_tests' : '') ]
); );
} }
$this->cacheKey = 'manifest';
if (!$forceRegen && $this->cache && ($data = $this->cache->get($this->cacheKey))) { if (!$forceRegen && $this->cache && ($data = $this->cache->get($this->cacheKey))) {
$this->classes = $data['classes']; $this->classes = $data['classes'];
@ -151,7 +153,7 @@ class ClassManifest
$this->implementors = $data['implementors']; $this->implementors = $data['implementors'];
$this->traits = $data['traits']; $this->traits = $data['traits'];
} else { } else {
$this->regenerate(); $this->regenerate($includeTests);
} }
} }
@ -346,8 +348,10 @@ class ClassManifest
/** /**
* Completely regenerates the manifest file. * Completely regenerates the manifest file.
*
* @param bool $includeTests
*/ */
public function regenerate() public function regenerate($includeTests)
{ {
$resets = array( $resets = array(
'classes', 'roots', 'children', 'descendants', 'interfaces', 'classes', 'roots', 'children', 'descendants', 'interfaces',
@ -363,8 +367,10 @@ class ClassManifest
$finder->setOptions(array( $finder->setOptions(array(
'name_regex' => '/^[^_].*\\.php$/', 'name_regex' => '/^[^_].*\\.php$/',
'ignore_files' => array('index.php', 'main.php', 'cli-script.php'), 'ignore_files' => array('index.php', 'main.php', 'cli-script.php'),
'ignore_tests' => !$this->tests, 'ignore_tests' => !$includeTests,
'file_callback' => array($this, 'handleFile'), 'file_callback' => function ($basename, $pathname) use ($includeTests) {
$this->handleFile($basename, $pathname, $includeTests);
},
)); ));
$finder->find($this->base); $finder->find($this->base);
@ -384,7 +390,7 @@ class ClassManifest
} }
} }
public function handleFile($basename, $pathname) public function handleFile($basename, $pathname, $includeTests)
{ {
$classes = null; $classes = null;
$interfaces = null; $interfaces = null;
@ -397,6 +403,7 @@ class ClassManifest
$key = preg_replace('/[^a-zA-Z0-9_]/', '_', $basename) . '_' . md5_file($pathname); $key = preg_replace('/[^a-zA-Z0-9_]/', '_', $basename) . '_' . md5_file($pathname);
// Attempt to load from cache // Attempt to load from cache
$changed = false;
if ($this->cache if ($this->cache
&& ($data = $this->cache->get($key)) && ($data = $this->cache->get($key))
&& $this->validateItemCache($data) && $this->validateItemCache($data)
@ -405,6 +412,7 @@ class ClassManifest
$interfaces = $data['interfaces']; $interfaces = $data['interfaces'];
$traits = $data['traits']; $traits = $data['traits'];
} else { } else {
$changed = true;
// Build from php file parser // Build from php file parser
$fileContents = ClassContentRemover::remove_class_content($pathname); $fileContents = ClassContentRemover::remove_class_content($pathname);
try { try {
@ -418,23 +426,16 @@ class ClassManifest
$classes = $this->getVisitor()->getClasses(); $classes = $this->getVisitor()->getClasses();
$interfaces = $this->getVisitor()->getInterfaces(); $interfaces = $this->getVisitor()->getInterfaces();
$traits = $this->getVisitor()->getTraits(); $traits = $this->getVisitor()->getTraits();
// Save back to cache if configured
if ($this->cache) {
$cache = array(
'classes' => $classes,
'interfaces' => $interfaces,
'traits' => $traits,
);
$this->cache->set($key, $cache);
}
} }
// Merge this data into the global list // Merge this data into the global list
foreach ($classes as $className => $classInfo) { foreach ($classes as $className => $classInfo) {
$extends = isset($classInfo['extends']) ? $classInfo['extends'] : null; $extends = !empty($classInfo['extends'])
$implements = isset($classInfo['interfaces']) ? $classInfo['interfaces'] : null; ? array_map('strtolower', $classInfo['extends'])
: [];
$implements = !empty($classInfo['interfaces'])
? array_map('strtolower', $classInfo['interfaces'])
: [];
$lowercaseName = strtolower($className); $lowercaseName = strtolower($className);
if (array_key_exists($lowercaseName, $this->classes)) { if (array_key_exists($lowercaseName, $this->classes)) {
throw new Exception(sprintf( throw new Exception(sprintf(
@ -445,12 +446,20 @@ class ClassManifest
)); ));
} }
// Skip if implements TestOnly, but doesn't include tests
if (!$includeTests
&& $implements
&& in_array(strtolower(TestOnly::class), $implements)
) {
$changed = true;
unset($classes[$className]);
continue;
}
$this->classes[$lowercaseName] = $pathname; $this->classes[$lowercaseName] = $pathname;
if ($extends) { if ($extends) {
foreach ($extends as $ancestor) { foreach ($extends as $ancestor) {
$ancestor = strtolower($ancestor);
if (!isset($this->children[$ancestor])) { if (!isset($this->children[$ancestor])) {
$this->children[$ancestor] = array($className); $this->children[$ancestor] = array($className);
} else { } else {
@ -463,8 +472,6 @@ class ClassManifest
if ($implements) { if ($implements) {
foreach ($implements as $interface) { foreach ($implements as $interface) {
$interface = strtolower($interface);
if (!isset($this->implementors[$interface])) { if (!isset($this->implementors[$interface])) {
$this->implementors[$interface] = array($className); $this->implementors[$interface] = array($className);
} else { } else {
@ -480,6 +487,16 @@ class ClassManifest
foreach ($traits as $traitName => $traitInfo) { foreach ($traits as $traitName => $traitInfo) {
$this->traits[strtolower($traitName)] = $pathname; $this->traits[strtolower($traitName)] = $pathname;
} }
// Save back to cache if configured
if ($changed && $this->cache) {
$cache = array(
'classes' => $classes,
'interfaces' => $interfaces,
'traits' => $traits,
);
$this->cache->set($key, $cache);
}
} }
/** /**

View File

@ -30,7 +30,7 @@ class ClassManifestVisitor extends NodeVisitorAbstract
public function enterNode(Node $node) public function enterNode(Node $node)
{ {
if ($node instanceof Node\Stmt\Class_) { if ($node instanceof Node\Stmt\Class_) {
$extends = ''; $extends = [];
$interfaces = []; $interfaces = [];
if ($node->extends) { if ($node->extends) {

View File

@ -22,7 +22,7 @@ class ModuleLoader
*/ */
public static function inst() public static function inst()
{ {
return self::$instance ? self::$instance : self::$instance = new self(); return self::$instance ? self::$instance : self::$instance = new static();
} }
/** /**
@ -85,4 +85,17 @@ class ModuleLoader
{ {
return count($this->manifests); return count($this->manifests);
} }
/**
* Initialise the module loader
*
* @param bool $includeTests
* @param bool $forceRegen
*/
public function init($includeTests = false, $forceRegen = false)
{
foreach ($this->manifests as $manifest) {
$manifest->init($includeTests, $forceRegen);
}
}
} }

View File

@ -26,11 +26,11 @@ class ModuleManifest
protected $cacheKey; protected $cacheKey;
/** /**
* Whether `test` directories should be searched when searching for configuration * Factory to use to build cache
* *
* @var bool * @var CacheFactory
*/ */
protected $includeTests; protected $cacheFactory;
/** /**
* @var CacheInterface * @var CacheInterface
@ -87,19 +87,24 @@ class ModuleManifest
* from the cache or re-scanning for classes. * from the cache or re-scanning for classes.
* *
* @param string $base The project base path. * @param string $base The project base path.
* @param bool $includeTests
* @param bool $forceRegen Force the manifest to be regenerated.
* @param CacheFactory $cacheFactory Cache factory to use * @param CacheFactory $cacheFactory Cache factory to use
*/ */
public function __construct($base, $includeTests = false, $forceRegen = false, CacheFactory $cacheFactory = null) public function __construct($base, CacheFactory $cacheFactory = null)
{ {
$this->base = $base; $this->base = $base;
$this->cacheKey = sha1($base).'_modules'; $this->cacheKey = sha1($base) . '_modules';
$this->includeTests = $includeTests; $this->cacheFactory = $cacheFactory;
}
/**
* @param bool $includeTests
* @param bool $forceRegen Force the manifest to be regenerated.
*/
public function init($includeTests = false, $forceRegen = false)
{
// build cache from factory // build cache from factory
if ($cacheFactory) { if ($this->cacheFactory) {
$this->cache = $cacheFactory->create( $this->cache = $this->cacheFactory->create(
CacheInterface::class.'.modulemanifest', CacheInterface::class.'.modulemanifest',
[ 'namespace' => 'modulemanifest' . ($includeTests ? '_tests' : '') ] [ 'namespace' => 'modulemanifest' . ($includeTests ? '_tests' : '') ]
); );

View File

@ -0,0 +1,119 @@
<?php
namespace SilverStripe\Core\Startup;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Core\Application;
use SilverStripe\Control\HTTPMiddleware;
use SilverStripe\Security\Permission;
use SilverStripe\Security\Security;
/**
* Decorates application bootstrapping with errorcontrolchain
*/
class ErrorControlChainMiddleware implements HTTPMiddleware
{
/**
* @var Application
*/
protected $application = null;
/**
* Build error control chain for an application
*
* @param Application $application
*/
public function __construct(Application $application)
{
$this->application = $application;
}
public function process(HTTPRequest $request, callable $next)
{
$result = null;
// Prepare tokens and execute chain
$reloadToken = ParameterConfirmationToken::prepare_tokens(
['isTest', 'isDev', 'flush'],
$request
);
$chain = new ErrorControlChain();
$chain
->then(function () use ($request, $chain, $reloadToken, $next, &$result) {
// If no redirection is necessary then we can disable error supression
if (!$reloadToken) {
$chain->setSuppression(false);
}
try {
// Check if a token is requesting a redirect
if ($reloadToken) {
$result = $this->safeReloadWithToken($request, $reloadToken);
} else {
// If no reload necessary, process application
$result = call_user_func($next, $request);
}
} catch (HTTPResponse_Exception $exception) {
$result = $exception->getResponse();
}
})
// Finally if a token was requested but there was an error while figuring out if it's allowed, do it anyway
->thenIfErrored(function () use ($reloadToken, &$result) {
if ($reloadToken) {
$result = $reloadToken->reloadWithToken();
}
})
->execute();
return $result;
}
/**
* Reload application with the given token, but only if either the user is authenticated,
* or authentication is impossible.
*
* @param HTTPRequest $request
* @param ParameterConfirmationToken $reloadToken
* @return HTTPResponse
*/
protected function safeReloadWithToken(HTTPRequest $request, $reloadToken)
{
// Safe reload requires manual boot
$this->getApplication()->getKernel()->boot(false);
// Ensure session is started
$request->getSession()->init();
// Next, check if we're in dev mode, or the database doesn't have any security data, or we are admin
if (Director::isDev() || !Security::database_is_ready() || Permission::check('ADMIN')) {
return $reloadToken->reloadWithToken();
}
// Fail and redirect the user to the login page
$loginPage = Director::absoluteURL(Security::config()->get('login_url'));
$loginPage .= "?BackURL=" . urlencode($request->getURL());
$result = new HTTPResponse();
$result->redirect($loginPage);
return $result;
}
/**
* @return Application
*/
public function getApplication()
{
return $this->application;
}
/**
* @param Application $application
* @return $this
*/
public function setApplication(Application $application)
{
$this->application = $application;
return $this;
}
}

View File

@ -2,7 +2,10 @@
namespace SilverStripe\Core\Startup; namespace SilverStripe\Core\Startup;
use SilverStripe\Control\Director; use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Convert;
use SilverStripe\Security\RandomGenerator; use SilverStripe\Security\RandomGenerator;
/** /**
@ -25,6 +28,11 @@ class ParameterConfirmationToken
*/ */
protected $parameterName = null; protected $parameterName = null;
/**
* @var HTTPRequest
*/
protected $request = null;
/** /**
* The parameter given * The parameter given
* *
@ -88,17 +96,19 @@ class ParameterConfirmationToken
* Create a new ParameterConfirmationToken * Create a new ParameterConfirmationToken
* *
* @param string $parameterName Name of the querystring parameter to check * @param string $parameterName Name of the querystring parameter to check
* @param HTTPRequest $request
*/ */
public function __construct($parameterName) public function __construct($parameterName, HTTPRequest $request)
{ {
// Store the parameter name // Store the parameter name
$this->parameterName = $parameterName; $this->parameterName = $parameterName;
$this->request = $request;
// Store the parameter value // Store the parameter value
$this->parameter = isset($_GET[$parameterName]) ? $_GET[$parameterName] : null; $this->parameter = $request->getVar($parameterName);
// If the token provided is valid, mark it as such // If the token provided is valid, mark it as such
$token = isset($_GET[$parameterName.'token']) ? $_GET[$parameterName.'token'] : null; $token = $request->getVar($parameterName.'token');
if ($this->checkToken($token)) { if ($this->checkToken($token)) {
$this->token = $token; $this->token = $token;
} }
@ -151,7 +161,7 @@ class ParameterConfirmationToken
*/ */
public function suppress() public function suppress()
{ {
unset($_GET[$this->parameterName]); $this->request->offsetUnset($this->parameterName);
} }
/** /**
@ -167,81 +177,45 @@ class ParameterConfirmationToken
); );
} }
/** What to use instead of BASE_URL. Must not contain protocol or host. @var string */ /**
static public $alternateBaseURL = null; * Get redirect url, excluding querystring
*
protected function currentAbsoluteURL() * @return string
*/
protected function currentURL()
{ {
global $url; return Controller::join_links(
BASE_URL,
// Are we http or https? Replicates Director::is_https() without its dependencies/ '/',
$proto = 'http'; $this->request->getURL(false)
// See https://en.wikipedia.org/wiki/List_of_HTTP_header_fields );
// See https://support.microsoft.com/en-us/kb/307347
$headerOverride = false;
if (TRUSTED_PROXY) {
$headers = (getenv('SS_TRUSTED_PROXY_PROTOCOL_HEADER')) ? array(getenv('SS_TRUSTED_PROXY_PROTOCOL_HEADER')) : null;
if (!$headers) {
// Backwards compatible defaults
$headers = array('HTTP_X_FORWARDED_PROTO', 'HTTP_X_FORWARDED_PROTOCOL', 'HTTP_FRONT_END_HTTPS');
}
foreach ($headers as $header) {
$headerCompareVal = ($header === 'HTTP_FRONT_END_HTTPS' ? 'on' : 'https');
if (!empty($_SERVER[$header]) && strtolower($_SERVER[$header]) == $headerCompareVal) {
$headerOverride = true;
break;
}
}
}
if ($headerOverride) {
$proto = 'https';
} elseif ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')) {
$proto = 'https';
} elseif (isset($_SERVER['SSL'])) {
$proto = 'https';
}
$parts = array_filter(array(
// What's our host
Director::host(),
// SilverStripe base
self::$alternateBaseURL !== null ? self::$alternateBaseURL : BASE_URL,
// And URL including base script (eg: if it's index.php/page/url/)
(defined('BASE_SCRIPT_URL') ? '/' . BASE_SCRIPT_URL : '') . $url,
));
// Join together with protocol into our current absolute URL, avoiding duplicated "/" characters
return "$proto://" . preg_replace('#/{2,}#', '/', implode('/', $parts));
} }
/** /**
* Forces a reload of the request with the token included * Forces a reload of the request with the token included
* This method will terminate the script with `die` *
* @return HTTPResponse
*/ */
public function reloadWithToken() public function reloadWithToken()
{ {
$location = $this->currentAbsoluteURL(); // Merge get params with current url
$params = array_merge($this->request->getVars(), $this->params());
$location = Controller::join_links(
$this->currentURL(),
'?'.http_build_query($params)
);
$locationJS = Convert::raw2js($location);
$locationATT = Convert::raw2att($location);
$body = <<<HTML
<script>location.href='$locationJS';</script>
<noscript><meta http-equiv="refresh" content="0; url=$locationATT"></noscript>
You are being redirected. If you are not redirected soon, <a href="$locationATT">click here to continue the flush</a>
HTML;
// What's our GET params (ensuring they include the original parameter + a new token) // Build response
$params = array_merge($_GET, $this->params()); $result = new HTTPResponse($body);
unset($params['url']); $result->redirect($location);
return $result;
if ($params) {
$location .= '?'.http_build_query($params);
}
// And redirect
if (headers_sent()) {
echo "
<script>location.href='$location';</script>
<noscript><meta http-equiv='refresh' content='0; url=$location'></noscript>
You are being redirected. If you are not redirected soon, <a href='$location'>click here to continue the flush</a>
";
} else {
header('location: '.$location, true, 302);
}
die;
} }
/** /**
@ -249,13 +223,14 @@ You are being redirected. If you are not redirected soon, <a href='$location'>cl
* return the non-validated token with the highest priority * return the non-validated token with the highest priority
* *
* @param array $keys List of token keys in ascending priority (low to high) * @param array $keys List of token keys in ascending priority (low to high)
* @param HTTPRequest $request
* @return ParameterConfirmationToken The token container for the unvalidated $key given with the highest priority * @return ParameterConfirmationToken The token container for the unvalidated $key given with the highest priority
*/ */
public static function prepare_tokens($keys) public static function prepare_tokens($keys, HTTPRequest $request)
{ {
$target = null; $target = null;
foreach ($keys as $key) { foreach ($keys as $key) {
$token = new ParameterConfirmationToken($key); $token = new ParameterConfirmationToken($key, $request);
// Validate this token // Validate this token
if ($token->reloadRequired()) { if ($token->reloadRequired()) {
$token->suppress(); $token->suppress();

117
src/Core/TempFolder.php Normal file
View File

@ -0,0 +1,117 @@
<?php
namespace SilverStripe\Core;
use Exception;
/**
* Guesses location for temp folder
*/
class TempFolder
{
/**
* Returns the temporary folder path that silverstripe should use for its cache files.
*
* @param string $base The base path to use for determining the temporary path
* @return string Path to temp
*/
public static function getTempFolder($base)
{
$parent = static::getTempParentFolder($base);
// The actual temp folder is a subfolder of getTempParentFolder(), named by username
$subfolder = $parent . DIRECTORY_SEPARATOR . static::getTempFolderUsername();
if (!@file_exists($subfolder)) {
mkdir($subfolder);
}
return $subfolder;
}
/**
* Returns as best a representation of the current username as we can glean.
*
* @return string
*/
public static function getTempFolderUsername()
{
$user = getenv('APACHE_RUN_USER');
if (!$user) {
$user = getenv('USER');
}
if (!$user) {
$user = getenv('USERNAME');
}
if (!$user && function_exists('posix_getpwuid') && function_exists('posix_getuid')) {
$userDetails = posix_getpwuid(posix_getuid());
$user = $userDetails['name'];
}
if (!$user) {
$user = 'unknown';
}
$user = preg_replace('/[^A-Za-z0-9_\-]/', '', $user);
return $user;
}
/**
* Return the parent folder of the temp folder.
* The temp folder will be a subfolder of this, named by username.
* This structure prevents permission problems.
*
* @param string $base
* @return string
* @throws Exception
*/
protected static function getTempParentFolder($base)
{
// first, try finding a silverstripe-cache dir built off the base path
$tempPath = $base . DIRECTORY_SEPARATOR . 'silverstripe-cache';
if (@file_exists($tempPath)) {
if ((fileperms($tempPath) & 0777) != 0777) {
@chmod($tempPath, 0777);
}
return $tempPath;
}
// failing the above, try finding a namespaced silverstripe-cache dir in the system temp
$tempPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR .
'silverstripe-cache-php' . preg_replace('/[^\w-\.+]+/', '-', PHP_VERSION) .
str_replace(array(' ', '/', ':', '\\'), '-', $base);
if (!@file_exists($tempPath)) {
$oldUMask = umask(0);
@mkdir($tempPath, 0777);
umask($oldUMask);
// if the folder already exists, correct perms
} else {
if ((fileperms($tempPath) & 0777) != 0777) {
@chmod($tempPath, 0777);
}
}
$worked = @file_exists($tempPath) && @is_writable($tempPath);
// failing to use the system path, attempt to create a local silverstripe-cache dir
if (!$worked) {
$tempPath = $base . DIRECTORY_SEPARATOR . 'silverstripe-cache';
if (!@file_exists($tempPath)) {
$oldUMask = umask(0);
@mkdir($tempPath, 0777);
umask($oldUMask);
}
$worked = @file_exists($tempPath) && @is_writable($tempPath);
}
if (!$worked) {
throw new Exception(
'Permission problem gaining access to a temp folder. ' .
'Please create a folder named silverstripe-cache in the base folder ' .
'of the installation and ensure it has the correct permissions'
);
}
return $tempPath;
}
}

View File

@ -1,111 +0,0 @@
<?php
/**
* Returns the temporary folder path that silverstripe should use for its cache files.
*
* @param string $base The base path to use for determining the temporary path
* @return string Path to temp
*/
function getTempFolder($base = null)
{
$parent = getTempParentFolder($base);
// The actual temp folder is a subfolder of getTempParentFolder(), named by username
$subfolder = $parent . DIRECTORY_SEPARATOR . getTempFolderUsername();
if (!@file_exists($subfolder)) {
mkdir($subfolder);
}
return $subfolder;
}
/**
* Returns as best a representation of the current username as we can glean.
*
* @return string
*/
function getTempFolderUsername()
{
$user = getenv('APACHE_RUN_USER');
if (!$user) {
$user = getenv('USER');
}
if (!$user) {
$user = getenv('USERNAME');
}
if (!$user && function_exists('posix_getpwuid') && function_exists('posix_getuid')) {
$userDetails = posix_getpwuid(posix_getuid());
$user = $userDetails['name'];
}
if (!$user) {
$user = 'unknown';
}
$user = preg_replace('/[^A-Za-z0-9_\-]/', '', $user);
return $user;
}
/**
* Return the parent folder of the temp folder.
* The temp folder will be a subfolder of this, named by username.
* This structure prevents permission problems.
*
* @param string $base
* @return string
* @throws Exception
*/
function getTempParentFolder($base = null)
{
if (!$base && defined('BASE_PATH')) {
$base = BASE_PATH;
}
// first, try finding a silverstripe-cache dir built off the base path
$tempPath = $base . DIRECTORY_SEPARATOR . 'silverstripe-cache';
if (@file_exists($tempPath)) {
if ((fileperms($tempPath) & 0777) != 0777) {
@chmod($tempPath, 0777);
}
return $tempPath;
}
// failing the above, try finding a namespaced silverstripe-cache dir in the system temp
$tempPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR .
'silverstripe-cache-php' . preg_replace('/[^\w-\.+]+/', '-', PHP_VERSION) .
str_replace(array(' ', '/', ':', '\\'), '-', $base);
if (!@file_exists($tempPath)) {
$oldUMask = umask(0);
@mkdir($tempPath, 0777);
umask($oldUMask);
// if the folder already exists, correct perms
} else {
if ((fileperms($tempPath) & 0777) != 0777) {
@chmod($tempPath, 0777);
}
}
$worked = @file_exists($tempPath) && @is_writable($tempPath);
// failing to use the system path, attempt to create a local silverstripe-cache dir
if (!$worked) {
$tempPath = $base . DIRECTORY_SEPARATOR . 'silverstripe-cache';
if (!@file_exists($tempPath)) {
$oldUMask = umask(0);
@mkdir($tempPath, 0777);
umask($oldUMask);
}
$worked = @file_exists($tempPath) && @is_writable($tempPath);
}
if (!$worked) {
throw new Exception(
'Permission problem gaining access to a temp folder. ' .
'Please create a folder named silverstripe-cache in the base folder ' .
'of the installation and ensure it has the correct permissions'
);
}
return $tempPath;
}

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Dev; namespace SilverStripe\Dev;
use SilverStripe\Core\Environment;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\View\ViewableData; use SilverStripe\View\ViewableData;
@ -142,8 +143,8 @@ abstract class BulkLoader extends ViewableData
*/ */
public function load($filepath) public function load($filepath)
{ {
increase_time_limit_to(3600); Environment::increaseTimeLimitTo(3600);
increase_memory_limit_to('512M'); Environment::increaseMemoryLimitTo('512M');
//get all instances of the to be imported data object //get all instances of the to be imported data object
if ($this->deleteExistingRecords) { if ($this->deleteExistingRecords) {

View File

@ -69,7 +69,7 @@ class CSSContentParser
* See {@link getByXpath()} for a more direct selector syntax. * See {@link getByXpath()} for a more direct selector syntax.
* *
* @param String $selector * @param String $selector
* @return SimpleXMLElement * @return SimpleXMLElement[]
*/ */
public function getBySelector($selector) public function getBySelector($selector)
{ {
@ -81,7 +81,7 @@ class CSSContentParser
* Allows querying the content through XPATH selectors. * Allows querying the content through XPATH selectors.
* *
* @param String $xpath SimpleXML compatible XPATH statement * @param String $xpath SimpleXML compatible XPATH statement
* @return SimpleXMLElement|false * @return SimpleXMLElement[]
*/ */
public function getByXpath($xpath) public function getByXpath($xpath)
{ {

View File

@ -21,7 +21,7 @@ class DevBuildController extends Controller
{ {
if (Director::is_cli()) { if (Director::is_cli()) {
$da = DatabaseAdmin::create(); $da = DatabaseAdmin::create();
return $da->handleRequest($request, $this->model); return $da->handleRequest($request);
} else { } else {
$renderer = DebugView::create(); $renderer = DebugView::create();
echo $renderer->renderHeader(); echo $renderer->renderHeader();
@ -29,7 +29,7 @@ class DevBuildController extends Controller
echo "<div class=\"build\">"; echo "<div class=\"build\">";
$da = DatabaseAdmin::create(); $da = DatabaseAdmin::create();
$response = $da->handleRequest($request, $this->model); $response = $da->handleRequest($request);
echo "</div>"; echo "</div>";
echo $renderer->renderFooter(); echo $renderer->renderFooter();

View File

@ -42,6 +42,16 @@ class DevelopmentAdmin extends Controller
'generatesecuretoken', 'generatesecuretoken',
); );
/**
* Assume that CLI equals admin permissions
* If set to false, normal permission model will apply even in CLI mode
* Applies to all development admin tasks (E.g. TaskRunner, DatabaseAdmin)
*
* @config
* @var bool
*/
private static $allow_all_cli = true;
protected function init() protected function init()
{ {
parent::init(); parent::init();
@ -52,10 +62,11 @@ class DevelopmentAdmin extends Controller
// We allow access to this controller regardless of live-status or ADMIN permission only // We allow access to this controller regardless of live-status or ADMIN permission only
// if on CLI. Access to this controller is always allowed in "dev-mode", or of the user is ADMIN. // if on CLI. Access to this controller is always allowed in "dev-mode", or of the user is ADMIN.
$allowAllCLI = static::config()->get('allow_all_cli');
$canAccess = ( $canAccess = (
$requestedDevBuild $requestedDevBuild
|| Director::isDev() || Director::isDev()
|| Director::is_cli() || (Director::is_cli() && $allowAllCLI)
// Its important that we don't run this check if dev/build was requested // Its important that we don't run this check if dev/build was requested
|| Permission::check("ADMIN") || Permission::check("ADMIN")
); );

View File

@ -2,13 +2,13 @@
namespace SilverStripe\Dev; namespace SilverStripe\Dev;
use SilverStripe\Assets\File;
use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\DataObject;
use SilverStripe\Core\Config\Config;
use InvalidArgumentException;
use Exception; use Exception;
use InvalidArgumentException;
use SilverStripe\Assets\File;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
/** /**
* A blueprint on how to create instances of a certain {@link DataObject} subclass. * A blueprint on how to create instances of a certain {@link DataObject} subclass.
@ -94,7 +94,7 @@ class FixtureBlueprint
try { try {
$class = $this->class; $class = $this->class;
$schema = DataObject::getSchema(); $schema = DataObject::getSchema();
$obj = DataModel::inst()->$class->newObject(); $obj = Injector::inst()->create($class);
// If an ID is explicitly passed, then we'll sort out the initial write straight away // If an ID is explicitly passed, then we'll sort out the initial write straight away
// This is just in case field setters triggered by the population code in the next block // This is just in case field setters triggered by the population code in the next block

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Dev; namespace SilverStripe\Dev;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
@ -33,7 +34,7 @@ use SimpleXMLElement;
* } * }
* </code> * </code>
*/ */
class FunctionalTest extends SapphireTest class FunctionalTest extends SapphireTest implements TestOnly
{ {
/** /**
* Set this to true on your sub-class to disable the use of themes in this test. * Set this to true on your sub-class to disable the use of themes in this test.
@ -84,12 +85,13 @@ class FunctionalTest extends SapphireTest
protected function setUp() protected function setUp()
{ {
parent::setUp();
// Skip calling FunctionalTest directly. // Skip calling FunctionalTest directly.
if (static::class == __CLASS__) { if (static::class == __CLASS__) {
$this->markTestSkipped(sprintf('Skipping %s ', static::class)); $this->markTestSkipped(sprintf('Skipping %s ', static::class));
} }
parent::setUp();
$this->mainSession = new TestSession(); $this->mainSession = new TestSession();
// Disable theme, if necessary // Disable theme, if necessary
@ -114,9 +116,8 @@ class FunctionalTest extends SapphireTest
protected function tearDown() protected function tearDown()
{ {
SecurityToken::enable(); SecurityToken::enable();
parent::tearDown();
unset($this->mainSession); unset($this->mainSession);
parent::tearDown();
} }
/** /**
@ -408,11 +409,11 @@ class FunctionalTest extends SapphireTest
public function useDraftSite($enabled = true) public function useDraftSite($enabled = true)
{ {
if ($enabled) { if ($enabled) {
$this->session()->inst_set('readingMode', 'Stage.Stage'); $this->session()->set('readingMode', 'Stage.Stage');
$this->session()->inst_set('unsecuredDraftSite', true); $this->session()->set('unsecuredDraftSite', true);
} else { } else {
$this->session()->inst_set('readingMode', 'Stage.Live'); $this->session()->set('readingMode', 'Stage.Live');
$this->session()->inst_set('unsecuredDraftSite', false); $this->session()->set('unsecuredDraftSite', false);
} }
} }

View File

@ -0,0 +1,1131 @@
<?php
namespace SilverStripe\Dev\Install;
use Exception;
use InvalidArgumentException;
use SilverStripe\Core\TempFolder;
/**
* This class checks requirements
* Each of the requireXXX functions takes an argument which gives a user description of the test.
* It's an array of 3 parts:
* $description[0] - The test catetgory
* $description[1] - The test title
* $description[2] - The test error to show, if it goes wrong
*/
class InstallRequirements
{
protected $errors = [];
protected $warnings = [];
protected $tests = [];
/**
* Backup of original ini settings
* @var array
*/
protected $originalIni = [];
/**
* Check the database configuration. These are done one after another
* starting with checking the database function exists in PHP, and
* continuing onto more difficult checks like database permissions.
*
* @param array $databaseConfig The list of database parameters
* @return boolean Validity of database configuration details
*/
public function checkDatabase($databaseConfig)
{
// Check if support is available
if (!$this->requireDatabaseFunctions(
$databaseConfig,
array(
"Database Configuration",
"Database support",
"Database support in PHP",
$this->getDatabaseTypeNice($databaseConfig['type'])
)
)
) {
return false;
}
// Check if the server is available
$usePath = !empty($databaseConfig['path']) && empty($databaseConfig['server']);
if (!$this->requireDatabaseServer(
$databaseConfig,
array(
"Database Configuration",
"Database server",
$usePath
? "I couldn't write to path '$databaseConfig[path]'"
: "I couldn't find a database server on '$databaseConfig[server]'",
$usePath ? $databaseConfig['path'] : $databaseConfig['server']
)
)
) {
return false;
}
// Check if the connection credentials allow access to the server / database
if (!$this->requireDatabaseConnection(
$databaseConfig,
array(
"Database Configuration",
"Database access credentials",
"That username/password doesn't work"
)
)
) {
return false;
}
// Check the necessary server version is available
if (!$this->requireDatabaseVersion(
$databaseConfig,
array(
"Database Configuration",
"Database server version requirement",
'',
'Version ' . $this->getDatabaseConfigurationHelper($databaseConfig['type'])->getDatabaseVersion($databaseConfig)
)
)
) {
return false;
}
// Check that database creation permissions are available
if (!$this->requireDatabaseOrCreatePermissions(
$databaseConfig,
array(
"Database Configuration",
"Can I access/create the database",
"I can't create new databases and the database '$databaseConfig[database]' doesn't exist"
)
)
) {
return false;
}
// Check alter permission (necessary to create tables etc)
if (!$this->requireDatabaseAlterPermissions(
$databaseConfig,
array(
"Database Configuration",
"Can I ALTER tables",
"I don't have permission to ALTER tables"
)
)
) {
return false;
}
// Success!
return true;
}
public function checkAdminConfig($adminConfig)
{
if (!$adminConfig['username']) {
$this->error(array('', 'Please enter a username!'));
}
if (!$adminConfig['password']) {
$this->error(array('', 'Please enter a password!'));
}
}
/**
* Check if the web server is IIS and version greater than the given version.
*
* @param int $fromVersion
* @return bool
*/
public function isIIS($fromVersion = 7)
{
if (strpos($this->findWebserver(), 'IIS/') === false) {
return false;
}
return substr(strstr($this->findWebserver(), '/'), -3, 1) >= $fromVersion;
}
public function isApache()
{
if (strpos($this->findWebserver(), 'Apache') !== false) {
return true;
} else {
return false;
}
}
/**
* Find the webserver software running on the PHP host.
* @return string|boolean Server software or boolean FALSE
*/
public function findWebserver()
{
// Try finding from SERVER_SIGNATURE or SERVER_SOFTWARE
if (!empty($_SERVER['SERVER_SIGNATURE'])) {
$webserver = $_SERVER['SERVER_SIGNATURE'];
} elseif (!empty($_SERVER['SERVER_SOFTWARE'])) {
$webserver = $_SERVER['SERVER_SOFTWARE'];
} else {
return false;
}
return strip_tags(trim($webserver));
}
/**
* Check everything except the database
*/
public function check()
{
$this->errors = [];
$isApache = $this->isApache();
$isIIS = $this->isIIS();
$webserver = $this->findWebserver();
$this->requirePHPVersion('5.5.0', '5.5.0', array(
"PHP Configuration",
"PHP5 installed",
null,
"PHP version " . phpversion()
));
// Check that we can identify the root folder successfully
$this->requireFile('framework/src/Dev/Install/config-form.html', array(
"File permissions",
"Does the webserver know where files are stored?",
"The webserver isn't letting me identify where files are stored.",
$this->getBaseDir()
));
$this->requireModule('mysite', array(
"File permissions",
"mysite/ directory exists?",
''
));
$this->requireModule('framework', array(
"File permissions",
"framework/ directory exists?",
'',
));
if ($isApache) {
$this->checkApacheVersion(array(
"Webserver Configuration",
"Webserver is not Apache 1.x",
"SilverStripe requires Apache version 2 or greater",
$webserver
));
$this->requireWriteable('.htaccess', array("File permissions", "Is the .htaccess file writeable?", null));
} elseif ($isIIS) {
$this->requireWriteable('web.config', array("File permissions", "Is the web.config file writeable?", null));
}
$this->requireWriteable('mysite/_config.php', array(
"File permissions",
"Is the mysite/_config.php file writeable?",
null
));
$this->requireWriteable('mysite/_config/config.yml', array(
"File permissions",
"Is the mysite/_config/config.yml file writeable?",
null
));
if (!$this->checkModuleExists('cms')) {
$this->requireWriteable('mysite/code/RootURLController.php', array(
"File permissions",
"Is the mysite/code/RootURLController.php file writeable?",
null
));
}
$this->requireWriteable('assets', array("File permissions", "Is the assets/ directory writeable?", null));
try {
$tempFolder = TempFolder::getTempFolder(BASE_PATH);
} catch (Exception $e) {
$tempFolder = false;
}
$this->requireTempFolder(array('File permissions', 'Is a temporary directory available?', null, $tempFolder));
if ($tempFolder) {
// in addition to the temp folder being available, check it is writable
$this->requireWriteable($tempFolder, array(
"File permissions",
sprintf("Is the temporary directory writeable?", $tempFolder),
null
), true);
}
// Check for web server, unless we're calling the installer from the command-line
$this->isRunningWebServer(array("Webserver Configuration", "Server software", "Unknown", $webserver));
if ($isApache) {
$this->requireApacheRewriteModule('mod_rewrite', array(
"Webserver Configuration",
"URL rewriting support",
"You need mod_rewrite to use friendly URLs with SilverStripe, but it is not enabled."
));
} elseif ($isIIS) {
$this->requireIISRewriteModule('IIS_UrlRewriteModule', array(
"Webserver Configuration",
"URL rewriting support",
"You need to enable the IIS URL Rewrite Module to use friendly URLs with SilverStripe, "
. "but it is not installed or enabled. Download it for IIS 7 from http://www.iis.net/expand/URLRewrite"
));
} else {
$this->warning(array(
"Webserver Configuration",
"URL rewriting support",
"I can't tell whether any rewriting module is running. You may need to configure a rewriting rule yourself."
));
}
$this->requireServerVariables(array('SCRIPT_NAME', 'HTTP_HOST', 'SCRIPT_FILENAME'), array(
"Webserver Configuration",
"Recognised webserver",
"You seem to be using an unsupported webserver. "
. "The server variables SCRIPT_NAME, HTTP_HOST, SCRIPT_FILENAME need to be set."
));
$this->requirePostSupport(array(
"Webserver Configuration",
"POST Support",
'I can\'t find $_POST, make sure POST is enabled.'
));
// Check for GD support
if (!$this->requireFunction("imagecreatetruecolor", array(
"PHP Configuration",
"GD2 support",
"PHP must have GD version 2."
))
) {
$this->requireFunction("imagecreate", array(
"PHP Configuration",
"GD2 support",
"GD support for PHP not included."
));
}
// Check for XML support
$this->requireFunction('xml_set_object', array(
"PHP Configuration",
"XML support",
"XML support not included in PHP."
));
$this->requireClass('DOMDocument', array(
"PHP Configuration",
"DOM/XML support",
"DOM/XML support not included in PHP."
));
$this->requireFunction('simplexml_load_file', array(
'PHP Configuration',
'SimpleXML support',
'SimpleXML support not included in PHP.'
));
// Check for token_get_all
$this->requireFunction('token_get_all', array(
"PHP Configuration",
"Tokenizer support",
"Tokenizer support not included in PHP."
));
// Check for CType support
$this->requireFunction('ctype_digit', array(
'PHP Configuration',
'CType support',
'CType support not included in PHP.'
));
// Check for session support
$this->requireFunction('session_start', array(
'PHP Configuration',
'Session support',
'Session support not included in PHP.'
));
// Check for iconv support
$this->requireFunction('iconv', array(
'PHP Configuration',
'iconv support',
'iconv support not included in PHP.'
));
// Check for hash support
$this->requireFunction('hash', array('PHP Configuration', 'hash support', 'hash support not included in PHP.'));
// Check for mbstring support
$this->requireFunction('mb_internal_encoding', array(
'PHP Configuration',
'mbstring support',
'mbstring support not included in PHP.'
));
// Check for Reflection support
$this->requireClass('ReflectionClass', array(
'PHP Configuration',
'Reflection support',
'Reflection support not included in PHP.'
));
// Check for Standard PHP Library (SPL) support
$this->requireFunction('spl_classes', array(
'PHP Configuration',
'SPL support',
'Standard PHP Library (SPL) not included in PHP.'
));
$this->requireDateTimezone(array(
'PHP Configuration',
'date.timezone setting and validity',
'date.timezone option in php.ini must be set correctly.',
$this->getOriginalIni('date.timezone')
));
$this->suggestClass('finfo', array(
'PHP Configuration',
'fileinfo support',
'fileinfo should be enabled in PHP. SilverStripe uses it for MIME type detection of files. '
. 'SilverStripe will still operate, but email attachments and sending files to browser '
. '(e.g. export data to CSV) may not work correctly without finfo.'
));
$this->suggestFunction('curl_init', array(
'PHP Configuration',
'curl support',
'curl should be enabled in PHP. SilverStripe uses it for consuming web services'
. ' via the RestfulService class and many modules rely on it.'
));
$this->suggestClass('tidy', array(
'PHP Configuration',
'tidy support',
'Tidy provides a library of code to clean up your html. '
. 'SilverStripe will operate fine without tidy but HTMLCleaner will not be effective.'
));
$this->suggestPHPSetting('asp_tags', array(false), array(
'PHP Configuration',
'asp_tags option',
'This should be turned off as it can cause issues with SilverStripe'
));
$this->requirePHPSetting('magic_quotes_gpc', array(false), array(
'PHP Configuration',
'magic_quotes_gpc option',
'This should be turned off, as it can cause issues with cookies. '
. 'More specifically, unserializing data stored in cookies.'
));
$this->suggestPHPSetting('display_errors', array(false), array(
'PHP Configuration',
'display_errors option',
'Unless you\'re in a development environment, this should be turned off, '
. 'as it can expose sensitive data to website users.'
));
// on some weirdly configured webservers arg_separator.output is set to &amp;
// which will results in links like ?param=value&amp;foo=bar which will not be i
$this->suggestPHPSetting('arg_separator.output', array('&', ''), array(
'PHP Configuration',
'arg_separator.output option',
'This option defines how URL parameters are concatenated. '
. 'If not set to \'&\' this may cause issues with URL GET parameters'
));
// always_populate_raw_post_data should be set to -1 if PHP < 7.0
if (version_compare(PHP_VERSION, '7.0.0', '<')) {
$this->suggestPHPSetting('always_populate_raw_post_data', ['-1'], [
'PHP Configuration',
'always_populate_raw_post_data option',
'It\'s highly recommended to set this to \'-1\' in php 5.x, as $HTTP_RAW_POST_DATA is removed in php 7'
]);
}
// Check memory allocation
$this->requireMemory(32 * 1024 * 1024, 64 * 1024 * 1024, array(
"PHP Configuration",
"Memory allocation (PHP config option 'memory_limit')",
"SilverStripe needs a minimum of 32M allocated to PHP, but recommends 64M.",
$this->getOriginalIni("memory_limit")
));
return $this->errors;
}
/**
* Get ini setting
*
* @param string $settingName
* @return mixed
*/
protected function getOriginalIni($settingName)
{
if (isset($this->originalIni[$settingName])) {
return $this->originalIni[$settingName];
}
return ini_get($settingName);
}
public function suggestPHPSetting($settingName, $settingValues, $testDetails)
{
$this->testing($testDetails);
// special case for display_errors, check the original value before
// it was changed at the start of this script.
$val = $this->getOriginalIni($settingName);
if (!in_array($val, $settingValues) && $val != $settingValues) {
$this->warning($testDetails, "$settingName is set to '$val' in php.ini. $testDetails[2]");
}
}
public function requirePHPSetting($settingName, $settingValues, $testDetails)
{
$this->testing($testDetails);
$val = $this->getOriginalIni($settingName);
if (!in_array($val, $settingValues) && $val != $settingValues) {
$this->error($testDetails, "$settingName is set to '$val' in php.ini. $testDetails[2]");
}
}
public function suggestClass($class, $testDetails)
{
$this->testing($testDetails);
if (!class_exists($class)) {
$this->warning($testDetails);
}
}
public function suggestFunction($class, $testDetails)
{
$this->testing($testDetails);
if (!function_exists($class)) {
$this->warning($testDetails);
}
}
public function requireDateTimezone($testDetails)
{
$this->testing($testDetails);
$val = $this->getOriginalIni('date.timezone');
$result = $val && in_array($val, timezone_identifiers_list());
if (!$result) {
$this->error($testDetails);
}
}
public function requireMemory($min, $recommended, $testDetails)
{
$_SESSION['forcemem'] = false;
$mem = $this->getPHPMemory();
$memLimit = $this->getOriginalIni("memory_limit");
if ($mem < (64 * 1024 * 1024)) {
ini_set('memory_limit', '64M');
$mem = $this->getPHPMemory();
$testDetails[3] = $memLimit;
}
$this->testing($testDetails);
if ($mem < $min && $mem > 0) {
$message = $testDetails[2] . " You only have " . $memLimit . " allocated";
$this->error($testDetails, $message);
return false;
} elseif ($mem < $recommended && $mem > 0) {
$message = $testDetails[2] . " You only have " . $memLimit . " allocated";
$this->warning($testDetails, $message);
return false;
} elseif ($mem == 0) {
$message = $testDetails[2] . " We can't determine how much memory you have allocated. "
. "Install only if you're sure you've allocated at least 20 MB.";
$this->warning($testDetails, $message);
return false;
}
return true;
}
public function getPHPMemory()
{
$memString = $this->getOriginalIni("memory_limit");
switch (strtolower(substr($memString, -1))) {
case "k":
return round(substr($memString, 0, -1) * 1024);
case "m":
return round(substr($memString, 0, -1) * 1024 * 1024);
case "g":
return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
default:
return round($memString);
}
}
public function listErrors()
{
if ($this->errors) {
echo "<p>The following problems are preventing me from installing SilverStripe CMS:</p>\n\n";
foreach ($this->errors as $error) {
echo "<li>" . htmlentities(implode(", ", $error), ENT_COMPAT, 'UTF-8') . "</li>\n";
}
}
}
public function showTable($section = null)
{
if ($section) {
$tests = $this->tests[$section];
$id = strtolower(str_replace(' ', '_', $section));
echo "<table id=\"{$id}_results\" class=\"testResults\" width=\"100%\">";
foreach ($tests as $test => $result) {
echo "<tr class=\"$result[0]\"><td>$test</td><td>"
. nl2br(htmlentities($result[1], ENT_COMPAT, 'UTF-8')) . "</td></tr>";
}
echo "</table>";
} else {
foreach ($this->tests as $section => $tests) {
$failedRequirements = 0;
$warningRequirements = 0;
$output = "";
foreach ($tests as $test => $result) {
if (isset($result['0'])) {
switch ($result['0']) {
case 'error':
$failedRequirements++;
break;
case 'warning':
$warningRequirements++;
break;
}
}
$output .= "<tr class=\"$result[0]\"><td>$test</td><td>"
. nl2br(htmlentities($result[1], ENT_COMPAT, 'UTF-8')) . "</td></tr>";
}
$className = "good";
$text = "All Requirements Pass";
$pluralWarnings = ($warningRequirements == 1) ? 'Warning' : 'Warnings';
if ($failedRequirements > 0) {
$className = "error";
$pluralWarnings = ($warningRequirements == 1) ? 'Warning' : 'Warnings';
$text = $failedRequirements . ' Failed and ' . $warningRequirements . ' ' . $pluralWarnings;
} elseif ($warningRequirements > 0) {
$className = "warning";
$text = "All Requirements Pass but " . $warningRequirements . ' ' . $pluralWarnings;
}
echo "<h5 class='requirement $className'>$section <a href='#'>Show All Requirements</a> <span>$text</span></h5>";
echo "<table class=\"testResults\">";
echo $output;
echo "</table>";
}
}
}
public function requireFunction($funcName, $testDetails)
{
$this->testing($testDetails);
if (!function_exists($funcName)) {
$this->error($testDetails);
return false;
}
return true;
}
public function requireClass($className, $testDetails)
{
$this->testing($testDetails);
if (!class_exists($className)) {
$this->error($testDetails);
return false;
}
return true;
}
/**
* Require that the given class doesn't exist
*
* @param array $classNames
* @param array $testDetails
* @return bool
*/
public function requireNoClasses($classNames, $testDetails)
{
$this->testing($testDetails);
$badClasses = array();
foreach ($classNames as $className) {
if (class_exists($className)) {
$badClasses[] = $className;
}
}
if ($badClasses) {
$message = $testDetails[2] . ". The following classes are at fault: " . implode(', ', $badClasses);
$this->error($testDetails, $message);
return false;
}
return true;
}
public function checkApacheVersion($testDetails)
{
$this->testing($testDetails);
$is1pointx = preg_match('#Apache[/ ]1\.#', $testDetails[3]);
if ($is1pointx) {
$this->error($testDetails);
}
return true;
}
public function requirePHPVersion($recommendedVersion, $requiredVersion, $testDetails)
{
$this->testing($testDetails);
$installedVersion = phpversion();
if (version_compare($installedVersion, $requiredVersion, '<')) {
$message = "SilverStripe requires PHP version $requiredVersion or later.\n
PHP version $installedVersion is currently installed.\n
While SilverStripe requires at least PHP version $requiredVersion, upgrading to $recommendedVersion or later is recommended.\n
If you are installing SilverStripe on a shared web server, please ask your web hosting provider to upgrade PHP for you.";
$this->error($testDetails, $message);
return false;
}
if (version_compare($installedVersion, $recommendedVersion, '<')) {
$message = "PHP version $installedVersion is currently installed.\n
Upgrading to at least PHP version $recommendedVersion is recommended.\n
SilverStripe should run, but you may run into issues. Future releases may require a later version of PHP.\n";
$this->warning($testDetails, $message);
return false;
}
return true;
}
/**
* Check that a module exists
*
* @param string $dirname
* @return bool
*/
public function checkModuleExists($dirname)
{
$path = $this->getBaseDir() . $dirname;
return file_exists($path) && ($dirname == 'mysite' || file_exists($path . '/_config.php'));
}
/**
* The same as {@link requireFile()} but does additional checks
* to ensure the module directory is intact.
*
* @param string $dirname
* @param array $testDetails
*/
public function requireModule($dirname, $testDetails)
{
$this->testing($testDetails);
$path = $this->getBaseDir() . $dirname;
if (!file_exists($path)) {
$testDetails[2] .= " Directory '$path' not found. Please make sure you have uploaded the SilverStripe files to your webserver correctly.";
$this->error($testDetails);
} elseif (!file_exists($path . '/_config.php') && $dirname != 'mysite') {
$testDetails[2] .= " Directory '$path' exists, but is missing files. Please make sure you have uploaded "
. "the SilverStripe files to your webserver correctly.";
$this->error($testDetails);
}
}
public function requireFile($filename, $testDetails)
{
$this->testing($testDetails);
$filename = $this->getBaseDir() . $filename;
if (!file_exists($filename)) {
$testDetails[2] .= " (file '$filename' not found)";
$this->error($testDetails);
}
}
public function requireWriteable($filename, $testDetails, $absolute = false)
{
$this->testing($testDetails);
if ($absolute) {
$filename = str_replace('/', DIRECTORY_SEPARATOR, $filename);
} else {
$filename = $this->getBaseDir() . str_replace('/', DIRECTORY_SEPARATOR, $filename);
}
if (file_exists($filename)) {
$isWriteable = is_writeable($filename);
} else {
$isWriteable = is_writeable(dirname($filename));
}
if (!$isWriteable) {
if (function_exists('posix_getgroups')) {
$userID = posix_geteuid();
$user = posix_getpwuid($userID);
$currentOwnerID = fileowner(file_exists($filename) ? $filename : dirname($filename));
$currentOwner = posix_getpwuid($currentOwnerID);
$testDetails[2] .= "User '$user[name]' needs to be able to write to this file:\n$filename\n\nThe "
. "file is currently owned by '$currentOwner[name]'. ";
if ($user['name'] == $currentOwner['name']) {
$testDetails[2] .= "We recommend that you make the file writeable.";
} else {
$groups = posix_getgroups();
$groupList = array();
foreach ($groups as $group) {
$groupInfo = posix_getgrgid($group);
if (in_array($currentOwner['name'], $groupInfo['members'])) {
$groupList[] = $groupInfo['name'];
}
}
if ($groupList) {
$testDetails[2] .= " We recommend that you make the file group-writeable "
. "and change the group to one of these groups:\n - " . implode("\n - ", $groupList)
. "\n\nFor example:\nchmod g+w $filename\nchgrp " . $groupList[0] . " $filename";
} else {
$testDetails[2] .= " There is no user-group that contains both the web-server user and the "
. "owner of this file. Change the ownership of the file, create a new group, or "
. "temporarily make the file writeable by everyone during the install process.";
}
}
} else {
$testDetails[2] .= "The webserver user needs to be able to write to this file:\n$filename";
}
$this->error($testDetails);
}
}
public function requireTempFolder($testDetails)
{
$this->testing($testDetails);
try {
$tempFolder = TempFolder::getTempFolder(BASE_PATH);
} catch (Exception $e) {
$tempFolder = false;
}
if (!$tempFolder) {
$testDetails[2] = "Permission problem gaining access to a temp directory. " .
"Please create a folder named silverstripe-cache in the base directory " .
"of the installation and ensure it has the adequate permissions.";
$this->error($testDetails);
}
}
public function requireApacheModule($moduleName, $testDetails)
{
$this->testing($testDetails);
if (!in_array($moduleName, apache_get_modules())) {
$this->error($testDetails);
return false;
} else {
return true;
}
}
public function testApacheRewriteExists($moduleName = 'mod_rewrite')
{
if (function_exists('apache_get_modules') && in_array($moduleName, apache_get_modules())) {
return true;
}
if (isset($_SERVER['HTTP_MOD_REWRITE']) && $_SERVER['HTTP_MOD_REWRITE'] == 'On') {
return true;
}
if (isset($_SERVER['REDIRECT_HTTP_MOD_REWRITE']) && $_SERVER['REDIRECT_HTTP_MOD_REWRITE'] == 'On') {
return true;
}
return false;
}
public function testIISRewriteModuleExists($moduleName = 'IIS_UrlRewriteModule')
{
if (isset($_SERVER[$moduleName]) && $_SERVER[$moduleName]) {
return true;
} else {
return false;
}
}
public function requireApacheRewriteModule($moduleName, $testDetails)
{
$this->testing($testDetails);
if ($this->testApacheRewriteExists()) {
return true;
} else {
$this->warning($testDetails);
return false;
}
}
/**
* Determines if the web server has any rewriting capability.
* @return boolean
*/
public function hasRewritingCapability()
{
return ($this->testApacheRewriteExists() || $this->testIISRewriteModuleExists());
}
public function requireIISRewriteModule($moduleName, $testDetails)
{
$this->testing($testDetails);
if ($this->testIISRewriteModuleExists()) {
return true;
} else {
$this->warning($testDetails);
return false;
}
}
public function getDatabaseTypeNice($databaseClass)
{
return substr($databaseClass, 0, -8);
}
/**
* Get an instance of a helper class for the specific database.
*
* @param string $databaseClass e.g. MySQLDatabase or MSSQLDatabase
* @return DatabaseConfigurationHelper
*/
public function getDatabaseConfigurationHelper($databaseClass)
{
return DatabaseAdapterRegistry::getDatabaseConfigurationHelper($databaseClass);
}
public function requireDatabaseFunctions($databaseConfig, $testDetails)
{
$this->testing($testDetails);
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
if (!$helper) {
$this->error($testDetails, "Couldn't load database helper code for " . $databaseConfig['type']);
return false;
}
$result = $helper->requireDatabaseFunctions($databaseConfig);
if ($result) {
return true;
} else {
$this->error($testDetails);
return false;
}
}
public function requireDatabaseConnection($databaseConfig, $testDetails)
{
$this->testing($testDetails);
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
$result = $helper->requireDatabaseConnection($databaseConfig);
if ($result['success']) {
return true;
} else {
$testDetails[2] .= ": " . $result['error'];
$this->error($testDetails);
return false;
}
}
public function requireDatabaseVersion($databaseConfig, $testDetails)
{
$this->testing($testDetails);
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
if (method_exists($helper, 'requireDatabaseVersion')) {
$result = $helper->requireDatabaseVersion($databaseConfig);
if ($result['success']) {
return true;
} else {
$testDetails[2] .= $result['error'];
$this->warning($testDetails);
return false;
}
}
// Skipped test because this database has no required version
return true;
}
public function requireDatabaseServer($databaseConfig, $testDetails)
{
$this->testing($testDetails);
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
$result = $helper->requireDatabaseServer($databaseConfig);
if ($result['success']) {
return true;
} else {
$message = $testDetails[2] . ": " . $result['error'];
$this->error($testDetails, $message);
return false;
}
}
public function requireDatabaseOrCreatePermissions($databaseConfig, $testDetails)
{
$this->testing($testDetails);
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
$result = $helper->requireDatabaseOrCreatePermissions($databaseConfig);
if ($result['success']) {
if ($result['alreadyExists']) {
$testDetails[3] = "Database $databaseConfig[database]";
} else {
$testDetails[3] = "Able to create a new database";
}
$this->testing($testDetails);
return true;
} else {
if (empty($result['cannotCreate'])) {
$message = $testDetails[2] . ". Please create the database manually.";
} else {
$message = $testDetails[2] . " (user '$databaseConfig[username]' doesn't have CREATE DATABASE permissions.)";
}
$this->error($testDetails, $message);
return false;
}
}
public function requireDatabaseAlterPermissions($databaseConfig, $testDetails)
{
$this->testing($testDetails);
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
$result = $helper->requireDatabaseAlterPermissions($databaseConfig);
if ($result['success']) {
return true;
} else {
$message = "Silverstripe cannot alter tables. This won't prevent installation, however it may "
. "cause issues if you try to run a /dev/build once installed.";
$this->warning($testDetails, $message);
return false;
}
}
public function requireServerVariables($varNames, $testDetails)
{
$this->testing($testDetails);
$missing = array();
foreach ($varNames as $varName) {
if (!isset($_SERVER[$varName]) || !$_SERVER[$varName]) {
$missing[] = '$_SERVER[' . $varName . ']';
}
}
if (!$missing) {
return true;
}
$message = $testDetails[2] . " (the following PHP variables are missing: " . implode(", ", $missing) . ")";
$this->error($testDetails, $message);
return false;
}
public function requirePostSupport($testDetails)
{
$this->testing($testDetails);
if (!isset($_POST)) {
$this->error($testDetails);
return false;
}
return true;
}
public function isRunningWebServer($testDetails)
{
$this->testing($testDetails);
if ($testDetails[3]) {
return true;
} else {
$this->warning($testDetails);
return false;
}
}
// Must be PHP4 compatible
var $baseDir;
public function getBaseDir()
{
return BASE_PATH . '/';
}
public function testing($testDetails)
{
if (!$testDetails) {
return;
}
$section = $testDetails[0];
$test = $testDetails[1];
$message = "OK";
if (isset($testDetails[3])) {
$message .= " ($testDetails[3])";
}
$this->tests[$section][$test] = array("good", $message);
}
public function error($testDetails, $message = null)
{
if (!is_array($testDetails)) {
throw new InvalidArgumentException("Invalid error");
}
$section = $testDetails[0];
$test = $testDetails[1];
if (!$message && isset($testDetails[2])) {
$message = $testDetails[2];
}
$this->tests[$section][$test] = array("error", $message);
$this->errors[] = $testDetails;
}
public function warning($testDetails, $message = null)
{
if (!is_array($testDetails)) {
throw new InvalidArgumentException("Invalid warning");
}
$section = $testDetails[0];
$test = $testDetails[1];
if (!$message && isset($testDetails[2])) {
$message = $testDetails[2];
}
$this->tests[$section][$test] = array("warning", $message);
$this->warnings[] = $testDetails;
}
public function hasErrors()
{
return sizeof($this->errors);
}
public function hasWarnings()
{
return sizeof($this->warnings);
}
}

View File

@ -0,0 +1,491 @@
<?php
namespace SilverStripe\Dev\Install;
use Exception;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Core\CoreKernel;
use SilverStripe\Control\HTTPApplication;
use SilverStripe\Core\Kernel;
use SilverStripe\Core\Startup\ParameterConfirmationToken;
use SilverStripe\ORM\DatabaseAdmin;
use SilverStripe\Security\DefaultAdminService;
use SilverStripe\Security\Security;
/**
* SilverStripe CMS SilverStripe\Dev\Install\Installer
* This installer doesn't use any of the fancy SilverStripe stuff in case it's unsupported.
*/
class Installer extends InstallRequirements
{
public function __construct()
{
// Cache the baseDir value
$this->getBaseDir();
}
protected function installHeader()
{
?>
<html>
<head>
<meta charset="utf-8"/>
<title>Installing SilverStripe...</title>
<link rel="stylesheet" type="text/css"
href="framework/src/Dev/Install/client/styles/install.css"/>
<script src="//code.jquery.com/jquery-1.7.2.min.js"></script>
</head>
<body>
<div class="install-header">
<div class="inner">
<div class="brand">
<span class="logo"></span>
<h1>SilverStripe</h1>
</div>
</div>
</div>
<div id="Navigation">&nbsp;</div>
<div class="clear"><!-- --></div>
<div class="main">
<div class="inner">
<h2>Installing SilverStripe...</h2>
<p>I am now running through the installation steps (this should take about 30 seconds)</p>
<p>If you receive a fatal error, refresh this page to continue the installation</p>
<ul>
<?php
}
public function install($config)
{
// Render header
$this->installHeader();
$webserver = $this->findWebserver();
$isIIS = $this->isIIS();
$isApache = $this->isApache();
flush();
if (isset($config['stats'])) {
if (file_exists(FRAMEWORK_PATH . '/silverstripe_version')) {
$silverstripe_version = file_get_contents(FRAMEWORK_PATH . '/silverstripe_version');
} else {
$silverstripe_version = "unknown";
}
$phpVersion = urlencode(phpversion());
$encWebserver = urlencode($webserver);
$dbType = $config['db']['type'];
// Try to determine the database version from the helper
$databaseVersion = $config['db']['type'];
$helper = $this->getDatabaseConfigurationHelper($dbType);
if ($helper && method_exists($helper, 'getDatabaseVersion')) {
$versionConfig = $config['db'][$dbType];
$versionConfig['type'] = $dbType;
$databaseVersion = urlencode($dbType . ': ' . $helper->getDatabaseVersion($versionConfig));
}
$url = "http://ss2stat.silverstripe.com/Installation/add?SilverStripe=$silverstripe_version&PHP=$phpVersion&Database=$databaseVersion&WebServer=$encWebserver";
if (isset($_SESSION['StatsID']) && $_SESSION['StatsID']) {
$url .= '&ID=' . $_SESSION['StatsID'];
}
@$_SESSION['StatsID'] = file_get_contents($url);
}
if (file_exists('mysite/_config.php')) {
// Truncate the contents of _config instead of deleting it - we can't re-create it because Windows handles permissions slightly
// differently to UNIX based filesystems - it takes the permissions from the parent directory instead of retaining them
$fh = fopen('mysite/_config.php', 'wb');
fclose($fh);
}
// Escape user input for safe insertion into PHP file
$theme = isset($_POST['template']) ? addcslashes($_POST['template'], "\'") : 'simple';
$locale = isset($_POST['locale']) ? addcslashes($_POST['locale'], "\'") : 'en_US';
$type = addcslashes($config['db']['type'], "\'");
$dbConfig = $config['db'][$type];
foreach ($dbConfig as &$configValue) {
$configValue = addcslashes($configValue, "\\\'");
}
if (!isset($dbConfig['path'])) {
$dbConfig['path'] = '';
}
if (!$dbConfig) {
echo "<p style=\"color: red\">Bad config submitted</p><pre>";
print_r($config);
echo "</pre>";
die();
}
// Write the config file
global $usingEnv;
if ($usingEnv) {
$this->statusMessage("Setting up 'mysite/_config.php' for use with environment variables...");
$this->writeToFile("mysite/_config.php", "<?php\n ");
} else {
$this->statusMessage("Setting up 'mysite/_config.php'...");
// Create databaseConfig
$lines = array(
$lines[] = " 'type' => '$type'"
);
foreach ($dbConfig as $key => $value) {
$lines[] = " '{$key}' => '$value'";
}
$databaseConfigContent = implode(",\n", $lines);
$this->writeToFile("mysite/_config.php", <<<PHP
<?php
use SilverStripe\\ORM\\DB;
DB::setConfig([
{$databaseConfigContent}
]);
PHP
);
}
$this->statusMessage("Setting up 'mysite/_config/config.yml'");
$this->writeToFile("mysite/_config/config.yml", <<<YML
---
Name: mysite
---
# YAML configuration for SilverStripe
# See http://doc.silverstripe.org/framework/en/topics/configuration
# Caution: Indentation through two spaces, not tabs
SilverStripe\\View\\SSViewer:
themes:
- '$theme'
- '\$default'
SilverStripe\\i18n\\i18n:
default_locale: '$locale'
YML
);
if (!$this->checkModuleExists('cms')) {
$this->writeToFile("mysite/code/RootURLController.php", <<<PHP
<?php
use SilverStripe\\Control\\Controller;
class RootURLController extends Controller {
public function index() {
echo "<html>Your site is now set up. Start adding controllers to mysite to get started.</html>";
}
}
PHP
);
}
// Write the appropriate web server configuration file for rewriting support
if ($this->hasRewritingCapability()) {
if ($isApache) {
$this->statusMessage("Setting up '.htaccess' file...");
$this->createHtaccess();
} elseif ($isIIS) {
$this->statusMessage("Setting up 'web.config' file...");
$this->createWebConfig();
}
}
// Mock request
$session = new Session(isset($_SESSION) ? $_SESSION : array());
$request = new HTTPRequest('GET', '/');
$request->setSession($session);
// Install kernel (fix to dev)
$kernel = new CoreKernel(BASE_PATH);
$kernel->setEnvironment(Kernel::DEV);
$app = new HTTPApplication($kernel);
// Build db within HTTPApplication
$app->execute($request, function (HTTPRequest $request) use ($config) {
// Start session and execute
$request->getSession()->init();
// Output status
$this->statusMessage("Building database schema...");
// Setup DB
$dbAdmin = new DatabaseAdmin();
$dbAdmin->setRequest($request);
$dbAdmin->pushCurrent();
$dbAdmin->doInit();
$dbAdmin->doBuild(true);
// Create default administrator user and group in database
// (not using Security::setDefaultAdmin())
$adminMember = DefaultAdminService::singleton()->findOrCreateDefaultAdmin();
$adminMember->Email = $config['admin']['username'];
$adminMember->Password = $config['admin']['password'];
$adminMember->PasswordEncryption = Security::config()->get('encryption_algorithm');
try {
$this->statusMessage('Creating default CMS admin account...');
$adminMember->write();
} catch (Exception $e) {
$this->statusMessage(
sprintf('Warning: Default CMS admin account could not be created (error: %s)', $e->getMessage())
);
}
$request->getSession()->set('username', $config['admin']['username']);
$request->getSession()->set('password', $config['admin']['password']);
$request->getSession()->save();
}, true);
// Check result of install
if (!$this->errors) {
if (isset($_SERVER['HTTP_HOST']) && $this->hasRewritingCapability()) {
$this->statusMessage("Checking that friendly URLs work...");
$this->checkRewrite();
} else {
$token = new ParameterConfirmationToken('flush', $request);
$params = http_build_query($token->params());
$destinationURL = 'index.php/' .
($this->checkModuleExists('cms') ? "home/successfullyinstalled?$params" : "?$params");
echo <<<HTML
<li>SilverStripe successfully installed; I am now redirecting you to your SilverStripe site...</li>
<script>
setTimeout(function() {
window.location = "$destinationURL";
}, 2000);
</script>
<noscript>
<li><a href="$destinationURL">Click here to access your site.</a></li>
</noscript>
HTML;
}
}
return $this->errors;
}
public function writeToFile($filename, $content)
{
$base = $this->getBaseDir();
$this->statusMessage("Setting up $base$filename");
if ((@$fh = fopen($base . $filename, 'wb')) && fwrite($fh, $content) && fclose($fh)) {
return true;
}
$this->error("Couldn't write to file $base$filename");
return false;
}
public function createHtaccess()
{
$start = "### SILVERSTRIPE START ###\n";
$end = "\n### SILVERSTRIPE END ###";
$base = dirname($_SERVER['SCRIPT_NAME']);
if (defined('DIRECTORY_SEPARATOR')) {
$base = str_replace(DIRECTORY_SEPARATOR, '/', $base);
} else {
$base = str_replace("\\", '/', $base);
}
if ($base != '.') {
$baseClause = "RewriteBase '$base'\n";
} else {
$baseClause = "";
}
if (strpos(strtolower(php_sapi_name()), "cgi") !== false) {
$cgiClause = "RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]\n";
} else {
$cgiClause = "";
}
$rewrite = <<<TEXT
# Deny access to templates (but allow from localhost)
<Files *.ss>
Order deny,allow
Deny from all
Allow from 127.0.0.1
</Files>
# Deny access to IIS configuration
<Files web.config>
Order deny,allow
Deny from all
</Files>
# Deny access to YAML configuration files which might include sensitive information
<Files *.yml>
Order allow,deny
Deny from all
</Files>
# Route errors to static pages automatically generated by SilverStripe
ErrorDocument 404 /assets/error-404.html
ErrorDocument 500 /assets/error-500.html
<IfModule mod_rewrite.c>
# Turn off index.php handling requests to the homepage fixes issue in apache >=2.4
<IfModule mod_dir.c>
DirectoryIndex disabled
</IfModule>
SetEnv HTTP_MOD_REWRITE On
RewriteEngine On
$baseClause
$cgiClause
# Deny access to potentially sensitive files and folders
RewriteRule ^vendor(/|$) - [F,L,NC]
RewriteRule silverstripe-cache(/|$) - [F,L,NC]
RewriteRule composer\.(json|lock) - [F,L,NC]
# Process through SilverStripe if no file with the requested name exists.
# Pass through the original path as a query parameter, and retain the existing parameters.
RewriteCond %{REQUEST_URI} ^(.*)$
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule .* framework/main.php?url=%1 [QSA]
</IfModule>
TEXT;
if (file_exists('.htaccess')) {
$htaccess = file_get_contents('.htaccess');
if (strpos($htaccess, '### SILVERSTRIPE START ###') === false
&& strpos($htaccess, '### SILVERSTRIPE END ###') === false
) {
$htaccess .= "\n### SILVERSTRIPE START ###\n### SILVERSTRIPE END ###\n";
}
if (strpos($htaccess, '### SILVERSTRIPE START ###') !== false
&& strpos($htaccess, '### SILVERSTRIPE END ###') !== false
) {
$start = substr($htaccess, 0, strpos($htaccess, '### SILVERSTRIPE START ###'))
. "### SILVERSTRIPE START ###\n";
$end = "\n" . substr($htaccess, strpos($htaccess, '### SILVERSTRIPE END ###'));
}
}
$this->writeToFile('.htaccess', $start . $rewrite . $end);
}
/**
* Writes basic configuration to the web.config for IIS
* so that rewriting capability can be use.
*/
public function createWebConfig()
{
$content = <<<TEXT
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<security>
<requestFiltering>
<hiddenSegments applyToWebDAV="false">
<add segment="silverstripe-cache" />
<add segment="vendor" />
<add segment="composer.json" />
<add segment="composer.lock" />
</hiddenSegments>
<fileExtensions allowUnlisted="true" >
<add fileExtension=".ss" allowed="false"/>
<add fileExtension=".yml" allowed="false"/>
</fileExtensions>
</requestFiltering>
</security>
<rewrite>
<rules>
<rule name="SilverStripe Clean URLs" stopProcessing="true">
<match url="^(.*)$" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
</conditions>
<action type="Rewrite" url="framework/main.php?url={R:1}" appendQueryString="true" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>
TEXT;
$this->writeToFile('web.config', $content);
}
public function checkRewrite()
{
$token = new ParameterConfirmationToken('flush', new HTTPRequest('GET', '/'));
$params = http_build_query($token->params());
$destinationURL = str_replace('install.php', '', $_SERVER['SCRIPT_NAME']) .
($this->checkModuleExists('cms') ? "home/successfullyinstalled?$params" : "?$params");
echo <<<HTML
<li id="ModRewriteResult">Testing...</li>
<script>
if (typeof $ == 'undefined') {
document.getElemenyById('ModeRewriteResult').innerHTML = "I can't run jQuery ajax to set rewriting; I will redirect you to the homepage to see if everything is working.";
setTimeout(function() {
window.location = "$destinationURL";
}, 10000);
} else {
$.ajax({
method: 'get',
url: 'InstallerTest/testrewrite',
complete: function(response) {
var r = response.responseText.replace(/[^A-Z]?/g,"");
if (r === "OK") {
$('#ModRewriteResult').html("Friendly URLs set up successfully; I am now redirecting you to your SilverStripe site...")
setTimeout(function() {
window.location = "$destinationURL";
}, 2000);
} else {
$('#ModRewriteResult').html("Friendly URLs are not working. This is most likely because a rewrite module isn't configured "
+ "correctly on your site. You may need to get your web host or server administrator to do this for you: "
+ "<ul>"
+ "<li><strong>mod_rewrite</strong> or other rewrite module is enabled on your web server</li>"
+ "<li><strong>AllowOverride All</strong> is set for the directory where SilverStripe is installed</li>"
+ "</ul>");
}
}
});
}
</script>
<noscript>
<li><a href="$destinationURL">Click here</a> to check friendly URLs are working. If you get a 404 then something is wrong.</li>
</noscript>
HTML;
}
public function var_export_array_nokeys($array)
{
$retval = "array(\n";
foreach ($array as $item) {
$retval .= "\t'";
$retval .= trim($item);
$retval .= "',\n";
}
$retval .= ")";
return $retval;
}
/**
* Show an installation status message.
* The output differs depending on whether this is CLI or web based
*
* @param string $msg
*/
public function statusMessage($msg)
{
echo "<li>$msg</li>\n";
flush();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -6,7 +6,7 @@
<head> <head>
<title>SilverStripe CMS / Framework Installation</title> <title>SilverStripe CMS / Framework Installation</title>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"> <meta http-equiv="Content-type" content="text/html; charset=utf-8">
<script type="application/javascript" src="http://code.jquery.com/jquery-1.7.2.min.js"></script> <script type="application/javascript" src="//code.jquery.com/jquery-1.7.2.min.js"></script>
<script type="application/javascript" src="<?=FRAMEWORK_NAME; ?>/src/Dev/Install/client/js/install.js"></script> <script type="application/javascript" src="<?=FRAMEWORK_NAME; ?>/src/Dev/Install/client/js/install.js"></script>
<link rel="stylesheet" type="text/css" href="<?=FRAMEWORK_NAME; ?>/src/Dev/Install/client/styles/install.css"> <link rel="stylesheet" type="text/css" href="<?=FRAMEWORK_NAME; ?>/src/Dev/Install/client/styles/install.css">
<link rel="shortcut icon" href="favicon.ico"> <link rel="shortcut icon" href="favicon.ico">
@ -29,13 +29,13 @@
<div id="Layout"> <div id="Layout">
<div class="typography"> <div class="typography">
<form action="install.php" method="post"> <form action="install.php" method="post">
<?php if(isset($hasErrorOtherThanDatabase)): ?> <?php if($hasErrorOtherThanDatabase): ?>
<p class="message error"> <p class="message error">
You aren't currently able to install the software. Please <a href="#requirements">see below</a> for details.<br> You aren't currently able to install the software. Please <a href="#requirements">see below</a> for details.<br>
If you are having problems meeting the requirements, see the <a href="http://doc.silverstripe.org/framework/en/installation/server-requirements" target="_blank">server requirements</a>. If you are having problems meeting the requirements, see the <a href="http://doc.silverstripe.org/framework/en/installation/server-requirements" target="_blank">server requirements</a>.
</p> </p>
<?php if (isset($phpIniLocation)): ?> <?php if ($phpIniLocation): ?>
<p>Your php.ini file is located at <?=$phpIniLocation; ?></p> <p class="message warning">Your php.ini file is located at <?=$phpIniLocation; ?></p>
<?php endif; ?> <?php endif; ?>
<?php else: ?> <?php else: ?>
<?php if($alreadyInstalled): ?> <?php if($alreadyInstalled): ?>
@ -252,7 +252,7 @@
</ul> </ul>
<h3 class="sectionHeading" id="install">Confirm Install <small>Step 5 of 5</small></h3> <h3 class="sectionHeading" id="install">Confirm Install <small>Step 5 of 5</small></h3>
<?php if(isset($hasErrorOtherThanDatabase)): ?> <?php if($hasErrorOtherThanDatabase): ?>
<p class="error"> <p class="error">
You aren't currently able to install the software. Please <a href="#requirements">see above</a> for details.<br> You aren't currently able to install the software. Please <a href="#requirements">see above</a> for details.<br>
If you are having problems meeting the requirements, see the <a href="http://doc.silverstripe.org/doku.php?id=server-requirements">server requirements page</a>. If you are having problems meeting the requirements, see the <a href="http://doc.silverstripe.org/doku.php?id=server-requirements">server requirements page</a>.

View File

@ -24,4 +24,4 @@ if (version_compare(phpversion(), '5.5.0', '<')) {
die(); die();
} }
include(__DIR__ . '/install.php5'); include(__DIR__ . '/install5.php');

View File

@ -1,1762 +0,0 @@
<?php
/************************************************************************************
************************************************************************************
** **
** If you can read this text in your browser then you don't have PHP installed. **
** Please install PHP 5.5.0 or higher. **
** **
************************************************************************************
************************************************************************************/
use SilverStripe\Control\Controller;
use SilverStripe\Core\Startup\ParameterConfirmationToken;
use SilverStripe\Dev\Install\DatabaseAdapterRegistry;
use SilverStripe\Dev\Install\DatabaseConfigurationHelper;
use SilverStripe\ORM\DatabaseAdmin;
use SilverStripe\ORM\DB;
use SilverStripe\Security\Security;
use SilverStripe\Security\DefaultAdminService;
/**
* SilverStripe CMS Installer
* This installer doesn't use any of the fancy SilverStripe stuff in case it's unsupported.
*/
// speed up mysql_connect timeout if the server can't be found
ini_set('mysql.connect_timeout', 5);
// Don't die half was through installation; that does more harm than good
ini_set('max_execution_time', 0);
// set display_errors php setting to on to force installer to avoid blank screen of death.
// get the original value so it can be used in PHP requirement checks later in this script.
$originalDisplayErrorsValue = ini_get('display_errors');
ini_set('display_errors', '1');
error_reporting(E_ALL | E_STRICT);
// Attempt to start a session so that the username and password can be sent back to the user.
if(function_exists('session_start') && !session_id()) {
session_start();
}
// require composers autoloader
$autoloadPaths = [
__DIR__ . '/../../../vendor/autoload.php', // framework/vendor
__DIR__ . '/../../../../vendor/autoload.php', // root vendor
];
$included = false;
foreach($autoloadPaths as $path) {
if (file_exists($path)) {
$included = true;
require_once $path;
break;
}
}
if (!$included) {
if (!headers_sent()) {
header($_SERVER['SERVER_PROTOCOL'] . " 500 Server Error");
header('Content-Type: text/plain');
}
echo "Failed to include composer's autoloader, unable to continue\n";
exit(1);
}
$usingEnv = !empty($_REQUEST['useEnv']);
require_once __DIR__ . '/DatabaseConfigurationHelper.php';
require_once __DIR__ . '/DatabaseAdapterRegistry.php';
// Set default locale, but try and sniff from the user agent
$defaultLocale = 'en_US';
$locales = array(
'af_ZA' => 'Afrikaans (South Africa)',
'ar_EG' => 'Arabic (Egypt)',
'hy_AM' => 'Armenian (Armenia)',
'ast_ES' => 'Asturian (Spain)',
'az_AZ' => 'Azerbaijani (Azerbaijan)',
'bs_BA' => 'Bosnian (Bosnia and Herzegovina)',
'bg_BG' => 'Bulgarian (Bulgaria)',
'ca_ES' => 'Catalan (Spain)',
'zh_CN' => 'Chinese (China)',
'zh_TW' => 'Chinese (Taiwan)',
'hr_HR' => 'Croatian (Croatia)',
'cs_CZ' => 'Czech (Czech Republic)',
'da_DK' => 'Danish (Denmark)',
'nl_NL' => 'Dutch (Netherlands)',
'en_GB' => 'English (United Kingdom)',
'en_US' => 'English (United States)',
'eo_XX' => 'Esperanto',
'et_EE' => 'Estonian (Estonia)',
'fo_FO' => 'Faroese (Faroe Islands)',
'fi_FI' => 'Finnish (Finland)',
'fr_FR' => 'French (France)',
'de_DE' => 'German (Germany)',
'el_GR' => 'Greek (Greece)',
'he_IL' => 'Hebrew (Israel)',
'hu_HU' => 'Hungarian (Hungary)',
'is_IS' => 'Icelandic (Iceland)',
'id_ID' => 'Indonesian (Indonesia)',
'it_IT' => 'Italian (Italy)',
'ja_JP' => 'Japanese (Japan)',
'km_KH' => 'Khmer (Cambodia)',
'lc_XX' => 'LOLCAT',
'lv_LV' => 'Latvian (Latvia)',
'lt_LT' => 'Lithuanian (Lithuania)',
'ms_MY' => 'Malay (Malaysia)',
'mi_NZ' => 'Maori (New Zealand)',
'ne_NP' => 'Nepali (Nepal)',
'nb_NO' => 'Norwegian',
'fa_IR' => 'Persian (Iran)',
'pl_PL' => 'Polish (Poland)',
'pt_BR' => 'Portuguese (Brazil)',
'pa_IN' => 'Punjabi (India)',
'ro_RO' => 'Romanian (Romania)',
'ru_RU' => 'Russian (Russia)',
'sr_RS' => 'Serbian (Serbia)',
'si_LK' => 'Sinhalese (Sri Lanka)',
'sk_SK' => 'Slovak (Slovakia)',
'sl_SI' => 'Slovenian (Slovenia)',
'es_AR' => 'Spanish (Argentina)',
'es_MX' => 'Spanish (Mexico)',
'es_ES' => 'Spanish (Spain)',
'sv_SE' => 'Swedish (Sweden)',
'th_TH' => 'Thai (Thailand)',
'tr_TR' => 'Turkish (Turkey)',
'uk_UA' => 'Ukrainian (Ukraine)',
'uz_UZ' => 'Uzbek (Uzbekistan)',
'vi_VN' => 'Vietnamese (Vietnam)',
);
// Discover which databases are available
DatabaseAdapterRegistry::autodiscover();
// Determine which external database modules are USABLE
$databaseClasses = DatabaseAdapterRegistry::get_adapters();
foreach($databaseClasses as $class => $details) {
$helper = DatabaseAdapterRegistry::getDatabaseConfigurationHelper($class);
$databaseClasses[$class]['hasModule'] = !empty($helper);
}
// Load database config
if(isset($_REQUEST['db'])) {
if(isset($_REQUEST['db']['type'])) {
$type = $_REQUEST['db']['type'];
} else {
if ($type = getenv('SS_DATABASE_CLASS')) {
$_REQUEST['db']['type'] = $type;
} elseif( $databaseClasses['MySQLPDODatabase']['supported'] ) {
$type = $_REQUEST['db']['type'] = 'MySQLPDODatabase';
} elseif( $databaseClasses['MySQLDatabase']['supported'] ) {
$type = $_REQUEST['db']['type'] = 'MySQLDatabase';
} else {
// handle error
}
}
// Disabled inputs don't submit anything - we need to use the environment (except the database name)
if($usingEnv) {
$_REQUEST['db'][$type] = $databaseConfig = array(
"type" => getenv('SS_DATABASE_CLASS') ?: $type,
"server" => getenv('SS_DATABASE_SERVER') ?: "localhost",
"username" => getenv('SS_DATABASE_USERNAME') ?: "root",
"password" => getenv('SS_DATABASE_PASSWORD') ?: "",
"database" => $_REQUEST['db'][$type]['database'],
);
} else {
// Normal behaviour without the environment
$databaseConfig = $_REQUEST['db'][$type];
$databaseConfig['type'] = $type;
}
} else {
if($type = getenv('SS_DATABASE_CLASS')) {
$_REQUEST['db']['type'] = $type;
} elseif( $databaseClasses['MySQLPDODatabase']['supported'] ) {
$type = $_REQUEST['db']['type'] = 'MySQLPDODatabase';
} elseif( $databaseClasses['MySQLDatabase']['supported'] ) {
$type = $_REQUEST['db']['type'] = 'MySQLDatabase';
} else {
// handle error
}
$_REQUEST['db'][$type] = $databaseConfig = array(
"type" => $type,
"server" => getenv('SS_DATABASE_SERVER') ?: "localhost",
"username" => getenv('SS_DATABASE_USERNAME') ?: "root",
"password" => getenv('SS_DATABASE_PASSWORD') ?: "",
"database" => isset($_SERVER['argv'][2]) ? $_SERVER['argv'][2] : "SS_mysite",
);
}
if(isset($_REQUEST['admin'])) {
// Disabled inputs don't submit anything - we need to use the environment (except the database name)
if($usingEnv) {
$_REQUEST['admin'] = $adminConfig = array(
'username' => getenv('SS_DEFAULT_ADMIN_USERNAME') ?: 'admin',
'password' => getenv('SS_DEFAULT_ADMIN_PASSWORD') ?: '',
);
} else {
$adminConfig = $_REQUEST['admin'];
}
} else {
$_REQUEST['admin'] = $adminConfig = array(
'username' => getenv('SS_DEFAULT_ADMIN_USERNAME') ?: 'admin',
'password' => getenv('SS_DEFAULT_ADMIN_PASSWORD') ?: '',
);
}
$alreadyInstalled = false;
if(file_exists('mysite/_config.php')) {
// Find the $database variable in the relevant config file without having to execute the config file
if(preg_match("/\\\$database\s*=\s*[^\n\r]+[\n\r]/", file_get_contents("mysite/_config.php"), $parts)) {
eval($parts[0]);
if(!empty($database)) {
$alreadyInstalled = true;
}
// Assume that if $databaseConfig is defined in mysite/_config.php, then a non-environment-based installation has
// already gone ahead
} else if(preg_match("/\\\$databaseConfig\s*=\s*[^\n\r]+[\n\r]/", file_get_contents("mysite/_config.php"), $parts)) {
$alreadyInstalled = true;
}
}
if(file_exists(FRAMEWORK_NAME . '/silverstripe_version')) {
$silverstripe_version = file_get_contents(FRAMEWORK_NAME . '/silverstripe_version');
} else {
$silverstripe_version = "unknown";
}
// Check requirements
$req = new InstallRequirements();
$req->check();
$webserverConfigFile = '';
if($req->isIIS()) {
$webserverConfigFile = 'web.config';
} else {
$webserverConfigFile = '.htaccess';
}
if($req->hasErrors()) {
$hasErrorOtherThanDatabase = true;
$phpIniLocation = php_ini_loaded_file();
}
if($databaseConfig) {
$dbReq = new InstallRequirements();
$dbReq->checkDatabase($databaseConfig);
}
if($adminConfig) {
$adminReq = new InstallRequirements();
$adminReq->checkAdminConfig($adminConfig);
}
// Actual processor
$installFromCli = (isset($_SERVER['argv'][1]) && $_SERVER['argv'][1] == 'install');
// CLI-install error message. exit(1) will halt any makefile.
if($installFromCli && ($req->hasErrors() || $dbReq->hasErrors())) {
echo "Cannot install due to errors:\n";
$req->listErrors();
$dbReq->listErrors();
exit(1);
}
if((isset($_REQUEST['go']) || $installFromCli) && !$req->hasErrors() && !$dbReq->hasErrors() && $adminConfig['username'] && $adminConfig['password']) {
// Confirm before reinstalling
if(!$installFromCli && $alreadyInstalled) {
include(__DIR__ . '/config-form.html');
} else {
$inst = new Installer();
if($_REQUEST) $inst->install($_REQUEST);
else $inst->install(array(
'db' => $databaseConfig,
'admin' => $adminConfig,
));
}
// Show the config form
} else {
include(__DIR__ . '/config-form.html');
}
/**
* This class checks requirements
* Each of the requireXXX functions takes an argument which gives a user description of the test.
* It's an array of 3 parts:
* $description[0] - The test catetgory
* $description[1] - The test title
* $description[2] - The test error to show, if it goes wrong
*/
class InstallRequirements {
var $errors, $warnings, $tests;
/**
* Check the database configuration. These are done one after another
* starting with checking the database function exists in PHP, and
* continuing onto more difficult checks like database permissions.
*
* @param array $databaseConfig The list of database parameters
* @return boolean Validity of database configuration details
*/
public function checkDatabase($databaseConfig) {
// Check if support is available
if(!$this->requireDatabaseFunctions(
$databaseConfig,
array(
"Database Configuration",
"Database support",
"Database support in PHP",
$this->getDatabaseTypeNice($databaseConfig['type'])
)
)) return false;
// Check if the server is available
$usePath = !empty($databaseConfig['path']) && empty($databaseConfig['server']);
if(!$this->requireDatabaseServer(
$databaseConfig,
array(
"Database Configuration",
"Database server",
$usePath
? "I couldn't write to path '$databaseConfig[path]'"
: "I couldn't find a database server on '$databaseConfig[server]'",
$usePath ? $databaseConfig['path'] : $databaseConfig['server']
)
)) return false;
// Check if the connection credentials allow access to the server / database
if(!$this->requireDatabaseConnection(
$databaseConfig,
array(
"Database Configuration",
"Database access credentials",
"That username/password doesn't work"
)
)) return false;
// Check the necessary server version is available
if(!$this->requireDatabaseVersion(
$databaseConfig,
array(
"Database Configuration",
"Database server version requirement",
'',
'Version ' . $this->getDatabaseConfigurationHelper($databaseConfig['type'])->getDatabaseVersion($databaseConfig)
)
)) return false;
// Check that database creation permissions are available
if(!$this->requireDatabaseOrCreatePermissions(
$databaseConfig,
array(
"Database Configuration",
"Can I access/create the database",
"I can't create new databases and the database '$databaseConfig[database]' doesn't exist"
)
)) return false;
// Check alter permission (necessary to create tables etc)
if(!$this->requireDatabaseAlterPermissions(
$databaseConfig,
array(
"Database Configuration",
"Can I ALTER tables",
"I don't have permission to ALTER tables"
)
)) return false;
// Success!
return true;
}
public function checkAdminConfig($adminConfig) {
if(!$adminConfig['username']) {
$this->error(array('', 'Please enter a username!'));
}
if(!$adminConfig['password']) {
$this->error(array('', 'Please enter a password!'));
}
}
/**
* Check if the web server is IIS and version greater than the given version.
*
* @param int $fromVersion
* @return bool
*/
public function isIIS($fromVersion = 7) {
if(strpos($this->findWebserver(), 'IIS/') === false) {
return false;
}
return substr(strstr($this->findWebserver(), '/'), -3, 1) >= $fromVersion;
}
public function isApache() {
if(strpos($this->findWebserver(), 'Apache') !== false) {
return true;
} else {
return false;
}
}
/**
* Find the webserver software running on the PHP host.
* @return string|boolean Server software or boolean FALSE
*/
public function findWebserver() {
// Try finding from SERVER_SIGNATURE or SERVER_SOFTWARE
if(!empty($_SERVER['SERVER_SIGNATURE'])) {
$webserver = $_SERVER['SERVER_SIGNATURE'];
} elseif(!empty($_SERVER['SERVER_SOFTWARE'])) {
$webserver = $_SERVER['SERVER_SOFTWARE'];
} else {
return false;
}
return strip_tags(trim($webserver));
}
/**
* Check everything except the database
*/
public function check() {
$this->errors = null;
$isApache = $this->isApache();
$isIIS = $this->isIIS();
$webserver = $this->findWebserver();
$this->requirePHPVersion('5.5.0', '5.5.0', array(
"PHP Configuration",
"PHP5 installed",
null,
"PHP version " . phpversion()
));
// Check that we can identify the root folder successfully
$this->requireFile(FRAMEWORK_NAME . '/src/Dev/Install/config-form.html', array("File permissions",
"Does the webserver know where files are stored?",
"The webserver isn't letting me identify where files are stored.",
$this->getBaseDir()
));
$this->requireModule('mysite', array("File permissions", "mysite/ directory exists?"));
$this->requireModule(FRAMEWORK_NAME, array("File permissions", FRAMEWORK_NAME . "/ directory exists?"));
if($isApache) {
$this->checkApacheVersion(array(
"Webserver Configuration",
"Webserver is not Apache 1.x", "SilverStripe requires Apache version 2 or greater",
$webserver
));
$this->requireWriteable('.htaccess', array("File permissions", "Is the .htaccess file writeable?", null));
} elseif($isIIS) {
$this->requireWriteable('web.config', array("File permissions", "Is the web.config file writeable?", null));
}
$this->requireWriteable('mysite/_config.php', array(
"File permissions",
"Is the mysite/_config.php file writeable?",
null
));
$this->requireWriteable('mysite/_config/config.yml', array(
"File permissions",
"Is the mysite/_config/config.yml file writeable?",
null
));
if(!$this->checkModuleExists('cms')) {
$this->requireWriteable('mysite/code/RootURLController.php', array(
"File permissions",
"Is the mysite/code/RootURLController.php file writeable?",
null
));
}
$this->requireWriteable('assets', array("File permissions", "Is the assets/ directory writeable?", null));
try {
$tempFolder = getTempFolder();
} catch(Exception $e) {
$tempFolder = false;
}
$this->requireTempFolder(array('File permissions', 'Is a temporary directory available?', null, $tempFolder));
if($tempFolder) {
// in addition to the temp folder being available, check it is writable
$this->requireWriteable($tempFolder, array(
"File permissions",
sprintf("Is the temporary directory writeable?", $tempFolder),
null
), true);
}
// Check for web server, unless we're calling the installer from the command-line
$this->isRunningWebServer(array("Webserver Configuration", "Server software", "Unknown", $webserver));
if($isApache) {
$this->requireApacheRewriteModule('mod_rewrite', array(
"Webserver Configuration",
"URL rewriting support",
"You need mod_rewrite to use friendly URLs with SilverStripe, but it is not enabled."
));
} elseif($isIIS) {
$this->requireIISRewriteModule('IIS_UrlRewriteModule', array(
"Webserver Configuration",
"URL rewriting support",
"You need to enable the IIS URL Rewrite Module to use friendly URLs with SilverStripe, "
. "but it is not installed or enabled. Download it for IIS 7 from http://www.iis.net/expand/URLRewrite"
));
} else {
$this->warning(array(
"Webserver Configuration",
"URL rewriting support",
"I can't tell whether any rewriting module is running. You may need to configure a rewriting rule yourself."));
}
$this->requireServerVariables(array('SCRIPT_NAME', 'HTTP_HOST', 'SCRIPT_FILENAME'), array(
"Webserver Configuration",
"Recognised webserver",
"You seem to be using an unsupported webserver. "
. "The server variables SCRIPT_NAME, HTTP_HOST, SCRIPT_FILENAME need to be set."
));
$this->requirePostSupport(array(
"Webserver Configuration",
"POST Support",
'I can\'t find $_POST, make sure POST is enabled.'
));
// Check for GD support
if(!$this->requireFunction("imagecreatetruecolor", array(
"PHP Configuration",
"GD2 support",
"PHP must have GD version 2."
))) {
$this->requireFunction("imagecreate", array(
"PHP Configuration",
"GD2 support",
"GD support for PHP not included."
));
}
// Check for XML support
$this->requireFunction('xml_set_object', array(
"PHP Configuration",
"XML support",
"XML support not included in PHP."
));
$this->requireClass('DOMDocument', array(
"PHP Configuration",
"DOM/XML support",
"DOM/XML support not included in PHP."
));
$this->requireFunction('simplexml_load_file', array(
'PHP Configuration',
'SimpleXML support',
'SimpleXML support not included in PHP.'
));
// Check for token_get_all
$this->requireFunction('token_get_all', array(
"PHP Configuration",
"Tokenizer support",
"Tokenizer support not included in PHP."
));
// Check for CType support
$this->requireFunction('ctype_digit', array(
'PHP Configuration',
'CType support',
'CType support not included in PHP.'
));
// Check for session support
$this->requireFunction('session_start', array(
'PHP Configuration',
'Session support',
'Session support not included in PHP.'
));
// Check for iconv support
$this->requireFunction('iconv', array(
'PHP Configuration',
'iconv support',
'iconv support not included in PHP.'
));
// Check for hash support
$this->requireFunction('hash', array('PHP Configuration', 'hash support', 'hash support not included in PHP.'));
// Check for mbstring support
$this->requireFunction('mb_internal_encoding', array(
'PHP Configuration',
'mbstring support',
'mbstring support not included in PHP.'
));
// Check for Reflection support
$this->requireClass('ReflectionClass', array(
'PHP Configuration',
'Reflection support',
'Reflection support not included in PHP.'
));
// Check for Standard PHP Library (SPL) support
$this->requireFunction('spl_classes', array(
'PHP Configuration',
'SPL support',
'Standard PHP Library (SPL) not included in PHP.'
));
$this->requireDateTimezone(array(
'PHP Configuration',
'date.timezone setting and validity',
'date.timezone option in php.ini must be set correctly.',
ini_get('date.timezone')
));
$this->suggestClass('finfo', array(
'PHP Configuration',
'fileinfo support',
'fileinfo should be enabled in PHP. SilverStripe uses it for MIME type detection of files. '
. 'SilverStripe will still operate, but email attachments and sending files to browser '
. '(e.g. export data to CSV) may not work correctly without finfo.'
));
$this->suggestFunction('curl_init', array(
'PHP Configuration',
'curl support',
'curl should be enabled in PHP. SilverStripe uses it for consuming web services'
. ' via the RestfulService class and many modules rely on it.'
));
$this->suggestClass('tidy', array(
'PHP Configuration',
'tidy support',
'Tidy provides a library of code to clean up your html. '
. 'SilverStripe will operate fine without tidy but HTMLCleaner will not be effective.'
));
$this->suggestPHPSetting('asp_tags', array(false), array(
'PHP Configuration',
'asp_tags option',
'This should be turned off as it can cause issues with SilverStripe'
));
$this->requirePHPSetting('magic_quotes_gpc', array(false), array(
'PHP Configuration',
'magic_quotes_gpc option',
'This should be turned off, as it can cause issues with cookies. '
. 'More specifically, unserializing data stored in cookies.'
));
$this->suggestPHPSetting('display_errors', array(false), array(
'PHP Configuration',
'display_errors option',
'Unless you\'re in a development environment, this should be turned off, '
. 'as it can expose sensitive data to website users.'
));
// on some weirdly configured webservers arg_separator.output is set to &amp;
// which will results in links like ?param=value&amp;foo=bar which will not be i
$this->suggestPHPSetting('arg_separator.output', array('&', ''), array(
'PHP Configuration',
'arg_separator.output option',
'This option defines how URL parameters are concatenated. '
. 'If not set to \'&\' this may cause issues with URL GET parameters'
));
// always_populate_raw_post_data should be set to -1 if PHP < 7.0
if (version_compare(PHP_VERSION, '7.0.0', '<')) {
$this->suggestPHPSetting('always_populate_raw_post_data', ['-1'], [
'PHP Configuration',
'always_populate_raw_post_data option',
'It\'s highly recommended to set this to \'-1\' in php 5.x, as $HTTP_RAW_POST_DATA is removed in php 7'
]);
}
// Check memory allocation
$this->requireMemory(32 * 1024 * 1024, 64 * 1024 * 1024, array(
"PHP Configuration",
"Memory allocation (PHP config option 'memory_limit')",
"SilverStripe needs a minimum of 32M allocated to PHP, but recommends 64M.",
ini_get("memory_limit")
));
return $this->errors;
}
public function suggestPHPSetting($settingName, $settingValues, $testDetails) {
$this->testing($testDetails);
// special case for display_errors, check the original value before
// it was changed at the start of this script.
if($settingName == 'display_errors') {
global $originalDisplayErrorsValue;
$val = $originalDisplayErrorsValue;
} else {
$val = ini_get($settingName);
}
if(!in_array($val, $settingValues) && $val != $settingValues) {
$this->warning($testDetails, "$settingName is set to '$val' in php.ini. $testDetails[2]");
}
}
public function requirePHPSetting($settingName, $settingValues, $testDetails) {
$this->testing($testDetails);
$val = ini_get($settingName);
if(!in_array($val, $settingValues) && $val != $settingValues) {
$this->error($testDetails, "$settingName is set to '$val' in php.ini. $testDetails[2]");
}
}
public function suggestClass($class, $testDetails) {
$this->testing($testDetails);
if(!class_exists($class)) {
$this->warning($testDetails);
}
}
public function suggestFunction($class, $testDetails) {
$this->testing($testDetails);
if(!function_exists($class)) {
$this->warning($testDetails);
}
}
public function requireDateTimezone($testDetails) {
$this->testing($testDetails);
$result = ini_get('date.timezone') && in_array(ini_get('date.timezone'), timezone_identifiers_list());
if(!$result) {
$this->error($testDetails);
}
}
public function requireMemory($min, $recommended, $testDetails) {
$_SESSION['forcemem'] = false;
$mem = $this->getPHPMemory();
if($mem < (64 * 1024 * 1024)) {
ini_set('memory_limit', '64M');
$mem = $this->getPHPMemory();
$testDetails[3] = ini_get("memory_limit");
}
$this->testing($testDetails);
if($mem < $min && $mem > 0) {
$message = $testDetails[2] . " You only have " . ini_get("memory_limit") . " allocated";
$this->error($testDetails, $message);
return false;
} else if($mem < $recommended && $mem > 0) {
$message = $testDetails[2] . " You only have " . ini_get("memory_limit") . " allocated";
$this->warning($testDetails, $message);
return false;
} elseif($mem == 0) {
$message = $testDetails[2] . " We can't determine how much memory you have allocated. "
. "Install only if you're sure you've allocated at least 20 MB.";
$this->warning($testDetails, $message);
return false;
}
return true;
}
public function getPHPMemory() {
$memString = ini_get("memory_limit");
switch(strtolower(substr($memString, -1))) {
case "k":
return round(substr($memString, 0, -1) * 1024);
case "m":
return round(substr($memString, 0, -1) * 1024 * 1024);
case "g":
return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
default:
return round($memString);
}
}
public function listErrors() {
if($this->errors) {
echo "<p>The following problems are preventing me from installing SilverStripe CMS:</p>\n\n";
foreach($this->errors as $error) {
echo "<li>" . htmlentities(implode(", ", $error), ENT_COMPAT, 'UTF-8') . "</li>\n";
}
}
}
public function showTable($section = null) {
if($section) {
$tests = $this->tests[$section];
$id = strtolower(str_replace(' ', '_', $section));
echo "<table id=\"{$id}_results\" class=\"testResults\" width=\"100%\">";
foreach($tests as $test => $result) {
echo "<tr class=\"$result[0]\"><td>$test</td><td>"
. nl2br(htmlentities($result[1], ENT_COMPAT, 'UTF-8')) . "</td></tr>";
}
echo "</table>";
} else {
foreach($this->tests as $section => $tests) {
$failedRequirements = 0;
$warningRequirements = 0;
$output = "";
foreach($tests as $test => $result) {
if(isset($result['0'])) {
switch($result['0']) {
case 'error':
$failedRequirements++;
break;
case 'warning':
$warningRequirements++;
break;
}
}
$output .= "<tr class=\"$result[0]\"><td>$test</td><td>"
. nl2br(htmlentities($result[1], ENT_COMPAT, 'UTF-8')) . "</td></tr>";
}
$className = "good";
$text = "All Requirements Pass";
$pluralWarnings = ($warningRequirements == 1) ? 'Warning' : 'Warnings';
if($failedRequirements > 0) {
$className = "error";
$pluralWarnings = ($warningRequirements == 1) ? 'Warning' : 'Warnings';
$text = $failedRequirements . ' Failed and ' . $warningRequirements . ' ' . $pluralWarnings;
} else if($warningRequirements > 0) {
$className = "warning";
$text = "All Requirements Pass but " . $warningRequirements . ' ' . $pluralWarnings;
}
echo "<h5 class='requirement $className'>$section <a href='#'>Show All Requirements</a> <span>$text</span></h5>";
echo "<table class=\"testResults\">";
echo $output;
echo "</table>";
}
}
}
public function requireFunction($funcName, $testDetails) {
$this->testing($testDetails);
if(!function_exists($funcName)) {
$this->error($testDetails);
return false;
}
return true;
}
public function requireClass($className, $testDetails) {
$this->testing($testDetails);
if(!class_exists($className)) {
$this->error($testDetails);
return false;
}
return true;
}
/**
* Require that the given class doesn't exist
*
* @param array $classNames
* @param array $testDetails
* @return bool
*/
public function requireNoClasses($classNames, $testDetails) {
$this->testing($testDetails);
$badClasses = array();
foreach($classNames as $className) {
if(class_exists($className)) {
$badClasses[] = $className;
}
}
if($badClasses) {
$message = $testDetails[2] . ". The following classes are at fault: " . implode(', ', $badClasses);
$this->error($testDetails, $message);
return false;
}
return true;
}
public function checkApacheVersion($testDetails) {
$this->testing($testDetails);
$is1pointx = preg_match('#Apache[/ ]1\.#', $testDetails[3]);
if($is1pointx) {
$this->error($testDetails);
}
return true;
}
public function requirePHPVersion($recommendedVersion, $requiredVersion, $testDetails) {
$this->testing($testDetails);
$installedVersion = phpversion();
if(version_compare($installedVersion, $requiredVersion, '<')) {
$message = "SilverStripe requires PHP version $requiredVersion or later.\n
PHP version $installedVersion is currently installed.\n
While SilverStripe requires at least PHP version $requiredVersion, upgrading to $recommendedVersion or later is recommended.\n
If you are installing SilverStripe on a shared web server, please ask your web hosting provider to upgrade PHP for you.";
$this->error($testDetails, $message);
return false;
}
if(version_compare($installedVersion, $recommendedVersion, '<')) {
$message = "PHP version $installedVersion is currently installed.\n
Upgrading to at least PHP version $recommendedVersion is recommended.\n
SilverStripe should run, but you may run into issues. Future releases may require a later version of PHP.\n";
$this->warning($testDetails, $message);
return false;
}
return true;
}
/**
* Check that a module exists
*
* @param string $dirname
* @return bool
*/
public function checkModuleExists($dirname) {
$path = $this->getBaseDir() . $dirname;
return file_exists($path) && ($dirname == 'mysite' || file_exists($path . '/_config.php'));
}
/**
* The same as {@link requireFile()} but does additional checks
* to ensure the module directory is intact.
*
* @param string $dirname
* @param array $testDetails
*/
public function requireModule($dirname, $testDetails) {
$this->testing($testDetails);
$path = $this->getBaseDir() . $dirname;
if(!file_exists($path)) {
$testDetails[2] .= " Directory '$path' not found. Please make sure you have uploaded the SilverStripe files to your webserver correctly.";
$this->error($testDetails);
} elseif(!file_exists($path . '/_config.php') && $dirname != 'mysite') {
$testDetails[2] .= " Directory '$path' exists, but is missing files. Please make sure you have uploaded "
. "the SilverStripe files to your webserver correctly.";
$this->error($testDetails);
}
}
public function requireFile($filename, $testDetails) {
$this->testing($testDetails);
$filename = $this->getBaseDir() . $filename;
if(!file_exists($filename)) {
$testDetails[2] .= " (file '$filename' not found)";
$this->error($testDetails);
}
}
public function requireWriteable($filename, $testDetails, $absolute = false) {
$this->testing($testDetails);
if($absolute) {
$filename = str_replace('/', DIRECTORY_SEPARATOR, $filename);
} else {
$filename = $this->getBaseDir() . str_replace('/', DIRECTORY_SEPARATOR, $filename);
}
if(file_exists($filename)) $isWriteable = is_writeable($filename);
else $isWriteable = is_writeable(dirname($filename));
if(!$isWriteable) {
if(function_exists('posix_getgroups')) {
$userID = posix_geteuid();
$user = posix_getpwuid($userID);
$currentOwnerID = fileowner(file_exists($filename) ? $filename : dirname($filename));
$currentOwner = posix_getpwuid($currentOwnerID);
$testDetails[2] .= "User '$user[name]' needs to be able to write to this file:\n$filename\n\nThe "
. "file is currently owned by '$currentOwner[name]'. ";
if($user['name'] == $currentOwner['name']) {
$testDetails[2] .= "We recommend that you make the file writeable.";
} else {
$groups = posix_getgroups();
$groupList = array();
foreach($groups as $group) {
$groupInfo = posix_getgrgid($group);
if(in_array($currentOwner['name'], $groupInfo['members'])) $groupList[] = $groupInfo['name'];
}
if($groupList) {
$testDetails[2] .= " We recommend that you make the file group-writeable "
. "and change the group to one of these groups:\n - " . implode("\n - ", $groupList)
. "\n\nFor example:\nchmod g+w $filename\nchgrp " . $groupList[0] . " $filename";
} else {
$testDetails[2] .= " There is no user-group that contains both the web-server user and the "
. "owner of this file. Change the ownership of the file, create a new group, or "
. "temporarily make the file writeable by everyone during the install process.";
}
}
} else {
$testDetails[2] .= "The webserver user needs to be able to write to this file:\n$filename";
}
$this->error($testDetails);
}
}
public function requireTempFolder($testDetails) {
$this->testing($testDetails);
try {
$tempFolder = getTempFolder();
} catch(Exception $e) {
$tempFolder = false;
}
if(!$tempFolder) {
$testDetails[2] = "Permission problem gaining access to a temp directory. " .
"Please create a folder named silverstripe-cache in the base directory " .
"of the installation and ensure it has the adequate permissions.";
$this->error($testDetails);
}
}
public function requireApacheModule($moduleName, $testDetails) {
$this->testing($testDetails);
if(!in_array($moduleName, apache_get_modules())) {
$this->error($testDetails);
return false;
} else {
return true;
}
}
public function testApacheRewriteExists($moduleName = 'mod_rewrite') {
if(function_exists('apache_get_modules') && in_array($moduleName, apache_get_modules())) {
return true;
} elseif(isset($_SERVER['HTTP_MOD_REWRITE']) && $_SERVER['HTTP_MOD_REWRITE'] == 'On') {
return true;
} elseif(isset($_SERVER['REDIRECT_HTTP_MOD_REWRITE']) && $_SERVER['REDIRECT_HTTP_MOD_REWRITE'] == 'On') {
return true;
} else {
return false;
}
}
public function testIISRewriteModuleExists($moduleName = 'IIS_UrlRewriteModule') {
if(isset($_SERVER[$moduleName]) && $_SERVER[$moduleName]) {
return true;
} else {
return false;
}
}
public function requireApacheRewriteModule($moduleName, $testDetails) {
$this->testing($testDetails);
if($this->testApacheRewriteExists()) {
return true;
} else {
$this->warning($testDetails);
return false;
}
}
/**
* Determines if the web server has any rewriting capability.
* @return boolean
*/
public function hasRewritingCapability() {
return ($this->testApacheRewriteExists() || $this->testIISRewriteModuleExists());
}
public function requireIISRewriteModule($moduleName, $testDetails) {
$this->testing($testDetails);
if($this->testIISRewriteModuleExists()) {
return true;
} else {
$this->warning($testDetails);
return false;
}
}
public function getDatabaseTypeNice($databaseClass) {
return substr($databaseClass, 0, -8);
}
/**
* Get an instance of a helper class for the specific database.
*
* @param string $databaseClass e.g. MySQLDatabase or MSSQLDatabase
* @return DatabaseConfigurationHelper
*/
public function getDatabaseConfigurationHelper($databaseClass) {
return DatabaseAdapterRegistry::getDatabaseConfigurationHelper($databaseClass);
}
public function requireDatabaseFunctions($databaseConfig, $testDetails) {
$this->testing($testDetails);
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
if (!$helper) {
$this->error($testDetails, "Couldn't load database helper code for ". $databaseConfig['type']);
return false;
}
$result = $helper->requireDatabaseFunctions($databaseConfig);
if($result) {
return true;
} else {
$this->error($testDetails);
return false;
}
}
public function requireDatabaseConnection($databaseConfig, $testDetails) {
$this->testing($testDetails);
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
$result = $helper->requireDatabaseConnection($databaseConfig);
if($result['success']) {
return true;
} else {
$testDetails[2] .= ": " . $result['error'];
$this->error($testDetails);
return false;
}
}
public function requireDatabaseVersion($databaseConfig, $testDetails) {
$this->testing($testDetails);
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
if(method_exists($helper, 'requireDatabaseVersion')) {
$result = $helper->requireDatabaseVersion($databaseConfig);
if($result['success']) {
return true;
} else {
$testDetails[2] .= $result['error'];
$this->warning($testDetails);
return false;
}
}
// Skipped test because this database has no required version
return true;
}
public function requireDatabaseServer($databaseConfig, $testDetails) {
$this->testing($testDetails);
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
$result = $helper->requireDatabaseServer($databaseConfig);
if($result['success']) {
return true;
} else {
$message = $testDetails[2] . ": " . $result['error'];
$this->error($testDetails, $message);
return false;
}
}
public function requireDatabaseOrCreatePermissions($databaseConfig, $testDetails) {
$this->testing($testDetails);
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
$result = $helper->requireDatabaseOrCreatePermissions($databaseConfig);
if($result['success']) {
if($result['alreadyExists']) {
$testDetails[3] = "Database $databaseConfig[database]";
} else {
$testDetails[3] = "Able to create a new database";
}
$this->testing($testDetails);
return true;
} else {
if(empty($result['cannotCreate'])) {
$message = $testDetails[2] . ". Please create the database manually.";
} else {
$message = $testDetails[2] . " (user '$databaseConfig[username]' doesn't have CREATE DATABASE permissions.)";
}
$this->error($testDetails, $message);
return false;
}
}
public function requireDatabaseAlterPermissions($databaseConfig, $testDetails) {
$this->testing($testDetails);
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
$result = $helper->requireDatabaseAlterPermissions($databaseConfig);
if ($result['success']) {
return true;
} else {
$message = "Silverstripe cannot alter tables. This won't prevent installation, however it may "
. "cause issues if you try to run a /dev/build once installed.";
$this->warning($testDetails, $message);
return false;
}
}
public function requireServerVariables($varNames, $testDetails) {
$this->testing($testDetails);
$missing = array();
foreach($varNames as $varName) {
if(!isset($_SERVER[$varName]) || !$_SERVER[$varName]) {
$missing[] = '$_SERVER[' . $varName . ']';
}
}
if(!$missing) {
return true;
}
$message = $testDetails[2] . " (the following PHP variables are missing: " . implode(", ", $missing) . ")";
$this->error($testDetails, $message);
return false;
}
public function requirePostSupport($testDetails) {
$this->testing($testDetails);
if(!isset($_POST)) {
$this->error($testDetails);
return false;
}
return true;
}
public function isRunningWebServer($testDetails) {
$this->testing($testDetails);
if($testDetails[3]) {
return true;
} else {
$this->warning($testDetails);
return false;
}
}
// Must be PHP4 compatible
var $baseDir;
public function getBaseDir() {
// Cache the value so that when the installer mucks with SCRIPT_FILENAME half way through, this method
// still returns the correct value.
if(!$this->baseDir) $this->baseDir = realpath(dirname($_SERVER['SCRIPT_FILENAME'])) . DIRECTORY_SEPARATOR;
return $this->baseDir;
}
public function testing($testDetails) {
if(!$testDetails) return;
$section = $testDetails[0];
$test = $testDetails[1];
$message = "OK";
if(isset($testDetails[3])) $message .= " ($testDetails[3])";
$this->tests[$section][$test] = array("good", $message);
}
public function error($testDetails, $message = null) {
if (!is_array($testDetails)) {
throw new InvalidArgumentException("Invalid error");
}
$section = $testDetails[0];
$test = $testDetails[1];
if (!$message && isset($testDetails[2])) {
$message = $testDetails[2];
}
$this->tests[$section][$test] = array("error", $message);
$this->errors[] = $testDetails;
}
public function warning($testDetails, $message = null) {
if (!is_array($testDetails)) {
throw new InvalidArgumentException("Invalid warning");
}
$section = $testDetails[0];
$test = $testDetails[1];
if (!$message && isset($testDetails[2])) {
$message = $testDetails[2];
}
$this->tests[$section][$test] = array("warning", $message);
$this->warnings[] = $testDetails;
}
public function hasErrors() {
return sizeof($this->errors);
}
public function hasWarnings() {
return sizeof($this->warnings);
}
}
class Installer extends InstallRequirements {
public function __construct() {
// Cache the baseDir value
$this->getBaseDir();
}
public function install($config) {
?>
<html>
<head>
<meta charset="utf-8"/>
<title>Installing SilverStripe...</title>
<link rel="stylesheet" type="text/css" href="<?php echo FRAMEWORK_NAME; ?>/src/Dev/Install/client/styles/install.css"/>
<script src="//code.jquery.com/jquery-1.7.2.min.js"></script>
</head>
<body>
<div class="install-header">
<div class="inner">
<div class="brand">
<span class="logo"></span>
<h1>SilverStripe</h1>
</div>
</div>
</div>
<div id="Navigation">&nbsp;</div>
<div class="clear"><!-- --></div>
<div class="main">
<div class="inner">
<h2>Installing SilverStripe...</h2>
<p>I am now running through the installation steps (this should take about 30 seconds)</p>
<p>If you receive a fatal error, refresh this page to continue the installation</p>
<ul>
<?php
$webserver = $this->findWebserver();
$isIIS = $this->isIIS();
$isApache = $this->isApache();
flush();
if(isset($config['stats'])) {
if(file_exists(FRAMEWORK_NAME . '/silverstripe_version')) {
$silverstripe_version = file_get_contents(FRAMEWORK_NAME . '/silverstripe_version');
} else {
$silverstripe_version = "unknown";
}
$phpVersion = urlencode(phpversion());
$encWebserver = urlencode($webserver);
$dbType = $config['db']['type'];
// Try to determine the database version from the helper
$databaseVersion = $config['db']['type'];
$helper = $this->getDatabaseConfigurationHelper($dbType);
if($helper && method_exists($helper, 'getDatabaseVersion')) {
$versionConfig = $config['db'][$dbType];
$versionConfig['type'] = $dbType;
$databaseVersion = urlencode($dbType . ': ' . $helper->getDatabaseVersion($versionConfig));
}
$url = "http://ss2stat.silverstripe.com/Installation/add?SilverStripe=$silverstripe_version&PHP=$phpVersion&Database=$databaseVersion&WebServer=$encWebserver";
if(isset($_SESSION['StatsID']) && $_SESSION['StatsID']) {
$url .= '&ID=' . $_SESSION['StatsID'];
}
@$_SESSION['StatsID'] = file_get_contents($url);
}
if(file_exists('mysite/_config.php')) {
// Truncate the contents of _config instead of deleting it - we can't re-create it because Windows handles permissions slightly
// differently to UNIX based filesystems - it takes the permissions from the parent directory instead of retaining them
$fh = fopen('mysite/_config.php', 'wb');
fclose($fh);
}
// Escape user input for safe insertion into PHP file
$theme = isset($_POST['template']) ? addcslashes($_POST['template'], "\'") : 'simple';
$locale = isset($_POST['locale']) ? addcslashes($_POST['locale'], "\'") : 'en_US';
$type = addcslashes($config['db']['type'], "\'");
$dbConfig = $config['db'][$type];
foreach ($dbConfig as &$configValue) {
$configValue = addcslashes($configValue, "\\\'");
}
if(!isset($dbConfig['path'])) $dbConfig['path'] = '';
if(!$dbConfig) {
echo "<p style=\"color: red\">Bad config submitted</p><pre>";
print_r($config);
echo "</pre>";
die();
}
// Write the config file
global $usingEnv;
if($usingEnv) {
$this->statusMessage("Setting up 'mysite/_config.php' for use with environment variables...");
$this->writeToFile("mysite/_config.php", <<<PHP
<?php
global \$project;
\$project = 'mysite';
global \$database;
\$database = '{$dbConfig['database']}';
require_once('conf/ConfigureFromEnv.php');
PHP
);
} else {
$this->statusMessage("Setting up 'mysite/_config.php'...");
// Create databaseConfig
$lines = array(
$lines[] = "\t'type' => '$type'"
);
foreach($dbConfig as $key => $value) {
$lines[] = "\t'{$key}' => '$value'";
}
$databaseConfigContent = implode(",\n", $lines);
$this->writeToFile("mysite/_config.php", <<<PHP
<?php
global \$project;
\$project = 'mysite';
global \$databaseConfig;
\$databaseConfig = array(
{$databaseConfigContent}
);
PHP
);
}
$this->statusMessage("Setting up 'mysite/_config/config.yml'");
$this->writeToFile("mysite/_config/config.yml", <<<YML
---
Name: mysite
---
# YAML configuration for SilverStripe
# See http://doc.silverstripe.org/framework/en/topics/configuration
# Caution: Indentation through two spaces, not tabs
SilverStripe\\View\\SSViewer:
themes:
- '$theme'
- '\$default'
SilverStripe\\i18n\\i18n:
default_locale: '$locale'
YML
);
if(!$this->checkModuleExists('cms')) {
$this->writeToFile("mysite/code/RootURLController.php", <<<PHP
<?php
use SilverStripe\\Control\\Controller;
class RootURLController extends Controller {
public function index() {
echo "<html>Your site is now set up. Start adding controllers to mysite to get started.</html>";
}
}
PHP
);
}
// Write the appropriate web server configuration file for rewriting support
if($this->hasRewritingCapability()) {
if($isApache) {
$this->statusMessage("Setting up '.htaccess' file...");
$this->createHtaccess();
} elseif($isIIS) {
$this->statusMessage("Setting up 'web.config' file...");
$this->createWebConfig();
}
}
// Load the SilverStripe runtime
$_SERVER['SCRIPT_FILENAME'] = dirname(realpath($_SERVER['SCRIPT_FILENAME'])) . '/' . FRAMEWORK_NAME . '/main.php';
chdir(FRAMEWORK_NAME);
// Rebuild the manifest
$_GET['flush'] = true;
// Show errors as if you're in development mode
$_SESSION['isDev'] = 1;
$this->statusMessage("Building database schema...");
require_once 'Core/Core.php';
// Build database
$con = new Controller();
$con->pushCurrent();
global $databaseConfig;
DB::connect($databaseConfig);
$dbAdmin = new DatabaseAdmin();
$dbAdmin->doInit();
$dbAdmin->doBuild(true);
// Create default administrator user and group in database
// (not using Security::setDefaultAdmin())
$adminMember = DefaultAdminService::singleton()->findOrCreateDefaultAdmin();
$adminMember->Email = $config['admin']['username'];
$adminMember->Password = $config['admin']['password'];
$adminMember->PasswordEncryption = Security::config()->encryption_algorithm;
try {
$this->statusMessage('Creating default CMS admin account...');
$adminMember->write();
} catch(Exception $e) {
$this->statusMessage(
sprintf('Warning: Default CMS admin account could not be created (error: %s)', $e->getMessage())
);
}
$_SESSION['username'] = $config['admin']['username'];
$_SESSION['password'] = $config['admin']['password'];
if(!$this->errors) {
if(isset($_SERVER['HTTP_HOST']) && $this->hasRewritingCapability()) {
$this->statusMessage("Checking that friendly URLs work...");
$this->checkRewrite();
} else {
require_once 'Core/Startup/ParameterConfirmationToken.php';
$token = new ParameterConfirmationToken('flush');
$params = http_build_query($token->params());
$destinationURL = 'index.php/' .
($this->checkModuleExists('cms') ? "home/successfullyinstalled?$params" : "?$params");
echo <<<HTML
<li>SilverStripe successfully installed; I am now redirecting you to your SilverStripe site...</li>
<script>
setTimeout(function() {
window.location = "$destinationURL";
}, 2000);
</script>
<noscript>
<li><a href="$destinationURL">Click here to access your site.</a></li>
</noscript>
HTML;
}
}
return $this->errors;
}
public function writeToFile($filename, $content) {
$base = $this->getBaseDir();
$this->statusMessage("Setting up $base$filename");
if((@$fh = fopen($base . $filename, 'wb')) && fwrite($fh, $content) && fclose($fh)) {
return true;
}
$this->error("Couldn't write to file $base$filename");
return false;
}
public function createHtaccess() {
$start = "### SILVERSTRIPE START ###\n";
$end = "\n### SILVERSTRIPE END ###";
$base = dirname($_SERVER['SCRIPT_NAME']);
if(defined('DIRECTORY_SEPARATOR')) $base = str_replace(DIRECTORY_SEPARATOR, '/', $base);
else $base = str_replace("\\", '/', $base);
if($base != '.') $baseClause = "RewriteBase '$base'\n";
else $baseClause = "";
if(strpos(strtolower(php_sapi_name()), "cgi") !== false) $cgiClause = "RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]\n";
else $cgiClause = "";
$modulePath = FRAMEWORK_NAME;
$rewrite = <<<TEXT
# Deny access to templates (but allow from localhost)
<Files *.ss>
Order deny,allow
Deny from all
Allow from 127.0.0.1
</Files>
# Deny access to IIS configuration
<Files web.config>
Order deny,allow
Deny from all
</Files>
# Deny access to YAML configuration files which might include sensitive information
<Files *.yml>
Order allow,deny
Deny from all
</Files>
# Route errors to static pages automatically generated by SilverStripe
ErrorDocument 404 /assets/error-404.html
ErrorDocument 500 /assets/error-500.html
<IfModule mod_rewrite.c>
# Turn off index.php handling requests to the homepage fixes issue in apache >=2.4
<IfModule mod_dir.c>
DirectoryIndex disabled
</IfModule>
SetEnv HTTP_MOD_REWRITE On
RewriteEngine On
$baseClause
$cgiClause
# Deny access to potentially sensitive files and folders
RewriteRule ^vendor(/|$) - [F,L,NC]
RewriteRule silverstripe-cache(/|$) - [F,L,NC]
RewriteRule composer\.(json|lock) - [F,L,NC]
# Process through SilverStripe if no file with the requested name exists.
# Pass through the original path as a query parameter, and retain the existing parameters.
RewriteCond %{REQUEST_URI} ^(.*)$
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule .* $modulePath/main.php?url=%1 [QSA]
</IfModule>
TEXT;
if(file_exists('.htaccess')) {
$htaccess = file_get_contents('.htaccess');
if(strpos($htaccess, '### SILVERSTRIPE START ###') === false && strpos($htaccess, '### SILVERSTRIPE END ###') === false) {
$htaccess .= "\n### SILVERSTRIPE START ###\n### SILVERSTRIPE END ###\n";
}
if(strpos($htaccess, '### SILVERSTRIPE START ###') !== false && strpos($htaccess, '### SILVERSTRIPE END ###') !== false) {
$start = substr($htaccess, 0, strpos($htaccess, '### SILVERSTRIPE START ###')) . "### SILVERSTRIPE START ###\n";
$end = "\n" . substr($htaccess, strpos($htaccess, '### SILVERSTRIPE END ###'));
}
}
$this->writeToFile('.htaccess', $start . $rewrite . $end);
}
/**
* Writes basic configuration to the web.config for IIS
* so that rewriting capability can be use.
*/
public function createWebConfig() {
$modulePath = FRAMEWORK_NAME;
$content = <<<TEXT
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<security>
<requestFiltering>
<hiddenSegments applyToWebDAV="false">
<add segment="silverstripe-cache" />
<add segment="vendor" />
<add segment="composer.json" />
<add segment="composer.lock" />
</hiddenSegments>
<fileExtensions allowUnlisted="true" >
<add fileExtension=".ss" allowed="false"/>
<add fileExtension=".yml" allowed="false"/>
</fileExtensions>
</requestFiltering>
</security>
<rewrite>
<rules>
<rule name="SilverStripe Clean URLs" stopProcessing="true">
<match url="^(.*)$" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
</conditions>
<action type="Rewrite" url="$modulePath/main.php?url={R:1}" appendQueryString="true" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>
TEXT;
$this->writeToFile('web.config', $content);
}
public function checkRewrite() {
require_once 'Core/Startup/ParameterConfirmationToken.php';
$token = new ParameterConfirmationToken('flush');
$params = http_build_query($token->params());
$destinationURL = str_replace('install.php', '', $_SERVER['SCRIPT_NAME']) .
($this->checkModuleExists('cms') ? "home/successfullyinstalled?$params" : "?$params");
echo <<<HTML
<li id="ModRewriteResult">Testing...</li>
<script>
if(typeof $ == 'undefined') {
document.getElemenyById('ModeRewriteResult').innerHTML = "I can't run jQuery ajax to set rewriting; I will redirect you to the homepage to see if everything is working.";
setTimeout(function() {
window.location = "$destinationURL";
}, 10000);
} else {
$.ajax({
method: 'get',
url: 'InstallerTest/testrewrite',
complete: function(response) {
var r = response.responseText.replace(/[^A-Z]?/g,"");
if(r === "OK") {
$('#ModRewriteResult').html("Friendly URLs set up successfully; I am now redirecting you to your SilverStripe site...")
setTimeout(function() {
window.location = "$destinationURL";
}, 2000);
} else {
$('#ModRewriteResult').html("Friendly URLs are not working. This is most likely because a rewrite module isn't configured "
+ "correctly on your site. You may need to get your web host or server administrator to do this for you: "
+ "<ul>"
+ "<li><strong>mod_rewrite</strong> or other rewrite module is enabled on your web server</li>"
+ "<li><strong>AllowOverride All</strong> is set for the directory where SilverStripe is installed</li>"
+ "</ul>");
}
}
});
}
</script>
<noscript>
<li><a href="$destinationURL">Click here</a> to check friendly URLs are working. If you get a 404 then something is wrong.</li>
</noscript>
HTML;
}
public function var_export_array_nokeys($array) {
$retval = "array(\n";
foreach($array as $item) {
$retval .= "\t'";
$retval .= trim($item);
$retval .= "',\n";
}
$retval .= ")";
return $retval;
}
/**
* Show an installation status message.
* The output differs depending on whether this is CLI or web based
*
* @param string $msg
*/
public function statusMessage($msg) {
echo "<li>$msg</li>\n";
flush();
}
}

281
src/Dev/Install/install5.php Executable file
View File

@ -0,0 +1,281 @@
<?php
/************************************************************************************
************************************************************************************
** **
** If you can read this text in your browser then you don't have PHP installed. **
** Please install PHP 5.5.0 or higher. **
** **
************************************************************************************
************************************************************************************/
namespace SilverStripe\Dev\Install;
// Back up original ini config
$originalIni = [];
$iniSet = function ($name, $value) use (&$originalIni) {
if (!isset($originalIni[$name])) {
$originalIni[$name] = ini_get($name);
}
ini_set($name, $value);
};
// speed up mysql_connect timeout if the server can't be found
$iniSet('mysql.connect_timeout', 5);
// Don't die half was through installation; that does more harm than good
$iniSet('max_execution_time', 0);
// set display_errors php setting to on to force installer to avoid blank screen of death.
// get the original value so it can be used in PHP requirement checks later in this script.
$iniSet('display_errors', '1');
error_reporting(E_ALL | E_STRICT);
// Attempt to start a session so that the username and password can be sent back to the user.
if (function_exists('session_start') && !session_id()) {
session_start();
}
// require composers autoloader
require __DIR__ . '/../../includes/autoload.php';
$usingEnv = !empty($_REQUEST['useEnv']);
// Set default locale, but try and sniff from the user agent
$defaultLocale = 'en_US';
$locales = array(
'af_ZA' => 'Afrikaans (South Africa)',
'ar_EG' => 'Arabic (Egypt)',
'hy_AM' => 'Armenian (Armenia)',
'ast_ES' => 'Asturian (Spain)',
'az_AZ' => 'Azerbaijani (Azerbaijan)',
'bs_BA' => 'Bosnian (Bosnia and Herzegovina)',
'bg_BG' => 'Bulgarian (Bulgaria)',
'ca_ES' => 'Catalan (Spain)',
'zh_CN' => 'Chinese (China)',
'zh_TW' => 'Chinese (Taiwan)',
'hr_HR' => 'Croatian (Croatia)',
'cs_CZ' => 'Czech (Czech Republic)',
'da_DK' => 'Danish (Denmark)',
'nl_NL' => 'Dutch (Netherlands)',
'en_GB' => 'English (United Kingdom)',
'en_US' => 'English (United States)',
'eo_XX' => 'Esperanto',
'et_EE' => 'Estonian (Estonia)',
'fo_FO' => 'Faroese (Faroe Islands)',
'fi_FI' => 'Finnish (Finland)',
'fr_FR' => 'French (France)',
'de_DE' => 'German (Germany)',
'el_GR' => 'Greek (Greece)',
'he_IL' => 'Hebrew (Israel)',
'hu_HU' => 'Hungarian (Hungary)',
'is_IS' => 'Icelandic (Iceland)',
'id_ID' => 'Indonesian (Indonesia)',
'it_IT' => 'Italian (Italy)',
'ja_JP' => 'Japanese (Japan)',
'km_KH' => 'Khmer (Cambodia)',
'lc_XX' => 'LOLCAT',
'lv_LV' => 'Latvian (Latvia)',
'lt_LT' => 'Lithuanian (Lithuania)',
'ms_MY' => 'Malay (Malaysia)',
'mi_NZ' => 'Maori (New Zealand)',
'ne_NP' => 'Nepali (Nepal)',
'nb_NO' => 'Norwegian',
'fa_IR' => 'Persian (Iran)',
'pl_PL' => 'Polish (Poland)',
'pt_BR' => 'Portuguese (Brazil)',
'pa_IN' => 'Punjabi (India)',
'ro_RO' => 'Romanian (Romania)',
'ru_RU' => 'Russian (Russia)',
'sr_RS' => 'Serbian (Serbia)',
'si_LK' => 'Sinhalese (Sri Lanka)',
'sk_SK' => 'Slovak (Slovakia)',
'sl_SI' => 'Slovenian (Slovenia)',
'es_AR' => 'Spanish (Argentina)',
'es_MX' => 'Spanish (Mexico)',
'es_ES' => 'Spanish (Spain)',
'sv_SE' => 'Swedish (Sweden)',
'th_TH' => 'Thai (Thailand)',
'tr_TR' => 'Turkish (Turkey)',
'uk_UA' => 'Ukrainian (Ukraine)',
'uz_UZ' => 'Uzbek (Uzbekistan)',
'vi_VN' => 'Vietnamese (Vietnam)',
);
// Discover which databases are available
DatabaseAdapterRegistry::autodiscover();
// Determine which external database modules are USABLE
$databaseClasses = DatabaseAdapterRegistry::get_adapters();
foreach ($databaseClasses as $class => $details) {
$helper = DatabaseAdapterRegistry::getDatabaseConfigurationHelper($class);
$databaseClasses[$class]['hasModule'] = !empty($helper);
}
// Load database config
if (isset($_REQUEST['db'])) {
if (isset($_REQUEST['db']['type'])) {
$type = $_REQUEST['db']['type'];
} else {
if ($type = getenv('SS_DATABASE_CLASS')) {
$_REQUEST['db']['type'] = $type;
} elseif ($databaseClasses['MySQLPDODatabase']['supported']) {
$type = $_REQUEST['db']['type'] = 'MySQLPDODatabase';
} elseif ($databaseClasses['MySQLDatabase']['supported']) {
$type = $_REQUEST['db']['type'] = 'MySQLDatabase';
} else {
// handle error
}
}
// Disabled inputs don't submit anything - we need to use the environment (except the database name)
if ($usingEnv) {
$_REQUEST['db'][$type] = $databaseConfig = array(
"type" => getenv('SS_DATABASE_CLASS') ?: $type,
"server" => getenv('SS_DATABASE_SERVER') ?: "localhost",
"username" => getenv('SS_DATABASE_USERNAME') ?: "root",
"password" => getenv('SS_DATABASE_PASSWORD') ?: "",
"database" => $_REQUEST['db'][$type]['database'],
);
} else {
// Normal behaviour without the environment
$databaseConfig = $_REQUEST['db'][$type];
$databaseConfig['type'] = $type;
}
} else {
if ($type = getenv('SS_DATABASE_CLASS')) {
$_REQUEST['db']['type'] = $type;
} elseif ($databaseClasses['MySQLPDODatabase']['supported']) {
$type = $_REQUEST['db']['type'] = 'MySQLPDODatabase';
} elseif ($databaseClasses['MySQLDatabase']['supported']) {
$type = $_REQUEST['db']['type'] = 'MySQLDatabase';
} else {
// handle error
}
$_REQUEST['db'][$type] = $databaseConfig = array(
"type" => $type,
"server" => getenv('SS_DATABASE_SERVER') ?: "localhost",
"username" => getenv('SS_DATABASE_USERNAME') ?: "root",
"password" => getenv('SS_DATABASE_PASSWORD') ?: "",
"database" => isset($_SERVER['argv'][2]) ? $_SERVER['argv'][2] : "SS_mysite",
);
}
if (isset($_REQUEST['admin'])) {
// Disabled inputs don't submit anything - we need to use the environment (except the database name)
if ($usingEnv) {
$_REQUEST['admin'] = $adminConfig = array(
'username' => getenv('SS_DEFAULT_ADMIN_USERNAME') ?: 'admin',
'password' => getenv('SS_DEFAULT_ADMIN_PASSWORD') ?: '',
);
} else {
$adminConfig = $_REQUEST['admin'];
}
} else {
$_REQUEST['admin'] = $adminConfig = array(
'username' => getenv('SS_DEFAULT_ADMIN_USERNAME') ?: 'admin',
'password' => getenv('SS_DEFAULT_ADMIN_PASSWORD') ?: '',
);
}
$alreadyInstalled = false;
if (file_exists('mysite/_config.php')) {
// Find the $database variable in the relevant config file without having to execute the config file
if (preg_match("/\\\$database\s*=\s*[^\n\r]+[\n\r]/", file_get_contents("mysite/_config.php"), $parts)) {
eval($parts[0]);
if (!empty($database)) {
$alreadyInstalled = true;
}
// Assume that if $databaseConfig is defined in mysite/_config.php, then a non-environment-based installation has
// already gone ahead
} elseif (preg_match(
"/\\\$databaseConfig\s*=\s*[^\n\r]+[\n\r]/",
file_get_contents("mysite/_config.php"),
$parts
)) {
$alreadyInstalled = true;
}
}
if (file_exists(FRAMEWORK_NAME . '/silverstripe_version')) {
$silverstripe_version = file_get_contents(FRAMEWORK_NAME . '/silverstripe_version');
} else {
$silverstripe_version = "unknown";
}
// Check requirements
$req = new InstallRequirements($originalIni);
$req->check();
if ($req->isIIS()) {
$webserverConfigFile = 'web.config';
} else {
$webserverConfigFile = '.htaccess';
}
$hasErrorOtherThanDatabase = false;
$hasOnlyWarnings = false;
$phpIniLocation = php_ini_loaded_file();
if ($req->hasErrors()) {
$hasErrorOtherThanDatabase = true;
} elseif ($req->hasWarnings()) {
$hasOnlyWarnings = true;
}
$dbReq = new InstallRequirements();
if ($databaseConfig) {
$dbReq->checkDatabase($databaseConfig);
}
$adminReq = new InstallRequirements();
if ($adminConfig) {
$adminReq->checkAdminConfig($adminConfig);
}
// Actual processor
$installFromCli = (isset($_SERVER['argv'][1]) && $_SERVER['argv'][1] == 'install');
// CLI-install error message. exit(1) will halt any makefile.
if ($installFromCli && ($req->hasErrors() || $dbReq->hasErrors())) {
echo "Cannot install due to errors:\n";
$req->listErrors();
$dbReq->listErrors();
exit(1);
}
// config-form.html vars (placeholder to prevent deletion)
[
$defaultLocale,
$silverstripe_version,
$locales,
$webserverConfigFile,
$hasErrorOtherThanDatabase,
$hasOnlyWarnings, // If warnings but not errors
$phpIniLocation
];
if ((isset($_REQUEST['go']) || $installFromCli)
&& !$req->hasErrors()
&& !$dbReq->hasErrors()
&& $adminConfig['username']
&& $adminConfig['password']
) {
// Confirm before reinstalling
if (!$installFromCli && $alreadyInstalled) {
include(__DIR__ . '/config-form.html');
} else {
$inst = new Installer();
if ($_REQUEST) {
$inst->install($_REQUEST);
} else {
$inst->install(array(
'db' => $databaseConfig,
'admin' => $adminConfig,
));
}
}
// Show the config form
} else {
include(__DIR__ . '/config-form.html');
}

View File

@ -1,18 +1,18 @@
<html> <html>
<head> <head>
<title>PHP 5.5.0 is required</title> <title>PHP 5.6.0 is required</title>
<link rel="stylesheet" type="text/css" href="framework/src/Dev/Install/client/styles/install.css"> <link rel="stylesheet" type="text/css" href="framework/src/Dev/Install/client/styles/install.css">
</head> </head>
<body> <body>
<div id="BgContainer"> <div id="BgContainer">
<div id="Container"> <div id="Container">
<div id="Header"> <div id="Header">
<h1>PHP 5.5.0 required</h1> <h1>PHP 5.6.0 required</h1>
<div class="left"> <div class="left">
<h3>To run SilverStripe, please install PHP 5.5.0 or greater.</h3> <h3>To run SilverStripe, please install PHP 5.6.0 or greater.</h3>
<p>We have detected that you are running PHP version <b>$PHPVersion</b>. In order to run SilverStripe, <p>We have detected that you are running PHP version <b>$PHPVersion</b>. In order to run SilverStripe,
you must have PHP version 5.5.0 or higher.<p/> you must have PHP version 5.6.0 or higher.<p/>
<p>If you are running on a shared host, you may need to ask your hosting provider how to do this.</p> <p>If you are running on a shared host, you may need to ask your hosting provider how to do this.</p>
</div> </div>

View File

@ -1,14 +0,0 @@
<?php
// Ensure this class can be autoloaded when installed without dev dependencies.
// It's included by default through composer's autoloading.
// class_exists() triggers PSR-4 autoloaders, which should discover if PHPUnit is installed.
// TODO Factor out SapphireTest references from non-dev core code (avoid autoloading in the first place)
namespace {
if (!class_exists('PHPUnit_Framework_TestCase')) {
class PHPUnit_Framework_TestCase
{
}
}
}

View File

@ -2,62 +2,51 @@
namespace SilverStripe\Dev; namespace SilverStripe\Dev;
use Exception;
use LogicException;
use PHPUnit_Framework_TestCase;
use SilverStripe\CMS\Controllers\RootURLController; use SilverStripe\CMS\Controllers\RootURLController;
use SilverStripe\Control\CLIRequestBuilder;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Cookie; use SilverStripe\Control\Cookie;
use SilverStripe\Control\Director;
use SilverStripe\Control\Email\Email; use SilverStripe\Control\Email\Email;
use SilverStripe\Control\Email\Mailer; use SilverStripe\Control\Email\Mailer;
use SilverStripe\Control\Session; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\Tests\FakeController;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\ClassInfo; use SilverStripe\Control\HTTPApplication;
use SilverStripe\Core\Config\ConfigLoader;
use SilverStripe\Core\Config\CoreConfigFactory;
use SilverStripe\Core\Config\DefaultConfig;
use SilverStripe\Core\Config\Middleware\ExtensionMiddleware;
use SilverStripe\Core\Extension;
use SilverStripe\Core\Flushable;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Manifest\ClassManifest; use SilverStripe\Core\Injector\InjectorLoader;
use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Resettable; use SilverStripe\Dev\State\SapphireTestState;
use SilverStripe\Dev\State\TestState;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\ORM\DataExtension; use SilverStripe\ORM\Connect\TempDatabase;
use SilverStripe\ORM\SS_List;
use SilverStripe\Security\IdentityStore;
use SilverStripe\Versioned\Versioned;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\DB; use SilverStripe\ORM\SS_List;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
use SilverStripe\Security\Group; use SilverStripe\Security\Group;
use SilverStripe\Security\IdentityStore;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\View\Requirements; use SilverStripe\Security\Security;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
use SilverStripe\View\ThemeResourceLoader;
use SilverStripe\View\ThemeManifest; if (!class_exists(PHPUnit_Framework_TestCase::class)) {
use PHPUnit_Framework_TestCase; return;
use Translatable; }
use LogicException;
use Exception;
/** /**
* Test case class for the Sapphire framework. * Test case class for the Sapphire framework.
* Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
* to work with. * to work with.
*
* This class should not be used anywhere outside of unit tests, as phpunit may not be installed
* in production sites.
*/ */
class SapphireTest extends PHPUnit_Framework_TestCase class SapphireTest extends PHPUnit_Framework_TestCase implements TestOnly
{ {
/** @config */
private static $dependencies = array(
'fixtureFactory' => '%$FixtureFactory',
);
/** /**
* Path to fixture data for this test run. * Path to fixture data for this test run.
* If passed as an array, multiple fixture files will be loaded. * If passed as an array, multiple fixture files will be loaded.
@ -77,38 +66,22 @@ class SapphireTest extends PHPUnit_Framework_TestCase
* @var Boolean If set to TRUE, this will force a test database to be generated * @var Boolean If set to TRUE, this will force a test database to be generated
* in {@link setUp()}. Note that this flag is overruled by the presence of a * in {@link setUp()}. Note that this flag is overruled by the presence of a
* {@link $fixture_file}, which always forces a database build. * {@link $fixture_file}, which always forces a database build.
*
* @var bool
*/ */
protected $usesDatabase = null; protected $usesDatabase = null;
protected $originalMemberPasswordValidator;
protected $originalRequirements;
protected $originalIsRunningTest;
protected $originalNestedURLsState;
protected $originalMemoryLimit;
/** /**
* @var TestMailer * @var bool
*/
protected $mailer;
/**
* Pointer to the manifest that isn't a test manifest
*/
protected static $regular_manifest;
/**
* @var boolean
*/ */
protected static $is_running_test = false; protected static $is_running_test = false;
/**
* @var ClassManifest
*/
protected static $test_class_manifest;
/** /**
* By default, setUp() does not require default records. Pass * By default, setUp() does not require default records. Pass
* class names in here, and the require/augment default records * class names in here, and the require/augment default records
* function will be called on them. * function will be called on them.
*
* @var array
*/ */
protected $requireDefaultRecordsFrom = array(); protected $requireDefaultRecordsFrom = array();
@ -120,6 +93,8 @@ class SapphireTest extends PHPUnit_Framework_TestCase
* the values are an array of illegal extensions on that class. * the values are an array of illegal extensions on that class.
* *
* Set a class to `*` to remove all extensions (unadvised) * Set a class to `*` to remove all extensions (unadvised)
*
* @var array
*/ */
protected static $illegal_extensions = []; protected static $illegal_extensions = [];
@ -134,6 +109,8 @@ class SapphireTest extends PHPUnit_Framework_TestCase
* <code> * <code>
* array("MyTreeDataObject" => array("Versioned", "Hierarchy")) * array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
* </code> * </code>
*
* @var array
*/ */
protected static $required_extensions = []; protected static $required_extensions = [];
@ -141,6 +118,8 @@ class SapphireTest extends PHPUnit_Framework_TestCase
* By default, the test database won't contain any DataObjects that have the interface TestOnly. * By default, the test database won't contain any DataObjects that have the interface TestOnly.
* This variable lets you define additional TestOnly DataObjects to set up for this test. * This variable lets you define additional TestOnly DataObjects to set up for this test.
* Set it to an array of DataObject subclass names. * Set it to an array of DataObject subclass names.
*
* @var array
*/ */
protected static $extra_dataobjects = []; protected static $extra_dataobjects = [];
@ -161,58 +140,60 @@ class SapphireTest extends PHPUnit_Framework_TestCase
protected $backupGlobals = false; protected $backupGlobals = false;
/** /**
* Helper arrays for illegal_extensions/required_extensions code * State management container for SapphireTest
*
* @var TestState
*/ */
private static $extensions_to_reapply = []; protected static $state = null;
private static $extensions_to_remove = [];
/** /**
* Check flushables on setupBeforeClass() * Temp database helper
* *
* @var bool * @var TempDatabase
*/ */
protected static $flushedFlushables = false; protected static $tempDB = null;
/** /**
* Determines if unit tests are currently run, flag set during test bootstrap. * Gets illegal extensions for this class
* This is used as a cheap replacement for fully mockable state
* in certain contiditions (e.g. access checks).
* Caution: When set to FALSE, certain controllers might bypass
* access checks, so this is a very security sensitive setting.
* *
* @return boolean * @return array
*/ */
public static function is_running_test() public static function getIllegalExtensions()
{
return static::$illegal_extensions;
}
/**
* Gets required extensions for this class
*
* @return array
*/
public static function getRequiredExtensions()
{
return static::$required_extensions;
}
/**
* Check if test bootstrapping has been performed. Must not be relied on
* outside of unit tests.
*
* @return bool
*/
protected static function is_running_test()
{ {
return self::$is_running_test; return self::$is_running_test;
} }
public static function set_is_running_test($bool) /**
* Set test running state
*
* @param bool $bool
*/
protected static function set_is_running_test($bool)
{ {
self::$is_running_test = $bool; self::$is_running_test = $bool;
} }
/**
* Set the manifest to be used to look up test classes by helper functions
*
* @param ClassManifest $manifest
*/
public static function set_test_class_manifest($manifest)
{
self::$test_class_manifest = $manifest;
}
/**
* Return the manifest being used to look up test classes by helper functions
*
* @return ClassManifest
*/
public static function get_test_class_manifest()
{
return self::$test_class_manifest;
}
/** /**
* @return String * @return String
*/ */
@ -221,27 +202,19 @@ class SapphireTest extends PHPUnit_Framework_TestCase
return static::$fixture_file; return static::$fixture_file;
} }
protected $model;
/** /**
* State of Versioned before this test is run * Setup the test.
* Always sets up in order:
* - Reset php state
* - Nest
* - Custom state helpers
* *
* @var string * User code should call parent::setUp() before custom setup code
*/ */
protected $originalReadingMode = null;
protected $originalEnv = null;
protected function setUp() protected function setUp()
{ {
//nest config and injector for each test so they are effectively sandboxed per test // Call state helpers
Config::nest(); static::$state->setUp($this);
Injector::nest();
$this->originalEnv = Director::get_environment_type();
if (class_exists(Versioned::class)) {
$this->originalReadingMode = Versioned::get_reading_mode();
}
// We cannot run the tests on this abstract class. // We cannot run the tests on this abstract class.
if (static::class == __CLASS__) { if (static::class == __CLASS__) {
@ -249,34 +222,18 @@ class SapphireTest extends PHPUnit_Framework_TestCase
return; return;
} }
// Mark test as being run
$this->originalIsRunningTest = self::$is_running_test;
self::$is_running_test = true;
// i18n needs to be set to the defaults or tests fail // i18n needs to be set to the defaults or tests fail
i18n::set_locale(i18n::config()->uninherited('default_locale')); i18n::set_locale(i18n::config()->uninherited('default_locale'));
// Set default timezone consistently to avoid NZ-specific dependencies // Set default timezone consistently to avoid NZ-specific dependencies
date_default_timezone_set('UTC'); date_default_timezone_set('UTC');
// Remove password validation
$this->originalMemberPasswordValidator = Member::password_validator();
$this->originalRequirements = Requirements::backend();
Member::set_password_validator(null); Member::set_password_validator(null);
Cookie::config()->update('report_errors', false); Cookie::config()->update('report_errors', false);
if (class_exists(RootURLController::class)) { if (class_exists(RootURLController::class)) {
RootURLController::reset(); RootURLController::reset();
} }
// Reset all resettables
/** @var Resettable $resettable */
foreach (ClassInfo::implementorsOf(Resettable::class) as $resettable) {
$resettable::reset();
}
if (Controller::has_curr()) {
Controller::curr()->setSession(Session::create(array()));
}
Security::clear_database_is_ready(); Security::clear_database_is_ready();
// Set up test routes // Set up test routes
@ -284,18 +241,15 @@ class SapphireTest extends PHPUnit_Framework_TestCase
$fixtureFiles = $this->getFixturePaths(); $fixtureFiles = $this->getFixturePaths();
// Todo: this could be a special test model
$this->model = DataModel::inst();
// Set up fixture // Set up fixture
if ($fixtureFiles || $this->usesDatabase) { if ($fixtureFiles || $this->usesDatabase) {
if (!self::using_temp_db()) { if (!static::$tempDB->isUsed()) {
self::create_temp_db(); static::$tempDB->build();
} }
DataObject::singleton()->flushCache(); DataObject::singleton()->flushCache();
self::empty_temp_db(); static::$tempDB->clearAllData();
foreach ($this->requireDefaultRecordsFrom as $className) { foreach ($this->requireDefaultRecordsFrom as $className) {
$instance = singleton($className); $instance = singleton($className);
@ -315,18 +269,11 @@ class SapphireTest extends PHPUnit_Framework_TestCase
$this->logInWithPermission("ADMIN"); $this->logInWithPermission("ADMIN");
} }
// Preserve memory settings
$this->originalMemoryLimit = ini_get('memory_limit');
// turn off template debugging // turn off template debugging
SSViewer::config()->update('source_file_comments', false); SSViewer::config()->update('source_file_comments', false);
// Clear requirements
Requirements::clear();
// Set up the test mailer // Set up the test mailer
$this->mailer = new TestMailer(); Injector::inst()->registerService(new TestMailer(), Mailer::class);
Injector::inst()->registerService($this->mailer, Mailer::class);
Email::config()->remove('send_all_emails_to'); Email::config()->remove('send_all_emails_to');
Email::config()->remove('send_all_emails_from'); Email::config()->remove('send_all_emails_from');
Email::config()->remove('cc_all_emails_to'); Email::config()->remove('cc_all_emails_to');
@ -340,126 +287,47 @@ class SapphireTest extends PHPUnit_Framework_TestCase
* don't change state for any called method inside the test, * don't change state for any called method inside the test,
* e.g. dynamically adding an extension. See {@link teardownAfterClass()} * e.g. dynamically adding an extension. See {@link teardownAfterClass()}
* for tearing down the state again. * for tearing down the state again.
*
* Always sets up in order:
* - Reset php state
* - Nest
* - Custom state helpers
*
* User code should call parent::setUpBeforeClass() before custom setup code
*/ */
public static function setUpBeforeClass() public static function setUpBeforeClass()
{ {
// Start tests
static::start(); static::start();
//nest config and injector for each suite so they are effectively sandboxed // Call state helpers
Config::nest(); static::$state->setUpOnce(static::class);
Injector::nest();
$isAltered = false;
if (!Director::isDev()) { // Build DB if we have objects
user_error('Tests can only run in "dev" mode', E_USER_ERROR); if (static::getExtraDataObjects()) {
}
// Remove any illegal extensions that are present
foreach (static::$illegal_extensions as $class => $extensions) {
if (!class_exists($class)) {
continue;
}
if ($extensions === '*') {
$extensions = $class::get_extensions();
}
foreach ($extensions as $extension) {
if (!class_exists($extension) || !$class::has_extension($extension)) {
continue;
}
if (!isset(self::$extensions_to_reapply[$class])) {
self::$extensions_to_reapply[$class] = array();
}
self::$extensions_to_reapply[$class][] = $extension;
$class::remove_extension($extension);
$isAltered = true;
}
}
// Add any required extensions that aren't present
foreach (static::$required_extensions as $class => $extensions) {
if (!class_exists($class)) {
$self = static::class;
throw new LogicException("Test {$self} requires class {$class} which doesn't exist");
}
self::$extensions_to_remove[$class] = array();
foreach ($extensions as $extension) {
$extensionClass = Extension::get_classname_without_arguments($extension);
if (!class_exists($extensionClass)) {
$self = static::class;
throw new LogicException("Test {$self} requires extension {$extension} which doesn't exist");
}
if (!$class::has_extension($extension)) {
if (!isset(self::$extensions_to_remove[$class])) {
self::$extensions_to_reapply[$class] = array();
}
self::$extensions_to_remove[$class][] = $extension;
$class::add_extension($extension);
$isAltered = true;
}
}
}
// If we have made changes to the extensions present, then migrate the database schema.
if ($isAltered || self::$extensions_to_reapply || self::$extensions_to_remove || static::getExtraDataObjects()) {
DataObject::reset(); DataObject::reset();
if (!self::using_temp_db()) { static::resetDBSchema(true, true);
self::create_temp_db();
}
static::resetDBSchema(true);
}
// clear singletons, they're caching old extension info
// which is used in DatabaseAdmin->doBuild()
Injector::inst()->unregisterAllObjects();
// Set default timezone consistently to avoid NZ-specific dependencies
date_default_timezone_set('UTC');
// Flush all flushable records
$flush = !empty($_GET['flush']);
if (!self::$flushedFlushables && $flush) {
self::$flushedFlushables = true;
foreach (ClassInfo::implementorsOf(Flushable::class) as $class) {
$class::flush();
}
} }
} }
/** /**
* tearDown method that's called once per test class rather once per test method. * tearDown method that's called once per test class rather once per test method.
*
* Always sets up in order:
* - Custom state helpers
* - Unnest
* - Reset php state
*
* User code should call parent::tearDownAfterClass() after custom tear down code
*/ */
public static function tearDownAfterClass() public static function tearDownAfterClass()
{ {
// If we have made changes to the extensions present, then migrate the database schema. // Call state helpers
if (self::$extensions_to_reapply || self::$extensions_to_remove) { static::$state->tearDownOnce(static::class);
// @todo: This isn't strictly necessary to restore extensions, but only to ensure that
// Object::$extra_methods is properly flushed. This should be replaced with a simple
// flush mechanism for each $class.
//
// Remove extensions added for testing
foreach (self::$extensions_to_remove as $class => $extensions) {
foreach ($extensions as $extension) {
$class::remove_extension($extension);
}
}
// Reapply ones removed // Reset DB schema
foreach (self::$extensions_to_reapply as $class => $extensions) {
foreach ($extensions as $extension) {
$class::add_extension($extension);
}
}
}
//unnest injector / config now that the test suite is over
// this will reset all the extensions on the object too (see setUpBeforeClass)
Injector::unnest();
Config::unnest();
$extraDataObjects = static::getExtraDataObjects();
if (!empty(self::$extensions_to_reapply) || !empty(self::$extensions_to_remove) || !empty($extraDataObjects)) {
static::resetDBSchema(); static::resetDBSchema();
} }
}
/** /**
* @return FixtureFactory * @return FixtureFactory
@ -472,6 +340,12 @@ class SapphireTest extends PHPUnit_Framework_TestCase
return $this->fixtureFactory; return $this->fixtureFactory;
} }
/**
* Sets a new fixture factory
*
* @param FixtureFactory $factory
* @return $this
*/
public function setFixtureFactory(FixtureFactory $factory) public function setFixtureFactory(FixtureFactory $factory)
{ {
$this->fixtureFactory = $factory; $this->fixtureFactory = $factory;
@ -560,11 +434,11 @@ class SapphireTest extends PHPUnit_Framework_TestCase
/** /**
* Useful for writing unit tests without hardcoding folder structures. * Useful for writing unit tests without hardcoding folder structures.
* *
* @return String Absolute path to current class. * @return string Absolute path to current class.
*/ */
protected function getCurrentAbsolutePath() protected function getCurrentAbsolutePath()
{ {
$filename = self::$test_class_manifest->getItemPath(static::class); $filename = ClassLoader::inst()->getItemPath(static::class);
if (!$filename) { if (!$filename) {
throw new LogicException("getItemPath returned null for " . static::class); throw new LogicException("getItemPath returned null for " . static::class);
} }
@ -572,7 +446,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase
} }
/** /**
* @return String File path relative to webroot * @return string File path relative to webroot
*/ */
protected function getCurrentRelativePath() protected function getCurrentRelativePath()
{ {
@ -584,28 +458,17 @@ class SapphireTest extends PHPUnit_Framework_TestCase
return $path; return $path;
} }
/**
* Setup the test.
* Always sets up in order:
* - Custom state helpers
* - Unnest
* - Reset php state
*
* User code should call parent::tearDown() after custom tear down code
*/
protected function tearDown() protected function tearDown()
{ {
// Preserve memory settings
ini_set('memory_limit', ($this->originalMemoryLimit) ? $this->originalMemoryLimit : -1);
// Restore email configuration
$this->mailer = null;
// Restore password validation
if ($this->originalMemberPasswordValidator) {
Member::set_password_validator($this->originalMemberPasswordValidator);
}
// Restore requirements
if ($this->originalRequirements) {
Requirements::set_backend($this->originalRequirements);
}
// Mark test as no longer being run - we use originalIsRunningTest to allow for nested SapphireTest calls
self::$is_running_test = $this->originalIsRunningTest;
$this->originalIsRunningTest = null;
// Reset mocked datetime // Reset mocked datetime
DBDatetime::clear_mock_now(); DBDatetime::clear_mock_now();
@ -618,14 +481,8 @@ class SapphireTest extends PHPUnit_Framework_TestCase
$response->removeHeader('Location'); $response->removeHeader('Location');
} }
Director::set_environment_type($this->originalEnv); // Call state helpers
if (class_exists(Versioned::class)) { static::$state->tearDown($this);
Versioned::set_reading_mode($this->originalReadingMode);
}
//unnest injector / config now that tests are over
Injector::unnest();
Config::unnest();
} }
public static function assertContains( public static function assertContains(
@ -658,36 +515,48 @@ class SapphireTest extends PHPUnit_Framework_TestCase
/** /**
* Clear the log of emails sent * Clear the log of emails sent
*
* @return bool True if emails cleared
*/ */
public function clearEmails() public function clearEmails()
{ {
return $this->mailer->clearEmails(); /** @var Mailer $mailer */
$mailer = Injector::inst()->get(Mailer::class);
if ($mailer instanceof TestMailer) {
$mailer->clearEmails();
return true;
}
return false;
} }
/** /**
* Search for an email that was sent. * Search for an email that was sent.
* All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression. * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
* @param $to * @param string $to
* @param $from * @param string $from
* @param $subject * @param string $subject
* @param $content * @param string $content
* @return array Contains keys: 'type', 'to', 'from', 'subject','content', 'plainContent', 'attachedFiles', * @return array Contains keys: 'type', 'to', 'from', 'subject','content', 'plainContent', 'attachedFiles',
* 'customHeaders', 'htmlContent', 'inlineImages' * 'customHeaders', 'htmlContent', 'inlineImages'
*/ */
public function findEmail($to, $from = null, $subject = null, $content = null) public function findEmail($to, $from = null, $subject = null, $content = null)
{ {
return $this->mailer->findEmail($to, $from, $subject, $content); /** @var Mailer $mailer */
$mailer = Injector::inst()->get(Mailer::class);
if ($mailer instanceof TestMailer) {
return $mailer->findEmail($to, $from, $subject, $content);
}
return null;
} }
/** /**
* Assert that the matching email was sent since the last call to clearEmails() * Assert that the matching email was sent since the last call to clearEmails()
* All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression. * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
* @param $to *
* @param $from * @param string $to
* @param $subject * @param string $from
* @param $content * @param string $subject
* @return array Contains the keys: 'type', 'to', 'from', 'subject', 'content', 'plainContent', 'attachedFiles', * @param string $content
* 'customHeaders', 'htmlContent', inlineImages'
*/ */
public function assertEmailSent($to, $from = null, $subject = null, $content = null) public function assertEmailSent($to, $from = null, $subject = null, $content = null)
{ {
@ -1021,186 +890,62 @@ class SapphireTest extends PHPUnit_Framework_TestCase
*/ */
public static function start() public static function start()
{ {
if (!static::is_running_test()) { if (static::is_running_test()) {
new FakeController(); return;
static::use_test_manifest(); }
// Health check
if (InjectorLoader::inst()->countManifests()) {
throw new LogicException("SapphireTest::start() cannot be called within another application");
}
static::set_is_running_test(true); static::set_is_running_test(true);
}
}
/** // Mock request
* Pushes a class and template manifest instance that include tests onto the $_SERVER['argv'] = ['vendor/bin/phpunit', '/'];
* top of the loader stacks. $request = CLIRequestBuilder::createFromEnvironment();
*/
protected static function use_test_manifest()
{
$flush = !empty($_GET['flush']);
$classManifest = new ClassManifest(
BASE_PATH,
true,
$flush
);
ClassLoader::inst()->pushManifest($classManifest, false); // Test application
static::set_test_class_manifest($classManifest); $kernel = new TestKernel(BASE_PATH);
$app = new HTTPApplication($kernel);
ThemeResourceLoader::inst()->addSet('$default', new ThemeManifest( // Custom application
BASE_PATH, $app->execute($request, function (HTTPRequest $request) {
project(), // Start session and execute
true, $request->getSession()->init();
$flush
));
// Once new class loader is registered, push a new uncached config
$config = CoreConfigFactory::inst()->createCore();
ConfigLoader::inst()->pushManifest($config);
// Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
// (e.g. Member will now have various subclasses of DataObjects that implement TestOnly) // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
DataObject::reset(); DataObject::reset();
// Set dummy controller;
$controller = Controller::create();
$controller->setRequest($request);
$controller->pushCurrent();
$controller->doInit();
}, true);
// Register state
static::$state = SapphireTestState::singleton();
// Register temp DB holder
static::$tempDB = TempDatabase::create();
} }
/** /**
* Returns true if we are currently using a temporary database * Reset the testing database's schema, but only if it is active
*/
public static function using_temp_db()
{
$dbConn = DB::get_conn();
$prefix = getenv('SS_DATABASE_PREFIX') ?: 'ss_';
return 1 === preg_match(sprintf('/^%stmpdb_[0-9]+_[0-9]+$/i', preg_quote($prefix, '/')), $dbConn->getSelectedDatabase());
}
public static function kill_temp_db()
{
// Delete our temporary database
if (self::using_temp_db()) {
$dbConn = DB::get_conn();
$dbName = $dbConn->getSelectedDatabase();
if ($dbName && DB::get_conn()->databaseExists($dbName)) {
// Some DataExtensions keep a static cache of information that needs to
// be reset whenever the database is killed
foreach (ClassInfo::subclassesFor(DataExtension::class) as $class) {
$toCall = array($class, 'on_db_reset');
if (is_callable($toCall)) {
call_user_func($toCall);
}
}
// echo "Deleted temp database " . $dbConn->currentDatabase() . "\n";
$dbConn->dropSelectedDatabase();
}
}
}
/**
* Remove all content from the temporary database.
*/
public static function empty_temp_db()
{
if (self::using_temp_db()) {
DB::get_conn()->clearAllData();
// Some DataExtensions keep a static cache of information that needs to
// be reset whenever the database is cleaned out
$classes = array_merge(ClassInfo::subclassesFor(DataExtension::class), ClassInfo::subclassesFor(DataObject::class));
foreach ($classes as $class) {
$toCall = array($class, 'on_db_reset');
if (is_callable($toCall)) {
call_user_func($toCall);
}
}
}
}
public static function create_temp_db()
{
// Disable PHPUnit error handling
$oldErrorHandler = set_error_handler(null);
// Create a temporary database, and force the connection to use UTC for time
global $databaseConfig;
$databaseConfig['timezone'] = '+0:00';
DB::connect($databaseConfig);
$dbConn = DB::get_conn();
$prefix = getenv('SS_DATABASE_PREFIX') ?: 'ss_';
do {
$dbname = strtolower(sprintf('%stmpdb_%s_%s', $prefix, time(), rand(1000000, 9999999)));
} while ($dbConn->databaseExists($dbname));
$dbConn->selectDatabase($dbname, true);
static::resetDBSchema();
// Reinstate PHPUnit error handling
set_error_handler($oldErrorHandler);
// Ensure test db is killed on exit
register_shutdown_function(function () {
static::kill_temp_db();
});
return $dbname;
}
public static function delete_all_temp_dbs()
{
$prefix = getenv('SS_DATABASE_PREFIX') ?: 'ss_';
foreach (DB::get_schema()->databaseList() as $dbName) {
if (1 === preg_match(sprintf('/^%stmpdb_[0-9]+_[0-9]+$/i', preg_quote($prefix, '/')), $dbName)) {
DB::get_schema()->dropDatabase($dbName);
if (Director::is_cli()) {
echo "Dropped database \"$dbName\"" . PHP_EOL;
} else {
echo "<li>Dropped database \"$dbName\"</li>" . PHP_EOL;
}
flush();
}
}
}
/**
* Reset the testing database's schema.
* @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included
* @param bool $forceCreate Force DB to be created if it doesn't exist
*/ */
public static function resetDBSchema($includeExtraDataObjects = false) public static function resetDBSchema($includeExtraDataObjects = false, $forceCreate = false)
{ {
if (self::using_temp_db()) { // Check if DB is active before reset
DataObject::reset(); if (!static::$tempDB->isUsed()) {
if (!$forceCreate) {
// clear singletons, they're caching old extension info which is used in DatabaseAdmin->doBuild() return;
Injector::inst()->unregisterAllObjects();
$dataClasses = ClassInfo::subclassesFor(DataObject::class);
array_shift($dataClasses);
DB::quiet();
$schema = DB::get_schema();
$extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : null;
$schema->schemaUpdate(function () use ($dataClasses, $extraDataObjects) {
foreach ($dataClasses as $dataClass) {
// Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
if (class_exists($dataClass)) {
$SNG = singleton($dataClass);
if (!($SNG instanceof TestOnly)) {
$SNG->requireTable();
} }
static::$tempDB->build();
} }
} $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : [];
static::$tempDB->resetDBSchema((array)$extraDataObjects);
// If we have additional dataobjects which need schema, do so here:
if ($extraDataObjects) {
foreach ($extraDataObjects as $dataClass) {
$SNG = singleton($dataClass);
if (singleton($dataClass) instanceof DataObject) {
$SNG->requireTable();
}
}
}
});
ClassInfo::reset_db_cache();
DataObject::singleton()->flushCache();
}
} }
/** /**
@ -1275,7 +1020,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase
*/ */
public function logOut() public function logOut()
{ {
Injector::inst()->get(IdentityStore::class)->logOut(); /** @var IdentityStore $store */
$store = Injector::inst()->get(IdentityStore::class);
$store->logOut();
} }
/** /**
@ -1283,7 +1030,6 @@ class SapphireTest extends PHPUnit_Framework_TestCase
*/ */
protected $cache_generatedMembers = array(); protected $cache_generatedMembers = array();
/** /**
* Test against a theme. * Test against a theme.
* *
@ -1295,25 +1041,16 @@ class SapphireTest extends PHPUnit_Framework_TestCase
protected function useTestTheme($themeBaseDir, $theme, $callback) protected function useTestTheme($themeBaseDir, $theme, $callback)
{ {
Config::nest(); Config::nest();
if (strpos($themeBaseDir, BASE_PATH) === 0) { if (strpos($themeBaseDir, BASE_PATH) === 0) {
$themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH)); $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
} }
SSViewer::config()->update('theme_enabled', true); SSViewer::config()->update('theme_enabled', true);
SSViewer::set_themes([$themeBaseDir.'/themes/'.$theme, '$default']); SSViewer::set_themes([$themeBaseDir.'/themes/'.$theme, '$default']);
$e = null;
try { try {
$callback(); $callback();
} catch (Exception $e) { } finally {
/* NOP for now, just save $e */
}
Config::unnest(); Config::unnest();
if ($e) {
throw $e;
} }
} }
@ -1340,7 +1077,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase
* Return all extra objects to scaffold for this test * Return all extra objects to scaffold for this test
* @return array * @return array
*/ */
protected static function getExtraDataObjects() public static function getExtraDataObjects()
{ {
return static::$extra_dataobjects; return static::$extra_dataobjects;
} }
@ -1350,7 +1087,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase
* *
* @return array * @return array
*/ */
protected static function getExtraControllers() public static function getExtraControllers()
{ {
return static::$extra_controllers; return static::$extra_controllers;
} }

View File

@ -0,0 +1,124 @@
<?php
namespace SilverStripe\Dev\State;
use LogicException;
use SilverStripe\Core\Extension;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Debug;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\DataObject;
/**
* Manages illegal and required extensions for sapphiretest
*/
class ExtensionTestState implements TestState
{
/**
* @var array
*/
protected $extensionsToReapply = [];
/**
* @var array
*/
protected $extensionsToRemove = [];
/**
* Called on setup
*
* @param SapphireTest $test
*/
public function setUp(SapphireTest $test)
{
}
public function tearDown(SapphireTest $test)
{
}
public function setUpOnce($class)
{
// May be altered by another class
$isAltered = false;
$this->extensionsToReapply = [];
$this->extensionsToRemove = [];
/** @var string|SapphireTest $class */
/** @var string|DataObject $dataClass */
// Remove any illegal extensions that are present
foreach ($class::getIllegalExtensions() as $dataClass => $extensions) {
if (!class_exists($dataClass)) {
continue;
}
if ($extensions === '*') {
$extensions = $dataClass::get_extensions();
}
foreach ($extensions as $extension) {
if (!class_exists($extension) || !$dataClass::has_extension($extension)) {
continue;
}
if (!isset($this->extensionsToReapply[$dataClass])) {
$this->extensionsToReapply[$dataClass] = [];
}
$this->extensionsToReapply[$dataClass][] = $extension;
$dataClass::remove_extension($extension);
$isAltered = true;
}
}
// Add any required extensions that aren't present
foreach ($class::getRequiredExtensions() as $dataClass => $extensions) {
if (!class_exists($dataClass)) {
throw new LogicException("Test {$class} requires dataClass {$dataClass} which doesn't exist");
}
foreach ($extensions as $extension) {
$extension = Extension::get_classname_without_arguments($extension);
if (!class_exists($extension)) {
throw new LogicException("Test {$class} requires extension {$extension} which doesn't exist");
}
if (!$dataClass::has_extension($extension)) {
if (!isset($this->extensionsToRemove[$dataClass])) {
$this->extensionsToRemove[$dataClass] = [];
}
$this->extensionsToRemove[$dataClass][] = $extension;
$dataClass::add_extension($extension);
$isAltered = true;
}
}
}
// clear singletons, they're caching old extension info
// which is used in DatabaseAdmin->doBuild()
Injector::inst()->unregisterObjects(DataObject::class);
// If we have altered the schema, but SapphireTest::setUpBeforeClass() would not otherwise
// reset the schema (if there were extra objects) then force a reset
if ($isAltered && empty($class::getExtraDataObjects())) {
DataObject::reset();
$class::resetDBSchema(true, true);
}
}
public function tearDownOnce($class)
{
// @todo: This isn't strictly necessary to restore extensions, but only to ensure that
// Object::$extra_methods is properly flushed. This should be replaced with a simple
// flush mechanism for each $class.
/** @var string|DataObject $dataClass */
// Remove extensions added for testing
foreach ($this->extensionsToRemove as $dataClass => $extensions) {
foreach ($extensions as $extension) {
$dataClass::remove_extension($extension);
}
}
// Reapply ones removed
foreach ($this->extensionsToReapply as $dataClass => $extensions) {
foreach ($extensions as $extension) {
$dataClass::add_extension($extension);
}
}
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace SilverStripe\Dev\State;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Flushable;
use SilverStripe\Core\Resettable;
use SilverStripe\Dev\SapphireTest;
/**
* Clears flushable / resettable objects
*/
class FlushableTestState implements TestState
{
/**
* @var bool
*/
protected $flushed = false;
public function setUp(SapphireTest $test)
{
// Reset all resettables
/** @var Resettable $resettable */
foreach (ClassInfo::implementorsOf(Resettable::class) as $resettable) {
$resettable::reset();
}
}
public function tearDown(SapphireTest $test)
{
}
public function setUpOnce($class)
{
if ($this->flushed) {
return;
}
$this->flushed = true;
// Flush all flushable records
/** @var Flushable $class */
foreach (ClassInfo::implementorsOf(Flushable::class) as $class) {
$class::flush();
}
}
public function tearDownOnce($class)
{
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace SilverStripe\Dev\State;
use SilverStripe\Control\Director;
use SilverStripe\Core\Environment;
use SilverStripe\Dev\SapphireTest;
/**
* Cleans up and reset global env vars between tests
*/
class GlobalsTestState implements TestState
{
/**
* Var backed up for the class
* @var array
*/
protected $staticVars = [];
/**
* Vars backed up for the test
* @var array
*/
protected $vars = [];
public function setUp(SapphireTest $test)
{
$this->vars = Environment::getVariables();
}
public function tearDown(SapphireTest $test)
{
Environment::setVariables($this->vars);
}
public function setUpOnce($class)
{
$this->staticVars = Environment::getVariables();
}
public function tearDownOnce($class)
{
Environment::setVariables($this->staticVars);
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace SilverStripe\Dev\State;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Kernel;
use SilverStripe\Dev\TestKernel;
use SilverStripe\Dev\SapphireTest;
/**
* Handles nesting of kernel before / after tests
*/
class KernelTestState implements TestState
{
/**
* Stack of kernels
*
* @var TestKernel[]
*/
protected $kernels = [];
/**
* Get active Kernel instance
*
* @return \SilverStripe\Dev\TestKernel
*/
protected function kernel()
{
return end($this->kernels);
}
/**
* Called on setup
*
* @param SapphireTest $test
*/
public function setUp(SapphireTest $test)
{
$this->nest();
}
/**
* Called on tear down
*
* @param SapphireTest $test
*/
public function tearDown(SapphireTest $test)
{
$this->unnest();
}
/**
* Called once on setup
*
* @param string $class Class being setup
*/
public function setUpOnce($class)
{
// If first run, get initial kernel
if (empty($this->kernels)) {
$this->kernels[] = Injector::inst()->get(Kernel::class);
}
$this->nest();
}
/**
* Called once on tear down
*
* @param string $class Class being torn down
*/
public function tearDownOnce($class)
{
$this->unnest();
}
/**
* Nest the current kernel
*/
protected function nest()
{
// Reset state
$this->kernel()->reset();
$this->kernels[] = $this->kernel()->nest();
}
protected function unnest()
{
// Unnest and reset
array_pop($this->kernels);
$this->kernel()->activate();
$this->kernel()->reset();
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace SilverStripe\Dev\State;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Dev\Debug;
use SilverStripe\Dev\SapphireTest;
class SapphireTestState implements TestState
{
use Injectable;
/**
* @var TestState[]
*/
protected $states = [];
/**
* @return TestState[]
*/
public function getStates()
{
return $this->states;
}
/**
* @param TestState[] $states
* @return $this
*/
public function setStates(array $states)
{
$this->states = $states;
return $this;
}
public function setUp(SapphireTest $test)
{
foreach ($this->states as $state) {
$state->setUp($test);
}
}
public function tearDown(SapphireTest $test)
{
// Tear down in reverse order
/** @var TestState $state */
foreach (array_reverse($this->states) as $state) {
$state->tearDown($test);
}
}
public function setUpOnce($class)
{
foreach ($this->states as $state) {
$state->setUpOnce($class);
}
}
/**
* Called once on tear down
*
* @param string $class Class being torn down
*/
public function tearDownOnce($class)
{
// Tear down in reverse order
/** @var TestState $state */
foreach (array_reverse($this->states) as $state) {
$state->tearDownOnce($class);
}
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace SilverStripe\Dev\State;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Dev\TestOnly;
/**
* Helper for resetting, booting, or cleaning up test state.
*
* SapphireTest will detect all implementors of this interface during test execution
*/
interface TestState extends TestOnly
{
/**
* Called on setup
*
* @param SapphireTest $test
*/
public function setUp(SapphireTest $test);
/**
* Called on tear down
*
* @param SapphireTest $test
*/
public function tearDown(SapphireTest $test);
/**
* Called once on setup
*
* @param string $class Class being setup
*/
public function setUpOnce($class);
/**
* Called once on tear down
*
* @param string $class Class being torn down
*/
public function tearDownOnce($class);
}

View File

@ -29,12 +29,12 @@ class TaskRunner extends Controller
{ {
parent::init(); parent::init();
$isRunningTests = (class_exists('SilverStripe\\Dev\\SapphireTest', false) && SapphireTest::is_running_test()); $allowAllCLI = DevelopmentAdmin::config()->get('allow_all_cli');
$canAccess = ( $canAccess = (
Director::isDev() Director::isDev()
// We need to ensure that DevelopmentAdminTest can simulate permission failures when running // We need to ensure that DevelopmentAdminTest can simulate permission failures when running
// "dev/tasks" from CLI. // "dev/tasks" from CLI.
|| (Director::is_cli() && !$isRunningTests) || (Director::is_cli() && $allowAllCLI)
|| Permission::check("ADMIN") || Permission::check("ADMIN")
); );
if (!$canAccess) { if (!$canAccess) {

View File

@ -3,8 +3,8 @@
namespace SilverStripe\Dev\Tasks; namespace SilverStripe\Dev\Tasks;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Dev\BuildTask; use SilverStripe\Dev\BuildTask;
use SilverStripe\ORM\Connect\TempDatabase;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
@ -31,6 +31,7 @@ class CleanupTestDatabasesTask extends BuildTask
die; die;
} }
SapphireTest::delete_all_temp_dbs(); // Delete all temp DBs
TempDatabase::create()->deleteAll();
} }
} }

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Dev\Tasks; namespace SilverStripe\Dev\Tasks;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Environment;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Debug; use SilverStripe\Dev\Debug;
use SilverStripe\Dev\BuildTask; use SilverStripe\Dev\BuildTask;
@ -40,7 +41,7 @@ class i18nTextCollectorTask extends BuildTask
*/ */
public function run($request) public function run($request)
{ {
increase_time_limit_to(); Environment::increaseTimeLimitTo();
$collector = i18nTextCollector::create($request->getVar('locale')); $collector = i18nTextCollector::create($request->getVar('locale'));
$merge = $this->getIsMerge($request); $merge = $this->getIsMerge($request);

49
src/Dev/TestKernel.php Normal file
View File

@ -0,0 +1,49 @@
<?php
namespace SilverStripe\Dev;
use SilverStripe\Core\CoreKernel;
/**
* Kernel for running unit tests
*/
class TestKernel extends CoreKernel
{
public function __construct($basePath)
{
$this->setEnvironment(self::DEV);
parent::__construct($basePath);
}
/**
* Reset kernel between tests.
* Note: this avoids resetting services (See TestState for service specific reset)
*
* @return $this
*/
public function reset()
{
$this->setEnvironment(self::DEV);
$this->bootPHP();
return $this;
}
protected function bootPHP()
{
parent::bootPHP();
// Set default timezone consistently to avoid NZ-specific dependencies
date_default_timezone_set('UTC');
}
protected function getIncludeTests()
{
return true;
}
protected function bootErrorHandling()
{
// Leave phpunit to capture errors
restore_error_handler();
}
}

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Dev; namespace SilverStripe\Dev;
use SilverStripe\Control\Cookie_Backend; use SilverStripe\Control\Cookie_Backend;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
@ -53,11 +54,14 @@ class TestSession
public function __construct() public function __construct()
{ {
$this->session = Injector::inst()->create('SilverStripe\\Control\\Session', array()); $this->session = Injector::inst()->create(Session::class, array());
$this->cookies = Injector::inst()->create('SilverStripe\\Control\\Cookie_Backend'); $this->cookies = Injector::inst()->create(Cookie_Backend::class);
$request = new HTTPRequest('GET', '/');
$request->setSession($this->session());
$this->controller = new Controller(); $this->controller = new Controller();
$this->controller->setSession($this->session); $this->controller->setRequest($request);
$this->controller->pushCurrent(); $this->controller->pushCurrent();
$this->controller->doInit();
} }
public function __destruct() public function __destruct()

View File

@ -26,6 +26,12 @@ class DefaultFormFactory implements FormFactory
$this->constructExtensions(); $this->constructExtensions();
} }
/**
* @param RequestHandler $controller
* @param string $name
* @param array $context
* @return Form
*/
public function getForm(RequestHandler $controller = null, $name = FormFactory::DEFAULT_NAME, $context = []) public function getForm(RequestHandler $controller = null, $name = FormFactory::DEFAULT_NAME, $context = [])
{ {
// Validate context // Validate context

View File

@ -2,8 +2,12 @@
namespace SilverStripe\Forms; namespace SilverStripe\Forms;
use BadMethodCallException;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HasRequestHandler; use SilverStripe\Control\HasRequestHandler;
use SilverStripe\Control\HTTP; use SilverStripe\Control\HTTP;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\NullHTTPRequest;
use SilverStripe\Control\RequestHandler; use SilverStripe\Control\RequestHandler;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
@ -333,11 +337,46 @@ class Form extends ViewableData implements HasRequestHandler
*/ */
public function clearFormState() public function clearFormState()
{ {
Session::clear("FormInfo.{$this->FormName()}.result"); $this
Session::clear("FormInfo.{$this->FormName()}.data"); ->getSession()
->clear("FormInfo.{$this->FormName()}.result")
->clear("FormInfo.{$this->FormName()}.data");
return $this; return $this;
} }
/**
* Helper to get current request for this form
*
* @return HTTPRequest
*/
protected function getRequest()
{
// Check if current request handler has a request object
$controller = $this->getController();
if ($controller && !($controller->getRequest() instanceof NullHTTPRequest)) {
return $controller->getRequest();
}
// Fall back to current controller
if (Controller::has_curr() && !(Controller::curr()->getRequest() instanceof NullHTTPRequest)) {
return Controller::curr()->getRequest();
}
return null;
}
/**
* Get session for this form
*
* @return Session
*/
protected function getSession()
{
$request = $this->getRequest();
if ($request) {
return $request->getSession();
}
throw new BadMethodCallException("Session not available in the current context");
}
/** /**
* Return any form data stored in the session * Return any form data stored in the session
* *
@ -345,7 +384,7 @@ class Form extends ViewableData implements HasRequestHandler
*/ */
public function getSessionData() public function getSessionData()
{ {
return Session::get("FormInfo.{$this->FormName()}.data"); return $this->getSession()->get("FormInfo.{$this->FormName()}.data");
} }
/** /**
@ -356,7 +395,7 @@ class Form extends ViewableData implements HasRequestHandler
*/ */
public function setSessionData($data) public function setSessionData($data)
{ {
Session::set("FormInfo.{$this->FormName()}.data", $data); $this->getSession()->set("FormInfo.{$this->FormName()}.data", $data);
return $this; return $this;
} }
@ -367,7 +406,7 @@ class Form extends ViewableData implements HasRequestHandler
*/ */
public function getSessionValidationResult() public function getSessionValidationResult()
{ {
$resultData = Session::get("FormInfo.{$this->FormName()}.result"); $resultData = $this->getSession()->get("FormInfo.{$this->FormName()}.result");
if (isset($resultData)) { if (isset($resultData)) {
return unserialize($resultData); return unserialize($resultData);
} }
@ -396,7 +435,7 @@ class Form extends ViewableData implements HasRequestHandler
// Serialise // Serialise
$resultData = $result ? serialize($result) : null; $resultData = $result ? serialize($result) : null;
Session::set("FormInfo.{$this->FormName()}.result", $resultData); $this->getSession()->set("FormInfo.{$this->FormName()}.result", $resultData);
return $this; return $this;
} }

View File

@ -2,23 +2,21 @@
namespace SilverStripe\Forms\GridField; namespace SilverStripe\Forms\GridField;
use InvalidArgumentException;
use LogicException;
use SilverStripe\Control\HasRequestHandler; use SilverStripe\Control\HasRequestHandler;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormField;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\Control\HTTPRequest; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\Control\Session; use SilverStripe\ORM\SS_List;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Forms\FormField;
use SilverStripe\Forms\Form;
use LogicException;
use InvalidArgumentException;
use SilverStripe\View\HTML; use SilverStripe\View\HTML;
/** /**
@ -910,7 +908,7 @@ class GridField extends FormField
foreach ($data as $dataKey => $dataValue) { foreach ($data as $dataKey => $dataValue) {
if (preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) { if (preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) {
$stateChange = Session::get($matches[1]); $stateChange = $request->getSession()->get($matches[1]);
$actionName = $stateChange['actionName']; $actionName = $stateChange['actionName'];
$arguments = array(); $arguments = array();
@ -972,11 +970,10 @@ class GridField extends FormField
* @todo copy less code from RequestHandler. * @todo copy less code from RequestHandler.
* *
* @param HTTPRequest $request * @param HTTPRequest $request
* @param DataModel $model
* @return array|RequestHandler|HTTPResponse|string * @return array|RequestHandler|HTTPResponse|string
* @throws HTTPResponse_Exception * @throws HTTPResponse_Exception
*/ */
public function handleRequest(HTTPRequest $request, DataModel $model) public function handleRequest(HTTPRequest $request)
{ {
if ($this->brokenOnConstruct) { if ($this->brokenOnConstruct) {
user_error( user_error(
@ -989,7 +986,6 @@ class GridField extends FormField
} }
$this->setRequest($request); $this->setRequest($request);
$this->setDataModel($model);
$fieldData = $this->getRequest()->requestVar($this->getName()); $fieldData = $this->getRequest()->requestVar($this->getName());
@ -1038,7 +1034,7 @@ class GridField extends FormField
if ($result instanceof HasRequestHandler) { if ($result instanceof HasRequestHandler) {
$result = $result->getRequestHandler(); $result = $result->getRequestHandler();
} }
$returnValue = $result->handleRequest($request, $model); $returnValue = $result->handleRequest($request);
if (is_array($returnValue)) { if (is_array($returnValue)) {
throw new LogicException( throw new LogicException(
@ -1066,7 +1062,7 @@ class GridField extends FormField
} }
} }
return parent::handleRequest($request, $model); return parent::handleRequest($request);
} }
/** /**

View File

@ -2,17 +2,16 @@
namespace SilverStripe\Forms\GridField; namespace SilverStripe\Forms\GridField;
use Closure;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\RequestHandler; use SilverStripe\Control\RequestHandler;
use SilverStripe\Core\Extensible;
use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\DataObject;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Extensible;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Forms\Validator;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use Closure; use SilverStripe\Forms\Validator;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Filterable; use SilverStripe\ORM\Filterable;
/** /**
@ -118,7 +117,7 @@ class GridFieldDetailForm implements GridField_URLHandler
$this->setValidator($record->getCMSValidator()); $this->setValidator($record->getCMSValidator());
} }
return $handler->handleRequest($request, DataModel::inst()); return $handler->handleRequest($request);
} }
/** /**

View File

@ -4,9 +4,9 @@ namespace SilverStripe\Forms\GridField;
use SilverStripe\Admin\LeftAndMain; use SilverStripe\Admin\LeftAndMain;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;

View File

@ -134,9 +134,7 @@ class GridFieldExportButton implements GridField_HTMLProvider, GridField_ActionP
} }
/** @var GridFieldDataColumns $dataCols */ /** @var GridFieldDataColumns $dataCols */
$dataCols = $gridField->getConfig()->getComponentByType( $dataCols = $gridField->getConfig()->getComponentByType(GridFieldDataColumns::class);
'SilverStripe\\Forms\\GridField\\GridFieldDataColumns'
);
if ($dataCols) { if ($dataCols) {
return $dataCols->getDisplayFields($gridField); return $dataCols->getDisplayFields($gridField);
} }

View File

@ -2,7 +2,7 @@
namespace SilverStripe\Forms\GridField; namespace SilverStripe\Forms\GridField;
use SilverStripe\Control\Session; use SilverStripe\Control\Controller;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
@ -89,7 +89,9 @@ class GridField_FormAction extends FormAction
// Ensure $id doesn't contain only numeric characters // Ensure $id doesn't contain only numeric characters
$id = 'gf_' . substr(md5(serialize($state)), 0, 8); $id = 'gf_' . substr(md5(serialize($state)), 0, 8);
Session::set($id, $state);
$session = Controller::curr()->getRequest()->getSession();
$session->set($id, $state);
$actionData['StateID'] = $id; $actionData['StateID'] = $id;
return array_merge( return array_merge(

View File

@ -365,7 +365,7 @@ class TinyMCEConfig extends HTMLEditorConfig
/** /**
* Enable one or several plugins. Will properly handle being passed a plugin that is already disabled * Enable one or several plugins. Will properly handle being passed a plugin that is already disabled
* @param string $plugin,... a string, or several strings, or a single array of strings - The plugins to enable * @param string|array $plugin,... a string, or several strings, or a single array of strings - The plugins to enable
* @return $this * @return $this
*/ */
public function disablePlugins($plugin) public function disablePlugins($plugin)

View File

@ -24,7 +24,7 @@ class DetailedErrorFormatter implements FormatterInterface
'trace' => $exception->getTrace(), 'trace' => $exception->getTrace(),
); );
} else { } else {
$context = $record['context']; $context = isset($record['context']) ? $record['context'] : $record;
foreach (array('code','message','file','line') as $key) { foreach (array('code','message','file','line') as $key) {
if (!isset($context[$key])) { if (!isset($context[$key])) {
$context[$key] = isset($record[$key]) ? $record[$key] : null; $context[$key] = isset($record[$key]) ? $record[$key] : null;

View File

@ -0,0 +1,220 @@
<?php
namespace SilverStripe\ORM\Connect;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
class TempDatabase
{
use Injectable;
/**
* Connection name
*
* @var string
*/
protected $name = null;
/**
* Create a new temp database
*
* @param string $name DB Connection name to use
*/
public function __construct($name = 'default')
{
$this->name = $name;
}
/**
* Check if the given name matches the temp_db pattern
*
* @param string $name
* @return bool
*/
protected function isDBTemp($name)
{
$prefix = getenv('SS_DATABASE_PREFIX') ?: 'ss_';
$result = preg_match(
sprintf('/^%stmpdb_[0-9]+_[0-9]+$/i', preg_quote($prefix, '/')),
$name
);
return $result === 1;
}
/**
* @return Database
*/
protected function getConn()
{
return DB::get_conn($this->name);
}
/**
* Returns true if we are currently using a temporary database
*
* @return bool
*/
public function isUsed()
{
$selected = $this->getConn()->getSelectedDatabase();
return $this->isDBTemp($selected);
}
/**
* Destroy the current temp database
*/
public function kill()
{
// Delete our temporary database
if (!$this->isUsed()) {
return;
}
// Check the database actually exists
$dbConn = $this->getConn();
$dbName = $dbConn->getSelectedDatabase();
if (!$dbConn->databaseExists($dbName)) {
return;
}
// Some DataExtensions keep a static cache of information that needs to
// be reset whenever the database is killed
foreach (ClassInfo::subclassesFor(DataExtension::class) as $class) {
$toCall = array($class, 'on_db_reset');
if (is_callable($toCall)) {
call_user_func($toCall);
}
}
// echo "Deleted temp database " . $dbConn->currentDatabase() . "\n";
$dbConn->dropSelectedDatabase();
}
/**
* Remove all content from the temporary database.
*/
public function clearAllData()
{
if (!$this->isUsed()) {
return;
}
$this->getConn()->clearAllData();
// Some DataExtensions keep a static cache of information that needs to
// be reset whenever the database is cleaned out
$classes = array_merge(
ClassInfo::subclassesFor(DataExtension::class),
ClassInfo::subclassesFor(DataObject::class)
);
foreach ($classes as $class) {
$toCall = array($class, 'on_db_reset');
if (is_callable($toCall)) {
call_user_func($toCall);
}
}
}
/**
* Create temp DB without creating extra objects
*
* @return string DB name
*/
public function build()
{
// Disable PHPUnit error handling
$oldErrorHandler = set_error_handler(null);
// Create a temporary database, and force the connection to use UTC for time
$dbConn = $this->getConn();
$prefix = getenv('SS_DATABASE_PREFIX') ?: 'ss_';
do {
$dbname = strtolower(sprintf('%stmpdb_%s_%s', $prefix, time(), rand(1000000, 9999999)));
} while ($dbConn->databaseExists($dbname));
$dbConn->selectDatabase($dbname, true);
$this->resetDBSchema();
// Reinstate PHPUnit error handling
set_error_handler($oldErrorHandler);
// Ensure test db is killed on exit
register_shutdown_function(function () {
$this->kill();
});
return $dbname;
}
/**
* Clear all temp DBs on this connection
*
* Note: This will output results to stdout unless suppressOutput
* is set on the current db schema
*/
public function deleteAll()
{
$schema = $this->getConn()->getSchemaManager();
foreach ($schema->databaseList() as $dbName) {
if ($this->isDBTemp($dbName)) {
$schema->dropDatabase($dbName);
$schema->alterationMessage("Dropped database \"$dbName\"", 'deleted');
flush();
}
}
}
/**
* Reset the testing database's schema.
*
* @param array $extraDataObjects List of extra dataobjects to build
*/
public function resetDBSchema(array $extraDataObjects = [])
{
if (!$this->isUsed()) {
return;
}
DataObject::reset();
// clear singletons, they're caching old extension info which is used in DatabaseAdmin->doBuild()
Injector::inst()->unregisterObjects(DataObject::class);
$dataClasses = ClassInfo::subclassesFor(DataObject::class);
array_shift($dataClasses);
$schema = $this->getConn()->getSchemaManager();
$schema->quiet();
$schema->schemaUpdate(function () use ($dataClasses, $extraDataObjects) {
foreach ($dataClasses as $dataClass) {
// Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
if (class_exists($dataClass)) {
$SNG = singleton($dataClass);
if (!($SNG instanceof TestOnly)) {
$SNG->requireTable();
}
}
}
// If we have additional dataobjects which need schema, do so here:
if ($extraDataObjects) {
foreach ($extraDataObjects as $dataClass) {
$SNG = singleton($dataClass);
if (singleton($dataClass) instanceof DataObject) {
$SNG->requireTable();
}
}
}
});
ClassInfo::reset_db_cache();
DataObject::singleton()->flushCache();
}
}

View File

@ -2,19 +2,19 @@
namespace SilverStripe\ORM; namespace SilverStripe\ORM;
use InvalidArgumentException;
use LogicException;
use SilverStripe\Control\Cookie;
use SilverStripe\Control\Director;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Control\Director;
use SilverStripe\Control\Cookie;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\Connect\Database;
use SilverStripe\ORM\Connect\DBConnector; use SilverStripe\ORM\Connect\DBConnector;
use SilverStripe\ORM\Connect\DBSchemaManager; use SilverStripe\ORM\Connect\DBSchemaManager;
use SilverStripe\ORM\Connect\Query; use SilverStripe\ORM\Connect\Query;
use SilverStripe\ORM\Queries\SQLExpression; use SilverStripe\ORM\Queries\SQLExpression;
use SilverStripe\ORM\Connect\Database;
use InvalidArgumentException;
use LogicException;
/** /**
* Global database interface, complete with static methods. * Global database interface, complete with static methods.
@ -34,9 +34,19 @@ class DB
/** /**
* The global database connection. * The global database connection.
*
* @var Database * @var Database
*/ */
private static $connections = array(); protected static $connections = [];
/**
* List of configurations for each connection
*
* @var array List of configs each in the $databaseConfig format
*/
protected static $configs = [];
/** /**
* The last SQL query run. * The last SQL query run.
@ -77,6 +87,13 @@ class DB
if (isset(self::$connections[$name])) { if (isset(self::$connections[$name])) {
return self::$connections[$name]; return self::$connections[$name];
} }
// lazy connect
$config = static::getConfig($name);
if ($config) {
return static::connect($config, $name);
}
return null; return null;
} }
@ -247,7 +264,7 @@ class DB
} }
/** /**
* Connect to a database. * Specify connection to a database
* *
* Given the database configuration, this method will create the correct * Given the database configuration, this method will create the correct
* subclass of {@link SS_Database}. * subclass of {@link SS_Database}.
@ -259,14 +276,13 @@ class DB
*/ */
public static function connect($databaseConfig, $label = 'default') public static function connect($databaseConfig, $label = 'default')
{ {
// This is used by the "testsession" module to test up a test session using an alternative name // This is used by the "testsession" module to test up a test session using an alternative name
if ($name = self::get_alternative_database_name()) { if ($name = self::get_alternative_database_name()) {
$databaseConfig['database'] = $name; $databaseConfig['database'] = $name;
} }
if (!isset($databaseConfig['type']) || empty($databaseConfig['type'])) { if (!isset($databaseConfig['type']) || empty($databaseConfig['type'])) {
user_error("DB::connect: Not passed a valid database config", E_USER_ERROR); throw new InvalidArgumentException("DB::connect: Not passed a valid database config");
} }
self::$connection_attempted = true; self::$connection_attempted = true;
@ -283,6 +299,30 @@ class DB
return $conn; return $conn;
} }
/**
* Set config for a lazy-connected database
*
* @param array $databaseConfig
* @param string $name
*/
public static function setConfig($databaseConfig, $name = 'default')
{
static::$configs[$name] = $databaseConfig;
}
/**
* Get the named connection config
*
* @param string $name
* @return mixed
*/
public static function getConfig($name = 'default')
{
if (isset(static::$configs[$name])) {
return static::$configs[$name];
}
}
/** /**
* Returns true if a database connection has been attempted. * Returns true if a database connection has been attempted.
* In particular, it lets the caller know if we're still so early in the execution pipeline that * In particular, it lets the caller know if we're still so early in the execution pipeline that

View File

@ -49,13 +49,6 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
*/ */
protected $dataQuery; protected $dataQuery;
/**
* The DataModel from which this DataList comes.
*
* @var DataModel
*/
protected $model;
/** /**
* Create a new DataList. * Create a new DataList.
* No querying is done on construction, but the initial query schema is set up. * No querying is done on construction, but the initial query schema is set up.
@ -70,16 +63,6 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
parent::__construct(); parent::__construct();
} }
/**
* Set the DataModel
*
* @param DataModel $model
*/
public function setDataModel(DataModel $model)
{
$this->model = $model;
}
/** /**
* Get the dataClass name for this DataList, ie the DataObject ClassName * Get the dataClass name for this DataList, ie the DataObject ClassName
* *
@ -796,7 +779,7 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
$class = $row['RecordClassName']; $class = $row['RecordClassName'];
} }
$item = Injector::inst()->create($class, $row, false, $this->model, $this->getQueryParams()); $item = Injector::inst()->create($class, $row, false, $this->getQueryParams());
return $item; return $item;
} }
@ -1119,7 +1102,7 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
public function newObject($initialFields = null) public function newObject($initialFields = null)
{ {
$class = $this->dataClass; $class = $this->dataClass;
return Injector::inst()->create($class, $initialFields, false, $this->model); return Injector::inst()->create($class, $initialFields, false);
} }
/** /**

View File

@ -1,80 +0,0 @@
<?php
namespace SilverStripe\ORM;
/**
* Representation of a DataModel - a collection of DataLists for each different
* data type.
*
* Usage:
* <code>
* $model = new DataModel;
* $mainMenu = $model->SiteTree->where('"ParentID" = 0 AND "ShowInMenus" = 1');
* </code>
*/
class DataModel
{
/**
* @var DataModel
*/
protected static $inst;
/**
* @var array $customDataLists
*/
protected $customDataLists = array();
/**
* Get the global DataModel.
*
* @return DataModel
*/
public static function inst()
{
if (!self::$inst) {
self::$inst = new self;
}
return self::$inst;
}
/**
* Set the global DataModel, used when data is requested from static
* methods.
*
* @param DataModel $inst
*/
public static function set_inst(DataModel $inst)
{
self::$inst = $inst;
}
/**
* @param string
*
* @return DataList
*/
public function __get($class)
{
if (isset($this->customDataLists[$class])) {
return clone $this->customDataLists[$class];
} else {
$list = DataList::create($class);
$list->setDataModel($this);
return $list;
}
}
/**
* @param string $class
* @param DataList $item
*/
public function __set($class, $item)
{
$item = clone $item;
$item->setDataModel($this);
$this->customDataLists[$class] = $item;
}
}

View File

@ -141,11 +141,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/ */
public $destroyed = false; public $destroyed = false;
/**
* The DataModel from this this object comes
*/
protected $model;
/** /**
* Data stored in this objects database record. An array indexed by fieldname. * Data stored in this objects database record. An array indexed by fieldname.
* *
@ -289,10 +284,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* for populating data on new records. * for populating data on new records.
* @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods. * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
* Singletons don't have their defaults set. * Singletons don't have their defaults set.
* @param DataModel $model
* @param array $queryParams List of DataQuery params necessary to lazy load, or load related objects. * @param array $queryParams List of DataQuery params necessary to lazy load, or load related objects.
*/ */
public function __construct($record = null, $isSingleton = false, $model = null, $queryParams = array()) public function __construct($record = null, $isSingleton = false, $queryParams = array())
{ {
parent::__construct(); parent::__construct();
@ -365,10 +359,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
HTTP::register_modification_date($record['LastEdited']); HTTP::register_modification_date($record['LastEdited']);
} }
// this must be called before populateDefaults(), as field getters on a DataObject
// may call getComponent() and others, which rely on $this->model being set.
$this->model = $model ? $model : DataModel::inst();
// Must be called after parent constructor // Must be called after parent constructor
if (!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) { if (!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
$this->populateDefaults(); $this->populateDefaults();
@ -378,17 +368,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->changed = array(); $this->changed = array();
} }
/**
* Set the DataModel
* @param DataModel $model
* @return DataObject $this
*/
public function setDataModel(DataModel $model)
{
$this->model = $model;
return $this;
}
/** /**
* Destroy all of this objects dependant objects and local caches. * Destroy all of this objects dependant objects and local caches.
* You'll need to call this to get the memory of an object that has components or extensions freed. * You'll need to call this to get the memory of an object that has components or extensions freed.
@ -415,7 +394,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$map = $this->toMap(); $map = $this->toMap();
unset($map['Created']); unset($map['Created']);
/** @var static $clone */ /** @var static $clone */
$clone = Injector::inst()->create(static::class, $map, false, $this->model); $clone = Injector::inst()->create(static::class, $map, false, $this->getSourceQueryParams());
$clone->ID = 0; $clone->ID = 0;
$clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite, $manyMany); $clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite, $manyMany);
@ -549,7 +528,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$originalClass = $this->ClassName; $originalClass = $this->ClassName;
/** @var DataObject $newInstance */ /** @var DataObject $newInstance */
$newInstance = Injector::inst()->create($newClassName, $this->record, false, $this->model); $newInstance = Injector::inst()->create($newClassName, $this->record, false);
// Modify ClassName // Modify ClassName
if ($newClassName != $originalClass) { if ($newClassName != $originalClass) {
@ -1437,7 +1416,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// - move the details of the delete code in the DataQuery system // - move the details of the delete code in the DataQuery system
// - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest // - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
// obviously, that means getting requireTable() to configure cascading deletes ;-) // obviously, that means getting requireTable() to configure cascading deletes ;-)
$srcQuery = DataList::create(static::class, $this->model) $srcQuery = DataList::create(static::class)
->filter('ID', $this->ID) ->filter('ID', $this->ID)
->dataQuery() ->dataQuery()
->query(); ->query();
@ -1520,7 +1499,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
if (empty($component)) { if (empty($component)) {
$component = $this->model->$class->newObject(); $component = Injector::inst()->create($class);
} }
} elseif ($class = $schema->belongsToComponent(static::class, $componentName)) { } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
$joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic); $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
@ -1547,7 +1526,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
if (empty($component)) { if (empty($component)) {
$component = $this->model->$class->newObject(); $component = Injector::inst()->create($class);
if ($polymorphic) { if ($polymorphic) {
$component->{$joinField.'ID'} = $this->ID; $component->{$joinField.'ID'} = $this->ID;
$component->{$joinField.'Class'} = static::class; $component->{$joinField.'Class'} = static::class;
@ -1603,10 +1582,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$result = HasManyList::create($componentClass, $joinField); $result = HasManyList::create($componentClass, $joinField);
} }
if ($this->model) {
$result->setDataModel($this->model);
}
return $result return $result
->setDataQueryParam($this->getInheritableQueryParams()) ->setDataQueryParam($this->getInheritableQueryParams())
->forForeignID($this->ID); ->forForeignID($this->ID);
@ -1728,9 +1703,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$joinField = "{$remoteRelation}ID"; $joinField = "{$remoteRelation}ID";
$componentClass = $schema->classForField($remoteClass, $joinField); $componentClass = $schema->classForField($remoteClass, $joinField);
$result = HasManyList::create($componentClass, $joinField); $result = HasManyList::create($componentClass, $joinField);
if ($this->model) {
$result->setDataModel($this->model);
}
return $result return $result
->setDataQueryParam($this->getInheritableQueryParams()) ->setDataQueryParam($this->getInheritableQueryParams())
->forForeignID($this->ID); ->forForeignID($this->ID);
@ -1774,9 +1746,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$manyMany['childField'], // Reversed parent / child field $manyMany['childField'], // Reversed parent / child field
$extraFields $extraFields
); );
if ($this->model) {
$result->setDataModel($this->model);
}
$this->extend('updateManyManyComponents', $result); $this->extend('updateManyManyComponents', $result);
// If this is called on a singleton, then we return an 'orphaned relation' that can have the // If this is called on a singleton, then we return an 'orphaned relation' that can have the
@ -1835,10 +1804,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$query->setQueryParam('Component.ExtraFields', $extraFields); $query->setQueryParam('Component.ExtraFields', $extraFields);
}); });
if ($this->model) {
$result->setDataModel($this->model);
}
$this->extend('updateManyManyComponents', $result); $this->extend('updateManyManyComponents', $result);
// If this is called on a singleton, then we return an 'orphaned relation' that can have the // If this is called on a singleton, then we return an 'orphaned relation' that can have the
@ -2827,9 +2792,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
. ' arguments'); . ' arguments');
} }
$result = DataList::create(get_called_class()); return DataList::create(get_called_class());
$result->setDataModel(DataModel::inst());
return $result;
} }
if ($join) { if ($join) {
@ -2847,7 +2810,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$result = $result->limit($limit); $result = $result->limit($limit);
} }
$result->setDataModel(DataModel::inst());
return $result; return $result;
} }
@ -3150,7 +3112,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if (!$hasData) { if (!$hasData) {
$className = static::class; $className = static::class;
foreach ($defaultRecords as $record) { foreach ($defaultRecords as $record) {
$obj = $this->model->$className->newObject($record); $obj = Injector::inst()->create($className, $record);
$obj->write(); $obj->write();
} }
DB::alteration_message("Added default records to $className table", "created"); DB::alteration_message("Added default records to $className table", "created");

View File

@ -2,19 +2,17 @@
namespace SilverStripe\ORM; namespace SilverStripe\ORM;
use SilverStripe\Control\Director; use BadMethodCallException;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Environment;
use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\DevelopmentAdmin;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Versioned\Versioned;
use SilverStripe\Security\Security;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\Security\Security;
// Include the DB class use SilverStripe\Versioned\Versioned;
require_once("DB.php");
/** /**
* DatabaseAdmin class * DatabaseAdmin class
@ -58,13 +56,13 @@ class DatabaseAdmin extends Controller
// if on CLI or with the database not ready. The latter makes it less errorprone to do an // if on CLI or with the database not ready. The latter makes it less errorprone to do an
// initial schema build without requiring a default-admin login. // initial schema build without requiring a default-admin login.
// Access to this controller is always allowed in "dev-mode", or of the user is ADMIN. // Access to this controller is always allowed in "dev-mode", or of the user is ADMIN.
$isRunningTests = (class_exists('SilverStripe\\Dev\\SapphireTest', false) && SapphireTest::is_running_test()); $allowAllCLI = DevelopmentAdmin::config()->get('allow_all_cli');
$canAccess = ( $canAccess = (
Director::isDev() Director::isDev()
|| !Security::database_is_ready() || !Security::database_is_ready()
// We need to ensure that DevelopmentAdminTest can simulate permission failures when running // We need to ensure that DevelopmentAdminTest can simulate permission failures when running
// "dev/tests" from CLI. // "dev/tests" from CLI.
|| (Director::is_cli() && !$isRunningTests) || (Director::is_cli() && $allowAllCLI)
|| Permission::check("ADMIN") || Permission::check("ADMIN")
); );
if (!$canAccess) { if (!$canAccess) {
@ -87,14 +85,14 @@ class DatabaseAdmin extends Controller
$allClasses = get_declared_classes(); $allClasses = get_declared_classes();
$rootClasses = []; $rootClasses = [];
foreach ($allClasses as $class) { foreach ($allClasses as $class) {
if (get_parent_class($class) == 'SilverStripe\ORM\DataObject') { if (get_parent_class($class) == DataObject::class) {
$rootClasses[$class] = array(); $rootClasses[$class] = array();
} }
} }
// Assign every other data object one of those // Assign every other data object one of those
foreach ($allClasses as $class) { foreach ($allClasses as $class) {
if (!isset($rootClasses[$class]) && is_subclass_of($class, 'SilverStripe\ORM\DataObject')) { if (!isset($rootClasses[$class]) && is_subclass_of($class, DataObject::class)) {
foreach ($rootClasses as $rootClass => $dummy) { foreach ($rootClasses as $rootClass => $dummy) {
if (is_subclass_of($class, $rootClass)) { if (is_subclass_of($class, $rootClass)) {
$rootClasses[$rootClass][] = $class; $rootClasses[$rootClass][] = $class;
@ -122,10 +120,10 @@ class DatabaseAdmin extends Controller
public function build() public function build()
{ {
// The default time limit of 30 seconds is normally not enough // The default time limit of 30 seconds is normally not enough
increase_time_limit_to(600); Environment::increaseTimeLimitTo(600);
// Get all our classes // Get all our classes
ClassLoader::inst()->getManifest()->regenerate(); ClassLoader::inst()->getManifest()->regenerate(false);
$url = $this->getReturnURL(); $url = $this->getReturnURL();
if ($url) { if ($url) {
@ -224,17 +222,16 @@ class DatabaseAdmin extends Controller
} }
// Load parameters from existing configuration // Load parameters from existing configuration
global $databaseConfig; $databaseConfig = DB::getConfig();
if (empty($databaseConfig) && empty($_REQUEST['db'])) { if (empty($databaseConfig) && empty($_REQUEST['db'])) {
user_error("No database configuration available", E_USER_ERROR); throw new BadMethodCallException("No database configuration available");
} }
$parameters = (!empty($databaseConfig)) ? $databaseConfig : $_REQUEST['db']; $parameters = (!empty($databaseConfig)) ? $databaseConfig : $_REQUEST['db'];
// Check database name is given // Check database name is given
if (empty($parameters['database'])) { if (empty($parameters['database'])) {
user_error( throw new BadMethodCallException(
"No database name given; please give a value for \$databaseConfig['database']", "No database name given; please give a value for SS_DATABASE_NAME or set SS_DATABASE_CHOOSE_NAME"
E_USER_ERROR
); );
} }
$database = $parameters['database']; $database = $parameters['database'];
@ -260,7 +257,6 @@ class DatabaseAdmin extends Controller
// Initiate schema update // Initiate schema update
$dbSchema = DB::get_schema(); $dbSchema = DB::get_schema();
$dbSchema->schemaUpdate(function () use ($dataClasses, $testMode, $quiet) { $dbSchema->schemaUpdate(function () use ($dataClasses, $testMode, $quiet) {
/** @var SilverStripe\ORM\DataObjectSchema $dataObjectSchema */
$dataObjectSchema = DataObject::getSchema(); $dataObjectSchema = DataObject::getSchema();
foreach ($dataClasses as $dataClass) { foreach ($dataClasses as $dataClass) {

View File

@ -70,7 +70,7 @@ class ManyManyThroughList extends RelationList
if ($joinRow) { if ($joinRow) {
$joinClass = $this->manipulator->getJoinClass(); $joinClass = $this->manipulator->getJoinClass();
$joinQueryParams = $this->manipulator->extractInheritableQueryParameters($this->dataQuery); $joinQueryParams = $this->manipulator->extractInheritableQueryParameters($this->dataQuery);
$joinRecord = Injector::inst()->create($joinClass, $joinRow, false, $this->model, $joinQueryParams); $joinRecord = Injector::inst()->create($joinClass, $joinRow, false, $joinQueryParams);
$record->setJoin($joinRecord, $joinAlias); $record->setJoin($joinRecord, $joinAlias);
} }

View File

@ -6,9 +6,7 @@ use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\RequestFilter; use SilverStripe\Control\RequestFilter;
use SilverStripe\Control\Session;
use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Config\Configurable;
use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\ValidationException; use SilverStripe\ORM\ValidationException;
class AuthenticationRequestFilter implements RequestFilter class AuthenticationRequestFilter implements RequestFilter
@ -42,13 +40,15 @@ class AuthenticationRequestFilter implements RequestFilter
* Identify the current user from the request * Identify the current user from the request
* *
* @param HTTPRequest $request * @param HTTPRequest $request
* @param Session $session
* @param DataModel $model
* @return bool|void * @return bool|void
* @throws HTTPResponse_Exception * @throws HTTPResponse_Exception
*/ */
public function preRequest(HTTPRequest $request, Session $session, DataModel $model) public function preRequest(HTTPRequest $request)
{ {
if (!Security::database_is_ready()) {
return;
}
try { try {
$this $this
->getAuthenticationHandler() ->getAuthenticationHandler()
@ -66,10 +66,9 @@ class AuthenticationRequestFilter implements RequestFilter
* *
* @param HTTPRequest $request * @param HTTPRequest $request
* @param HTTPResponse $response * @param HTTPResponse $response
* @param DataModel $model
* @return bool|void * @return bool|void
*/ */
public function postRequest(HTTPRequest $request, HTTPResponse $response, DataModel $model) public function postRequest(HTTPRequest $request, HTTPResponse $response)
{ {
} }
} }

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Security; namespace SilverStripe\Security;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\MemberAuthenticator\LoginHandler; use SilverStripe\Security\MemberAuthenticator\LoginHandler;
use SilverStripe\Security\MemberAuthenticator\LogoutHandler; use SilverStripe\Security\MemberAuthenticator\LogoutHandler;
@ -105,10 +106,11 @@ interface Authenticator
* Method to authenticate an user. * Method to authenticate an user.
* *
* @param array $data Raw data to authenticate the user. * @param array $data Raw data to authenticate the user.
* @param HTTPRequest $request
* @param ValidationResult $result A validationresult which is either valid or contains the error message(s) * @param ValidationResult $result A validationresult which is either valid or contains the error message(s)
* @return Member The matched member, or null if the authentication fails * @return Member The matched member, or null if the authentication fails
*/ */
public function authenticate($data, ValidationResult &$result = null); public function authenticate(array $data, HTTPRequest $request, ValidationResult &$result = null);
/** /**
* Check if the passed password matches the stored one (if the member is not locked out). * Check if the passed password matches the stored one (if the member is not locked out).

View File

@ -8,7 +8,7 @@ use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Config\Configurable;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\Debug;
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator; use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
/** /**
@ -30,6 +30,13 @@ class BasicAuth
*/ */
private static $entire_site_protected = false; private static $entire_site_protected = false;
/**
* Set to true to ignore in CLI mode
*
* @var bool
*/
private static $ignore_cli = true;
/** /**
* @config * @config
* @var String|array Holds a {@link Permission} code that is required * @var String|array Holds a {@link Permission} code that is required
@ -65,8 +72,7 @@ class BasicAuth
$permissionCode = null, $permissionCode = null,
$tryUsingSessionLogin = true $tryUsingSessionLogin = true
) { ) {
$isRunningTests = (class_exists(SapphireTest::class, false) && SapphireTest::is_running_test()); if (!Security::database_is_ready() || (Director::is_cli() && static::config()->get('ignore_cli'))) {
if (!Security::database_is_ready() || (Director::is_cli() && !$isRunningTests)) {
return true; return true;
} }
@ -96,7 +102,7 @@ class BasicAuth
$member = $authenticator->authenticate([ $member = $authenticator->authenticate([
'Email' => $request->getHeader('PHP_AUTH_USER'), 'Email' => $request->getHeader('PHP_AUTH_USER'),
'Password' => $request->getHeader('PHP_AUTH_PW'), 'Password' => $request->getHeader('PHP_AUTH_PW'),
]); ], $request);
if ($member instanceof Member) { if ($member instanceof Member) {
break; break;
} }
@ -180,10 +186,13 @@ class BasicAuth
*/ */
public static function protect_entire_site($protect = true, $code = 'ADMIN', $message = null) public static function protect_entire_site($protect = true, $code = 'ADMIN', $message = null)
{ {
static::config()->set('entire_site_protected', $protect); static::config()
static::config()->set('entire_site_protected_code', $code); ->set('entire_site_protected', $protect)
->set('entire_site_protected_code', $code);
if ($message) {
static::config()->set('entire_site_protected_message', $message); static::config()->set('entire_site_protected_message', $message);
} }
}
/** /**
* Call {@link BasicAuth::requireLogin()} if {@link BasicAuth::protect_entire_site()} has been called. * Call {@link BasicAuth::requireLogin()} if {@link BasicAuth::protect_entire_site()} has been called.
@ -197,7 +206,6 @@ class BasicAuth
$config = static::config(); $config = static::config();
$request = Controller::curr()->getRequest(); $request = Controller::curr()->getRequest();
if ($config->get('entire_site_protected')) { if ($config->get('entire_site_protected')) {
/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
static::requireLogin( static::requireLogin(
$request, $request,
$config->get('entire_site_protected_message'), $config->get('entire_site_protected_message'),

View File

@ -187,7 +187,7 @@ PHP
$controller = $this->getResponseController(_t(__CLASS__.'.SUCCESS', 'Success')); $controller = $this->getResponseController(_t(__CLASS__.'.SUCCESS', 'Success'));
$backURLs = array( $backURLs = array(
$this->getRequest()->requestVar('BackURL'), $this->getRequest()->requestVar('BackURL'),
Session::get('BackURL'), $this->getRequest()->getSession()->get('BackURL'),
Director::absoluteURL(AdminRootController::config()->get('url_base'), true), Director::absoluteURL(AdminRootController::config()->get('url_base'), true),
); );
$backURL = null; $backURL = null;
@ -201,7 +201,7 @@ PHP
$controller = $controller->customise(array( $controller = $controller->customise(array(
'Content' => DBField::create_field(DBHTMLText::class, _t( 'Content' => DBField::create_field(DBHTMLText::class, _t(
__CLASS__.'.SUCCESSCONTENT', __CLASS__.'.SUCCESSCONTENT',
'<p>Login success. If you are not automatically redirected '. '<p>Login success. If you are not automatically redirected ' .
'<a target="_top" href="{link}">click here</a></p>', '<a target="_top" href="{link}">click here</a></p>',
'Login message displayed in the cms popup once a user has re-authenticated themselves', 'Login message displayed in the cms popup once a user has re-authenticated themselves',
array('link' => Convert::raw2att($backURL)) array('link' => Convert::raw2att($backURL))

View File

@ -18,9 +18,12 @@ class DefaultAdminService
use Injectable; use Injectable;
/** /**
* @var bool * Can be set to explicitly true or false, or left null.
* If null, hasDefaultAdmin() will be inferred from environment.
*
* @var bool|null
*/ */
protected static $has_default_admin = false; protected static $has_default_admin = null;
/** /**
* @var string * @var string
@ -72,7 +75,7 @@ class DefaultAdminService
"No default admin configured. Please call hasDefaultAdmin() before getting default admin username" "No default admin configured. Please call hasDefaultAdmin() before getting default admin username"
); );
} }
return static::$default_username; return static::$default_username ?: getenv('SS_DEFAULT_ADMIN_USERNAME');
} }
/** /**
@ -86,7 +89,7 @@ class DefaultAdminService
"No default admin configured. Please call hasDefaultAdmin() before getting default admin password" "No default admin configured. Please call hasDefaultAdmin() before getting default admin password"
); );
} }
return static::$default_password; return static::$default_password ?: getenv('SS_DEFAULT_ADMIN_PASSWORD');
} }
/** /**
@ -96,11 +99,16 @@ class DefaultAdminService
*/ */
public static function hasDefaultAdmin() public static function hasDefaultAdmin()
{ {
// Check environment if not explicitly set
if (!isset(static::$has_default_admin)) {
return !empty(getenv('SS_DEFAULT_ADMIN_USERNAME'))
&& !empty(getenv('SS_DEFAULT_ADMIN_PASSWORD'));
}
return static::$has_default_admin; return static::$has_default_admin;
} }
/** /**
* Flush the default admin credentials * Flush the default admin credentials.
*/ */
public static function clearDefaultAdmin() public static function clearDefaultAdmin()
{ {

View File

@ -10,6 +10,7 @@ use SilverStripe\Control\Controller;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\Email\Email; use SilverStripe\Control\Email\Email;
use SilverStripe\Control\Email\Mailer; use SilverStripe\Control\Email\Mailer;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
@ -176,14 +177,6 @@ class Member extends DataObject
*/ */
private static $unique_identifier_field = 'Email'; private static $unique_identifier_field = 'Email';
/**
* Object for validating user's password
*
* @config
* @var PasswordValidator
*/
private static $password_validator = null;
/** /**
* @config * @config
* The number of days that a password should be valid for. * The number of days that a password should be valid for.
@ -369,23 +362,31 @@ class Member extends DataObject
/** /**
* Set a {@link PasswordValidator} object to use to validate member's passwords. * Set a {@link PasswordValidator} object to use to validate member's passwords.
* *
* @param PasswordValidator $pv * @param PasswordValidator $validator
*/ */
public static function set_password_validator($pv) public static function set_password_validator(PasswordValidator $validator = null)
{ {
self::$password_validator = $pv; // Override existing config
Config::modify()->remove(Injector::class, PasswordValidator::class);
if ($validator) {
Injector::inst()->registerService($validator, PasswordValidator::class);
} else {
Injector::inst()->unregisterNamedObject(PasswordValidator::class);
}
} }
/** /**
* Returns the current {@link PasswordValidator} * Returns the default {@link PasswordValidator}
* *
* @return PasswordValidator * @return PasswordValidator
*/ */
public static function password_validator() public static function password_validator()
{ {
return self::$password_validator; if (Injector::inst()->has(PasswordValidator::class)) {
return Injector::inst()->get(PasswordValidator::class);
}
return null;
} }
public function isPasswordExpired() public function isPasswordExpired()
{ {
@ -1605,16 +1606,17 @@ class Member extends DataObject
public function validate() public function validate()
{ {
$valid = parent::validate(); $valid = parent::validate();
$validator = static::password_validator();
if (!$this->ID || $this->isChanged('Password')) { if (!$this->ID || $this->isChanged('Password')) {
if ($this->Password && self::$password_validator) { if ($this->Password && $validator) {
$valid->combineAnd(self::$password_validator->validate($this->Password, $this)); $valid->combineAnd($validator->validate($this->Password, $this));
} }
} }
if ((!$this->ID && $this->SetPassword) || $this->isChanged('SetPassword')) { if ((!$this->ID && $this->SetPassword) || $this->isChanged('SetPassword')) {
if ($this->SetPassword && self::$password_validator) { if ($this->SetPassword && $validator) {
$valid->combineAnd(self::$password_validator->validate($this->SetPassword, $this)); $valid->combineAnd($validator->validate($this->SetPassword, $this));
} }
} }

View File

@ -28,7 +28,8 @@ class ChangePasswordForm extends Form
*/ */
public function __construct($controller, $name, $fields = null, $actions = null) public function __construct($controller, $name, $fields = null, $actions = null)
{ {
$backURL = $controller->getBackURL() ?: Session::get('BackURL'); $backURL = $controller->getBackURL()
?: $controller->getRequest()->getSession()->get('BackURL');
if (!$fields) { if (!$fields) {
$fields = $this->getFormFields(); $fields = $this->getFormFields();

View File

@ -6,7 +6,6 @@ namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\RequestHandler; use SilverStripe\Control\RequestHandler;
use SilverStripe\Control\Session;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
@ -79,7 +78,8 @@ class ChangePasswordHandler extends RequestHandler
return $this->redirect($this->link); return $this->redirect($this->link);
} }
if (Session::get('AutoLoginHash')) { $session = $this->getRequest()->getSession();
if ($session->get('AutoLoginHash')) {
$message = DBField::create_field( $message = DBField::create_field(
'HTMLFragment', 'HTMLFragment',
'<p>' . _t( '<p>' . _t(
@ -157,7 +157,7 @@ class ChangePasswordHandler extends RequestHandler
} }
// Store the hash for the change password form. Will be unset after reload within the ChangePasswordForm. // Store the hash for the change password form. Will be unset after reload within the ChangePasswordForm.
Session::set('AutoLoginHash', $member->encryptWithUserSettings($token)); $this->getRequest()->getSession()->set('AutoLoginHash', $member->encryptWithUserSettings($token));
} }
/** /**
@ -214,14 +214,15 @@ class ChangePasswordHandler extends RequestHandler
return $this->redirectBackToForm(); return $this->redirectBackToForm();
} }
$session = $this->getRequest()->getSession();
if (!$member) { if (!$member) {
if (Session::get('AutoLoginHash')) { if ($session->get('AutoLoginHash')) {
$member = Member::member_from_autologinhash(Session::get('AutoLoginHash')); $member = Member::member_from_autologinhash($session->get('AutoLoginHash'));
} }
// The user is not logged in and no valid auto login hash is available // The user is not logged in and no valid auto login hash is available
if (!$member) { if (!$member) {
Session::clear('AutoLoginHash'); $session->clear('AutoLoginHash');
return $this->redirect($this->addBackURLParam(Security::singleton()->Link('login'))); return $this->redirect($this->addBackURLParam(Security::singleton()->Link('login')));
} }
@ -278,7 +279,7 @@ class ChangePasswordHandler extends RequestHandler
} }
// TODO Add confirmation message to login redirect // TODO Add confirmation message to login redirect
Session::clear('AutoLoginHash'); $session->clear('AutoLoginHash');
// Redirect to backurl // Redirect to backurl
$backURL = $this->getBackURL(); $backURL = $this->getBackURL();

View File

@ -6,7 +6,6 @@ use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\RequestHandler; use SilverStripe\Control\RequestHandler;
use SilverStripe\Control\Session;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Authenticator; use SilverStripe\Security\Authenticator;
@ -105,17 +104,18 @@ class LoginHandler extends RequestHandler
* *
* @param array $data Submitted data * @param array $data Submitted data
* @param MemberLoginForm $form * @param MemberLoginForm $form
* @param HTTPRequest $request
* @return HTTPResponse * @return HTTPResponse
*/ */
public function doLogin($data, $form) public function doLogin($data, MemberLoginForm $form, HTTPRequest $request)
{ {
$failureMessage = null; $failureMessage = null;
$this->extend('beforeLogin'); $this->extend('beforeLogin');
// Successful login // Successful login
/** @var ValidationResult $result */ /** @var ValidationResult $result */
if ($member = $this->checkLogin($data, $result)) { if ($member = $this->checkLogin($data, $request, $result)) {
$this->performLogin($member, $data, $form->getRequestHandler()->getRequest()); $this->performLogin($member, $data, $request);
// Allow operations on the member after successful login // Allow operations on the member after successful login
$this->extend('afterLogin', $member); $this->extend('afterLogin', $member);
@ -138,8 +138,11 @@ class LoginHandler extends RequestHandler
/** @skipUpgrade */ /** @skipUpgrade */
if (array_key_exists('Email', $data)) { if (array_key_exists('Email', $data)) {
$rememberMe = (isset($data['Remember']) && Security::config()->get('autologin_enabled') === true); $rememberMe = (isset($data['Remember']) && Security::config()->get('autologin_enabled') === true);
Session::set('SessionForms.MemberLoginForm.Email', $data['Email']); $this
Session::set('SessionForms.MemberLoginForm.Remember', $rememberMe); ->getRequest()
->getSession()
->set('SessionForms.MemberLoginForm.Email', $data['Email'])
->set('SessionForms.MemberLoginForm.Remember', $rememberMe);
} }
// Fail to login redirects back to form // Fail to login redirects back to form
@ -167,8 +170,11 @@ class LoginHandler extends RequestHandler
*/ */
protected function redirectAfterSuccessfulLogin() protected function redirectAfterSuccessfulLogin()
{ {
Session::clear('SessionForms.MemberLoginForm.Email'); $this
Session::clear('SessionForms.MemberLoginForm.Remember'); ->getRequest()
->getSession()
->clear('SessionForms.MemberLoginForm.Email')
->clear('SessionForms.MemberLoginForm.Remember');
$member = Security::getCurrentUser(); $member = Security::getCurrentUser();
if ($member->isPasswordExpired()) { if ($member->isPasswordExpired()) {
@ -206,13 +212,14 @@ class LoginHandler extends RequestHandler
* Try to authenticate the user * Try to authenticate the user
* *
* @param array $data Submitted data * @param array $data Submitted data
* @param HTTPRequest $request
* @param ValidationResult $result * @param ValidationResult $result
* @return Member Returns the member object on successful authentication * @return Member Returns the member object on successful authentication
* or NULL on failure. * or NULL on failure.
*/ */
public function checkLogin($data, ValidationResult &$result = null) public function checkLogin($data, HTTPRequest $request, ValidationResult &$result = null)
{ {
$member = $this->authenticator->authenticate($data, $result); $member = $this->authenticator->authenticate($data, $request, $result);
if ($member instanceof Member) { if ($member instanceof Member) {
return $member; return $member;
} }
@ -229,11 +236,13 @@ class LoginHandler extends RequestHandler
* @return Member Returns the member object on successful authentication * @return Member Returns the member object on successful authentication
* or NULL on failure. * or NULL on failure.
*/ */
public function performLogin($member, $data, $request) public function performLogin($member, $data, HTTPRequest $request)
{ {
/** IdentityStore */ /** IdentityStore */
$rememberMe = (isset($data['Remember']) && Security::config()->get('autologin_enabled')); $rememberMe = (isset($data['Remember']) && Security::config()->get('autologin_enabled'));
Injector::inst()->get(IdentityStore::class)->logIn($member, $rememberMe, $request); /** @var IdentityStore $identityStore */
$identityStore = Injector::inst()->get(IdentityStore::class);
$identityStore->logIn($member, $rememberMe, $request);
return $member; return $member;
} }

View File

@ -3,16 +3,16 @@
namespace SilverStripe\Security\MemberAuthenticator; namespace SilverStripe\Security\MemberAuthenticator;
use InvalidArgumentException; use InvalidArgumentException;
use SilverStripe\Control\Controller; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Core\Extensible; use SilverStripe\Core\Extensible;
use SilverStripe\Dev\Debug;
use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Authenticator; use SilverStripe\Security\Authenticator;
use SilverStripe\Security\DefaultAdminService;
use SilverStripe\Security\LoginAttempt; use SilverStripe\Security\LoginAttempt;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\PasswordEncryptor; use SilverStripe\Security\PasswordEncryptor;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Security\DefaultAdminService;
/** /**
* Authenticator for the default "member" method * Authenticator for the default "member" method
@ -31,21 +31,16 @@ class MemberAuthenticator implements Authenticator
| Authenticator::RESET_PASSWORD | Authenticator::CHECK_PASSWORD; | Authenticator::RESET_PASSWORD | Authenticator::CHECK_PASSWORD;
} }
/** public function authenticate(array $data, HTTPRequest $request, ValidationResult &$result = null)
* @param array $data
* @param null|ValidationResult $result
* @return null|Member
*/
public function authenticate($data, ValidationResult &$result = null)
{ {
// Find authenticated member // Find authenticated member
$member = $this->authenticateMember($data, $result); $member = $this->authenticateMember($data, $result);
// Optionally record every login attempt as a {@link LoginAttempt} object // Optionally record every login attempt as a {@link LoginAttempt} object
$this->recordLoginAttempt($data, $member, $result->isValid()); $this->recordLoginAttempt($data, $request, $member, $result->isValid());
if ($member) { if ($member) {
Session::clear('BackURL'); $request->getSession()->clear('BackURL');
} }
return $result->isValid() ? $member : null; return $result->isValid() ? $member : null;
@ -163,10 +158,11 @@ class MemberAuthenticator implements Authenticator
* TODO We could handle this with an extension * TODO We could handle this with an extension
* *
* @param array $data * @param array $data
* @param HTTPRequest $request
* @param Member $member * @param Member $member
* @param boolean $success * @param boolean $success
*/ */
protected function recordLoginAttempt($data, $member, $success) protected function recordLoginAttempt($data, HTTPRequest $request, $member, $success)
{ {
if (!Security::config()->get('login_recording')) { if (!Security::config()->get('login_recording')) {
return; return;
@ -193,15 +189,16 @@ class MemberAuthenticator implements Authenticator
if ($member) { if ($member) {
// Audit logging hook // Audit logging hook
$attempt->MemberID = $member->ID; $attempt->MemberID = $member->ID;
$member->extend('authenticationFailed'); $member->extend('authenticationFailed', $data, $request);
} else { } else {
// Audit logging hook // Audit logging hook
Member::singleton()->extend('authenticationFailedUnknownUser', $data); Member::singleton()
->extend('authenticationFailedUnknownUser', $data, $request);
} }
} }
$attempt->Email = $email; $attempt->Email = $email;
$attempt->IP = Controller::curr()->getRequest()->getIP(); $attempt->IP = $request->getIP();
$attempt->write(); $attempt->write();
} }

View File

@ -4,7 +4,6 @@ namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\RequestHandler; use SilverStripe\Control\RequestHandler;
use SilverStripe\Control\Session;
use SilverStripe\Forms\CheckboxField; use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
@ -124,11 +123,11 @@ class MemberLoginForm extends BaseLoginForm
*/ */
protected function getFormFields() protected function getFormFields()
{ {
$request = $this->getController()->getRequest(); $request = $this->getRequest();
if ($request->getVar('BackURL')) { if ($request->getVar('BackURL')) {
$backURL = $request->getVar('BackURL'); $backURL = $request->getVar('BackURL');
} else { } else {
$backURL = Session::get('BackURL'); $backURL = $request->getSession()->get('BackURL');
} }
$label = Member::singleton()->fieldLabel(Member::config()->get('unique_identifier_field')); $label = Member::singleton()->fieldLabel(Member::config()->get('unique_identifier_field'));
@ -143,7 +142,7 @@ class MemberLoginForm extends BaseLoginForm
$emailField->setAttribute('autofocus', 'true'); $emailField->setAttribute('autofocus', 'true');
if (Security::config()->get('remember_username')) { if (Security::config()->get('remember_username')) {
$emailField->setValue(Session::get('SessionForms.MemberLoginForm.Email')); $emailField->setValue($this->getSession()->get('SessionForms.MemberLoginForm.Email'));
} else { } else {
// Some browsers won't respect this attribute unless it's added to the form // Some browsers won't respect this attribute unless it's added to the form
$this->setAttribute('autocomplete', 'off'); $this->setAttribute('autocomplete', 'off');
@ -191,11 +190,14 @@ class MemberLoginForm extends BaseLoginForm
return $actions; return $actions;
} }
public function restoreFormState() public function restoreFormState()
{ {
parent::restoreFormState(); parent::restoreFormState();
$forceMessage = Session::get('MemberLoginForm.force_message'); $session = $this->getSession();
$forceMessage = $session->get('MemberLoginForm.force_message');
if (($member = Security::getCurrentUser()) && !$forceMessage) { if (($member = Security::getCurrentUser()) && !$forceMessage) {
$message = _t( $message = _t(
'SilverStripe\\Security\\Member.LOGGEDINAS', 'SilverStripe\\Security\\Member.LOGGEDINAS',
@ -207,7 +209,7 @@ class MemberLoginForm extends BaseLoginForm
// Reset forced message // Reset forced message
if ($forceMessage) { if ($forceMessage) {
Session::set('MemberLoginForm.force_message', false); $session->set('MemberLoginForm.force_message', false);
} }
return $this; return $this;

View File

@ -2,10 +2,10 @@
namespace SilverStripe\Security\MemberAuthenticator; namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Cookie; use SilverStripe\Control\Cookie;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Security\AuthenticationHandler; use SilverStripe\Security\AuthenticationHandler;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
@ -47,7 +47,7 @@ class SessionAuthenticationHandler implements AuthenticationHandler
{ {
// If ID is a bad ID it will be treated as if the user is not logged in, rather than throwing a // If ID is a bad ID it will be treated as if the user is not logged in, rather than throwing a
// ValidationException // ValidationException
$id = Session::get($this->getSessionVariable()); $id = $request->getSession()->get($this->getSessionVariable());
if (!$id) { if (!$id) {
return null; return null;
} }
@ -64,7 +64,8 @@ class SessionAuthenticationHandler implements AuthenticationHandler
public function logIn(Member $member, $persistent = false, HTTPRequest $request = null) public function logIn(Member $member, $persistent = false, HTTPRequest $request = null)
{ {
static::regenerateSessionId(); static::regenerateSessionId();
Session::set($this->getSessionVariable(), $member->ID); $request = $request ?: Controller::curr()->getRequest();
$request->getSession()->set($this->getSessionVariable(), $member->ID);
// This lets apache rules detect whether the user has logged in // This lets apache rules detect whether the user has logged in
// @todo make this a setting on the authentication handler // @todo make this a setting on the authentication handler
@ -102,6 +103,7 @@ class SessionAuthenticationHandler implements AuthenticationHandler
*/ */
public function logOut(HTTPRequest $request = null) public function logOut(HTTPRequest $request = null)
{ {
Session::clear($this->getSessionVariable()); $request = $request ?: Controller::curr()->getRequest();
$request->getSession()->clear($this->getSessionVariable());
} }
} }

View File

@ -20,7 +20,6 @@ use SilverStripe\Dev\Deprecation;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
@ -329,7 +328,7 @@ class Security extends Controller implements TemplateGlobalProvider
{ {
self::set_ignore_disallowed_actions(true); self::set_ignore_disallowed_actions(true);
if (!$controller) { if (!$controller && Controller::has_curr()) {
$controller = Controller::curr(); $controller = Controller::curr();
} }
@ -393,7 +392,11 @@ class Security extends Controller implements TemplateGlobalProvider
} }
static::singleton()->setSessionMessage($message, ValidationResult::TYPE_WARNING); static::singleton()->setSessionMessage($message, ValidationResult::TYPE_WARNING);
$loginResponse = static::singleton()->login(); $request = new HTTPRequest('GET', '/');
if ($controller) {
$request->setSession($controller->getRequest()->getSession());
}
$loginResponse = static::singleton()->login($request);
if ($loginResponse instanceof HTTPResponse) { if ($loginResponse instanceof HTTPResponse) {
return $loginResponse; return $loginResponse;
} }
@ -409,7 +412,7 @@ class Security extends Controller implements TemplateGlobalProvider
static::singleton()->setSessionMessage($message, ValidationResult::TYPE_WARNING); static::singleton()->setSessionMessage($message, ValidationResult::TYPE_WARNING);
Session::set("BackURL", $_SERVER['REQUEST_URI']); $controller->getRequest()->getSession()->set("BackURL", $_SERVER['REQUEST_URI']);
// TODO AccessLogEntry needs an extension to handle permission denied errors // TODO AccessLogEntry needs an extension to handle permission denied errors
// Audit logging hook // Audit logging hook
@ -522,6 +525,21 @@ class Security extends Controller implements TemplateGlobalProvider
return null; return null;
} }
public function getRequest()
{
// Support Security::singleton() where a request isn't always injected
$request = parent::getRequest();
if ($request) {
return $request;
}
if (Controller::has_curr() && Controller::curr() !== $this) {
return Controller::curr()->getRequest();
}
return null;
}
/** /**
* Prepare the controller for handling the response to this request * Prepare the controller for handling the response to this request
* *
@ -546,7 +564,6 @@ class Security extends Controller implements TemplateGlobalProvider
$holderPage->ID = -1 * random_int(1, 10000000); $holderPage->ID = -1 * random_int(1, 10000000);
$controller = ModelAsController::controller_for($holderPage); $controller = ModelAsController::controller_for($holderPage);
$controller->setDataModel($this->model);
$controller->doInit(); $controller->doInit();
return $controller; return $controller;
@ -581,14 +598,15 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
protected function getSessionMessage(&$messageType = null) protected function getSessionMessage(&$messageType = null)
{ {
$message = Session::get('Security.Message.message'); $session = $this->getRequest()->getSession();
$message = $session->get('Security.Message.message');
$messageType = null; $messageType = null;
if (empty($message)) { if (empty($message)) {
return null; return null;
} }
$messageType = Session::get('Security.Message.type'); $messageType = $session->get('Security.Message.type');
$messageCast = Session::get('Security.Message.cast'); $messageCast = $session->get('Security.Message.cast');
if ($messageCast !== ValidationResult::CAST_HTML) { if ($messageCast !== ValidationResult::CAST_HTML) {
$message = Convert::raw2xml($message); $message = Convert::raw2xml($message);
} }
@ -608,9 +626,12 @@ class Security extends Controller implements TemplateGlobalProvider
$messageType = ValidationResult::TYPE_WARNING, $messageType = ValidationResult::TYPE_WARNING,
$messageCast = ValidationResult::CAST_TEXT $messageCast = ValidationResult::CAST_TEXT
) { ) {
Session::set('Security.Message.message', $message); Controller::curr()
Session::set('Security.Message.type', $messageType); ->getRequest()
Session::set('Security.Message.cast', $messageCast); ->getSession()
->set("Security.Message.message", $message)
->set("Security.Message.type", $messageType)
->set("Security.Message.cast", $messageCast);
} }
/** /**
@ -618,7 +639,10 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
public static function clearSessionMessage() public static function clearSessionMessage()
{ {
Session::clear('Security.Message'); Controller::curr()
->getRequest()
->getSession()
->clear("Security.Message");
} }
@ -634,16 +658,20 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
public function login($request = null, $service = Authenticator::LOGIN) public function login($request = null, $service = Authenticator::LOGIN)
{ {
if ($request) {
$this->setRequest($request);
} elseif ($request) {
$request = $this->getRequest();
} else {
throw new HTTPResponse_Exception("No request available", 500);
}
// Check pre-login process // Check pre-login process
if ($response = $this->preLogin()) { if ($response = $this->preLogin()) {
return $response; return $response;
} }
$authName = null; $authName = null;
if (!$request) {
$request = $this->getRequest();
}
$handlers = $this->getServiceAuthenticatorsFromRequest($service, $request); $handlers = $this->getServiceAuthenticatorsFromRequest($service, $request);
$link = $this->Link('login'); $link = $this->Link('login');
@ -798,7 +826,7 @@ class Security extends Controller implements TemplateGlobalProvider
// Process each of the handlers // Process each of the handlers
$results = array_map( $results = array_map(
function (RequestHandler $handler) { function (RequestHandler $handler) {
return $handler->handleRequest($this->getRequest(), DataModel::inst()); return $handler->handleRequest($this->getRequest());
}, },
$handlers $handlers
); );
@ -818,7 +846,7 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
protected function delegateToHandler(RequestHandler $handler, $title, array $templates = []) protected function delegateToHandler(RequestHandler $handler, $title, array $templates = [])
{ {
$result = $handler->handleRequest($this->getRequest(), DataModel::inst()); $result = $handler->handleRequest($this->getRequest());
// Return the customised controller - may be used to render a Form (e.g. login form) // Return the customised controller - may be used to render a Form (e.g. login form)
if (is_array($result)) { if (is_array($result)) {
@ -998,7 +1026,6 @@ class Security extends Controller implements TemplateGlobalProvider
DefaultAdminService::clearDefaultAdmin(); DefaultAdminService::clearDefaultAdmin();
} }
/** /**
* Set a default admin in dev-mode * Set a default admin in dev-mode
* *

View File

@ -150,7 +150,8 @@ class SecurityToken implements TemplateGlobalProvider
*/ */
public function getValue() public function getValue()
{ {
$value = Session::get($this->getName()); $session = Controller::curr()->getRequest()->getSession();
$value = $session->get($this->getName());
// only regenerate if the token isn't already set in the session // only regenerate if the token isn't already set in the session
if (!$value) { if (!$value) {
@ -166,7 +167,8 @@ class SecurityToken implements TemplateGlobalProvider
*/ */
public function setValue($val) public function setValue($val)
{ {
Session::set($this->getName(), $val); $session = Controller::curr()->getRequest()->getSession();
$session->set($this->getName(), $val);
} }
/** /**

View File

@ -0,0 +1,39 @@
<?php
namespace SilverStripe\View\Dev;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Dev\State\TestState;
use SilverStripe\View\Requirements;
use SilverStripe\View\Requirements_Backend;
/**
* Resets requirements for test state
*/
class RequirementsTestState implements TestState
{
/**
* @var Requirements_Backend
*/
protected $backend = null;
public function setUp(SapphireTest $test)
{
$this->backend = Requirements::backend();
Requirements::set_backend(Requirements_Backend::create());
}
public function tearDown(SapphireTest $test)
{
Requirements::set_backend($this->backend);
}
public function setUpOnce($class)
{
}
public function tearDownOnce($class)
{
}
}

View File

@ -2,9 +2,8 @@
namespace SilverStripe\View; namespace SilverStripe\View;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataModel;
use InvalidArgumentException; use InvalidArgumentException;
use SilverStripe\ORM\DataList;
class GenericTemplateGlobalProvider implements TemplateGlobalProvider class GenericTemplateGlobalProvider implements TemplateGlobalProvider
{ {
@ -61,8 +60,6 @@ class GenericTemplateGlobalProvider implements TemplateGlobalProvider
*/ */
public static function getDataList($className) public static function getDataList($className)
{ {
$list = new DataList($className); return DataList::create($className);
$list->setDataModel(DataModel::inst());
return $list;
} }
} }

View File

@ -2,12 +2,11 @@
namespace SilverStripe\View; namespace SilverStripe\View;
use InvalidArgumentException;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Flushable; use SilverStripe\Core\Flushable;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
use SilverStripe\Control\HTTPResponse;
use InvalidArgumentException;
/** /**
* Requirements tracker for JavaScript and CSS. * Requirements tracker for JavaScript and CSS.
@ -104,7 +103,7 @@ class Requirements implements Flushable
public static function backend() public static function backend()
{ {
if (!self::$backend) { if (!self::$backend) {
self::$backend = Injector::inst()->create('SilverStripe\\View\\Requirements_Backend'); self::$backend = Requirements_Backend::create();
} }
return self::$backend; return self::$backend;
} }

View File

@ -2,8 +2,8 @@
namespace SilverStripe\View; namespace SilverStripe\View;
use InvalidArgumentException;
use Exception; use Exception;
use InvalidArgumentException;
use SilverStripe\Assets\File; use SilverStripe\Assets\File;
use SilverStripe\Assets\Storage\GeneratedAssetHandler; use SilverStripe\Assets\Storage\GeneratedAssetHandler;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
@ -13,7 +13,6 @@ use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Dev\Debug; use SilverStripe\Dev\Debug;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
class Requirements_Backend class Requirements_Backend
@ -33,9 +32,9 @@ class Requirements_Backend
/** /**
* Whether to combine CSS and JavaScript files * Whether to combine CSS and JavaScript files
* *
* @var bool * @var bool|null
*/ */
protected $combinedFilesEnabled = true; protected $combinedFilesEnabled = null;
/** /**
* Determine if files should be combined automatically on dev mode. * Determine if files should be combined automatically on dev mode.
@ -1326,9 +1325,12 @@ class Requirements_Backend
if ($minify && !$this->minifier) { if ($minify && !$this->minifier) {
throw new Exception( throw new Exception(
sprintf( sprintf(
'Cannot minify files without a minification service defined. <<<MESSAGE
Set %s::minifyCombinedFiles to false, or inject a %s service on Cannot minify files without a minification service defined.
%s.properties.minifier', Set %s::minifyCombinedFiles to false, or inject a %s service on
%s.properties.minifier
MESSAGE
,
__CLASS__, __CLASS__,
Requirements_Minifier::class, Requirements_Minifier::class,
__CLASS__ __CLASS__
@ -1394,18 +1396,8 @@ class Requirements_Backend
*/ */
public function getCombinedFilesEnabled() public function getCombinedFilesEnabled()
{ {
if (!$this->combinedFilesEnabled) { if (isset($this->combinedFilesEnabled)) {
return false; return $this->combinedFilesEnabled;
}
// Tests should be combined
if (class_exists('SilverStripe\\Dev\\SapphireTest', false) && SapphireTest::is_running_test()) {
return true;
}
// Check if specified via querystring
if (isset($_REQUEST['combine'])) {
return true;
} }
// Non-dev sites are always combined // Non-dev sites are always combined

View File

@ -21,13 +21,6 @@ class ThemeManifest implements ThemeList
*/ */
protected $base; protected $base;
/**
* Include tests
*
* @var bool
*/
protected $tests;
/** /**
* Path to application code * Path to application code
* *
@ -56,39 +49,44 @@ class ThemeManifest implements ThemeList
*/ */
protected $themes = null; protected $themes = null;
/**
* @var CacheFactory
*/
protected $cacheFactory= null;
/** /**
* Constructs a new template manifest. The manifest is not actually built * Constructs a new template manifest. The manifest is not actually built
* or loaded from cache until needed. * or loaded from cache until needed.
* *
* @param string $base The base path. * @param string $base The base path.
* @param string $project Path to application code * @param string $project Path to application code
*
* @param bool $includeTests Include tests in the manifest.
* @param bool $forceRegen Force the manifest to be regenerated.
* @param CacheFactory $cacheFactory Cache factory to generate backend cache with * @param CacheFactory $cacheFactory Cache factory to generate backend cache with
*/ */
public function __construct( public function __construct($base, $project, CacheFactory $cacheFactory = null)
$base, {
$project,
$includeTests = false,
$forceRegen = false,
CacheFactory $cacheFactory = null
) {
$this->base = $base; $this->base = $base;
$this->tests = $includeTests;
$this->project = $project; $this->project = $project;
$this->cacheFactory = $cacheFactory;
}
/**
* @param bool $includeTests Include tests in the manifest
* @param bool $forceRegen Force the manifest to be regenerated.
*/
public function init($includeTests = false, $forceRegen = false)
{
// build cache from factory // build cache from factory
if ($cacheFactory) { if ($this->cacheFactory) {
$this->cache = $cacheFactory->create( $this->cache = $this->cacheFactory->create(
CacheInterface::class.'.thememanifest', CacheInterface::class.'.thememanifest',
[ 'namespace' => 'thememanifest' . ($includeTests ? '_tests' : '') ] [ 'namespace' => 'thememanifest' . ($includeTests ? '_tests' : '') ]
); );
} }
$this->cacheKey = $this->getCacheKey(); $this->cacheKey = $this->getCacheKey($includeTests);
if (!$forceRegen && $this->cache && ($data = $this->cache->get($this->cacheKey))) {
if ($forceRegen) { $this->themes = $data;
$this->regenerate(); } else {
$this->regenerate($includeTests);
} }
} }
@ -104,36 +102,37 @@ class ThemeManifest implements ThemeList
* Generate a unique cache key to avoid manifest cache collisions. * Generate a unique cache key to avoid manifest cache collisions.
* We compartmentalise based on the base path, the given project, and whether * We compartmentalise based on the base path, the given project, and whether
* or not we intend to include tests. * or not we intend to include tests.
*
* @param bool $includeTests
* @return string * @return string
*/ */
public function getCacheKey() public function getCacheKey($includeTests = false)
{ {
return sha1(sprintf( return sha1(sprintf(
"manifest-%s-%s-%u", "manifest-%s-%s-%u",
$this->base, $this->base,
$this->project, $this->project,
$this->tests $includeTests
)); ));
} }
public function getThemes() public function getThemes()
{ {
if ($this->themes === null) {
$this->init();
}
return $this->themes; return $this->themes;
} }
/** /**
* Regenerates the manifest by scanning the base path. * Regenerates the manifest by scanning the base path.
*
* @param bool $includeTests
*/ */
public function regenerate() public function regenerate($includeTests = false)
{ {
$finder = new ManifestFileFinder(); $finder = new ManifestFileFinder();
$finder->setOptions(array( $finder->setOptions(array(
'include_themes' => false, 'include_themes' => false,
'ignore_dirs' => array('node_modules', THEMES_DIR), 'ignore_dirs' => array('node_modules', THEMES_DIR),
'ignore_tests' => !$this->tests, 'ignore_tests' => !$includeTests,
'dir_callback' => array($this, 'handleDirectory') 'dir_callback' => array($this, 'handleDirectory')
)); ));
@ -172,16 +171,4 @@ class ThemeManifest implements ThemeList
array_push($this->themes, $path); array_push($this->themes, $path);
} }
} }
/**
* Initialise the manifest
*/
protected function init()
{
if ($this->cache && ($data = $this->cache->get($this->cacheKey))) {
$this->themes = $data;
} else {
$this->regenerate();
}
}
} }

View File

@ -3,7 +3,6 @@
namespace SilverStripe\View; namespace SilverStripe\View;
use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Dev\Debug;
/** /**
* Handles finding templates from a stack of template manifest objects. * Handles finding templates from a stack of template manifest objects.

View File

@ -385,7 +385,7 @@ class ViewableData implements IteratorAggregate
$template = new SSViewer($template); $template = new SSViewer($template);
} }
$data = ($this->customisedObject) ? $this->customisedObject : $this; $data = $this->getCustomisedObj() ?: $this;
if ($customFields instanceof ViewableData) { if ($customFields instanceof ViewableData) {
$data = $data->customise($customFields); $data = $data->customise($customFields);

View File

@ -1,172 +1,10 @@
<?php <?php
/** /**
* Configure SilverStripe from the environment variables. * @deprecated 4.0...5.0
* Usage: Put "require_once('conf/ConfigureFromEnv.php');" into your _config.php file.
* *
* If you include this file, you will be able to use the following variables to control * Placeholder empty file
* your site.
*
* Your database connection will be set up using these defines:
* - SS_DATABASE_CLASS: The database class to use, MySQLDatabase, MSSQLDatabase, etc. defaults to
* MySQLPDODatabase
* - SS_DATABASE_SERVER: The database server to use, defaulting to localhost
* - SS_DATABASE_USERNAME: The database username (mandatory)
* - SS_DATABASE_PASSWORD: The database password (mandatory)
* - SS_DATABASE_PORT: The database port
* - SS_DATABASE_SUFFIX: A suffix to add to the database name.
* - SS_DATABASE_PREFIX: A prefix to add to the database name.
* - SS_DATABASE_TIMEZONE: Set the database timezone to something other than the system timezone.
* - SS_DATABASE_MEMORY: Use in-memory state if possible. Useful for testing, currently only
* supported by the SQLite database adapter.
*
* There is one more setting that is intended to be used by people who work on SilverStripe.
* - SS_DATABASE_CHOOSE_NAME: Boolean/Int. If set, then the system will choose a default database name for you if
* one isn't give in the $database variable. The database name will be "SS_" followed by the name of the folder
* into which you have installed SilverStripe. If this is enabled, it means that the phpinstaller will work out of
* the box without the installer needing to alter any files. This helps prevent accidental changes to the
* environment.
*
* If SS_DATABASE_CHOOSE_NAME is an integer greater than one, then an ancestor folder will be used for the database
* name. This is handy for a site that's hosted from /sites/examplesite/www or /buildbot/allmodules-2.3/build. If
* it's 2, the parent folder will be chosen; if it's 3 the grandparent, and so on.
*
* You can configure the environment with this define:
*
* - SS_ENVIRONMENT_TYPE: The environment type: dev, test or live.
*
* You can configure the default admin with these defines:
*
* - SS_DEFAULT_ADMIN_USERNAME: The username of the default admin - this is a non-database user with administrative
* privileges.
* - SS_DEFAULT_ADMIN_PASSWORD: The password of the default admin.
* - SS_USE_BASIC_AUTH: Protect the site with basic auth (good for test sites)
*
* Email:
* - SS_SEND_ALL_EMAILS_TO: If you set this define, all emails will be redirected to this address.
* - SS_SEND_ALL_EMAILS_FROM: If you set this define, all emails will be send from this address.
*/ */
use SilverStripe\Dev\Deprecation;
use Monolog\Handler\StreamHandler; Deprecation::notice('5.0', 'ConfigureFromEnv.php is deprecated', Deprecation::SCOPE_GLOBAL);
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use SilverStripe\Control\Email\Email;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Install\DatabaseAdapterRegistry;
use SilverStripe\Security\BasicAuth;
use SilverStripe\Security\DefaultAdminService;
global $database;
// No database provided
if (!isset($database) || !$database) {
if (!($database = getenv('SS_DATABASE_NAME')) && $chooseName = getenv('SS_DATABASE_CHOOSE_NAME')) {
$loopCount = (int)$chooseName;
$databaseDir = BASE_PATH;
for ($i=0; $i<$loopCount-1; $i++) {
$databaseDir = dirname($databaseDir);
}
$database = getenv('SS_DATABASE_PREFIX') ?: 'SS_';
$database .= basename($databaseDir);
$database = str_replace('.', '', $database);
}
}
if ($dbUser = getenv('SS_DATABASE_USERNAME')) {
global $databaseConfig;
// Checks if the database global is defined (if present, wraps with prefix and suffix)
$databaseNameWrapper = function ($name) {
if (!$name) {
return '';
} else {
return (getenv('SS_DATABASE_PREFIX') ?: '')
. $name
. (getenv('SS_DATABASE_SUFFIX') ?: '');
}
};
/** @skipUpgrade */
$databaseConfig = array(
"type" => getenv('SS_DATABASE_CLASS') ?: 'MySQLPDODatabase',
"server" => getenv('SS_DATABASE_SERVER') ?: 'localhost',
"username" => $dbUser,
"password" => getenv('SS_DATABASE_PASSWORD'),
"database" => $databaseNameWrapper($database),
);
// Set the port if called for
if ($dbPort = getenv('SS_DATABASE_PORT')) {
$databaseConfig['port'] = $dbPort;
}
// Set the timezone if called for
if ($dbTZ = getenv('SS_DATABASE_TIMEZONE')) {
$databaseConfig['timezone'] = $dbTZ;
}
// For schema enabled drivers:
if ($dbSchema = getenv('SS_DATABASE_SCHEMA')) {
$databaseConfig["schema"] = $dbSchema;
}
// For SQlite3 memory databases (mainly for testing purposes)
if ($dbMemory = getenv('SS_DATABASE_MEMORY')) {
$databaseConfig["memory"] = $dbMemory;
}
}
if ($sendAllEmailsTo = getenv('SS_SEND_ALL_EMAILS_TO')) {
Email::config()->send_all_emails_to = $sendAllEmailsTo;
}
if ($sendAllEmailsFrom = getenv('SS_SEND_ALL_EMAILS_FROM')) {
Email::config()->send_all_emails_from = $sendAllEmailsFrom;
}
if ($defaultAdminUser = getenv('SS_DEFAULT_ADMIN_USERNAME')) {
if (!$defaultAdminPass = getenv('SS_DEFAULT_ADMIN_PASSWORD')) {
user_error(
"SS_DEFAULT_ADMIN_PASSWORD must be defined in your environment,"
. "if SS_DEFAULT_ADMIN_USERNAME is defined. See "
. "http://doc.silverstripe.org/framework/en/topics/environment-management for more information",
E_USER_ERROR
);
} else {
DefaultAdminService::setDefaultAdmin($defaultAdminUser, $defaultAdminPass);
}
}
if ($useBasicAuth = getenv('SS_USE_BASIC_AUTH')) {
BasicAuth::config()->entire_site_protected = $useBasicAuth;
}
if ($errorLog = getenv('SS_ERROR_LOG')) {
$logger = Injector::inst()->get(LoggerInterface::class);
if ($logger instanceof Logger) {
$logger->pushHandler(new StreamHandler(BASE_PATH . '/' . $errorLog, Logger::WARNING));
} else {
user_error("SS_ERROR_LOG setting only works with Monolog, you are using another logger", E_USER_WARNING);
}
}
// Allow database adapters to handle their own configuration
DatabaseAdapterRegistry::autoconfigure();
unset(
$envType,
$chooseName,
$loopCount,
$databaseDir,
$i,
$databaseNameWrapper,
$dbUser,
$dbPort,
$dbTZ,
$dbSchema,
$dbMemory,
$sendAllEmailsTo,
$sendAllEmailsFrom,
$defaultAdminUser,
$defaultAdminPass,
$useBasicAuth,
$errorLog,
$logger
);

17
src/includes/autoload.php Normal file
View File

@ -0,0 +1,17 @@
<?php
// Init composer autoload
call_user_func(function () {
$candidates = [
__DIR__ . '/../../vendor/autoload.php',
__DIR__ . '/../../../vendor/autoload.php',
getcwd() . '/vendor/autoload.php',
];
foreach ($candidates as $candidate) {
if (file_exists($candidate)) {
require_once $candidate;
return;
}
}
die("Failed to include composer's autoloader, unable to continue");
});

View File

@ -3,6 +3,7 @@
use Dotenv\Dotenv; use Dotenv\Dotenv;
use Dotenv\Exception\InvalidPathException; use Dotenv\Exception\InvalidPathException;
use SilverStripe\Control\Util\IPUtils; use SilverStripe\Control\Util\IPUtils;
use SilverStripe\Core\TempFolder;
/** /**
* This file is the Framework constants bootstrap. It will prepare some basic common constants. * This file is the Framework constants bootstrap. It will prepare some basic common constants.
@ -27,6 +28,8 @@ use SilverStripe\Control\Util\IPUtils;
* headers from the given client are trustworthy (e.g. from a reverse proxy). * headers from the given client are trustworthy (e.g. from a reverse proxy).
*/ */
require_once __DIR__ . '/functions.php';
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// ENVIRONMENT CONFIG // ENVIRONMENT CONFIG
@ -57,7 +60,8 @@ if (!defined('BASE_PATH')) {
// Allow a first class env var to be set that disables .env file loading // Allow a first class env var to be set that disables .env file loading
if (!getenv('SS_IGNORE_DOT_ENV')) { if (!getenv('SS_IGNORE_DOT_ENV')) {
foreach ([ BASE_PATH, dirname(BASE_PATH) ] as $path) { call_user_func(function () {
foreach ([BASE_PATH, dirname(BASE_PATH)] as $path) {
try { try {
(new Dotenv($path))->load(); (new Dotenv($path))->load();
} catch (InvalidPathException $e) { } catch (InvalidPathException $e) {
@ -66,6 +70,7 @@ if (!getenv('SS_IGNORE_DOT_ENV')) {
} }
break; break;
} }
});
} }
/** /**
@ -142,11 +147,7 @@ if (defined('CUSTOM_INCLUDE_PATH')) {
set_include_path(CUSTOM_INCLUDE_PATH . PATH_SEPARATOR . get_include_path()); set_include_path(CUSTOM_INCLUDE_PATH . PATH_SEPARATOR . get_include_path());
} }
/** // Define the temporary folder if it wasn't defined yet
* Define the temporary folder if it wasn't defined yet
*/
require_once __DIR__ . '/TempPath.php';
if (!defined('TEMP_FOLDER')) { if (!defined('TEMP_FOLDER')) {
define('TEMP_FOLDER', getTempFolder(BASE_PATH)); define('TEMP_FOLDER', TempFolder::getTempFolder(BASE_PATH));
} }

View File

@ -0,0 +1,68 @@
<?php
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\i18n\i18n;
///////////////////////////////////////////////////////////////////////////////
// HELPER FUNCTIONS
/**
* Creates a class instance by the "singleton" design pattern.
* It will always return the same instance for this class,
* which can be used for performance reasons and as a simple
* way to access instance methods which don't rely on instance
* data (e.g. the custom SilverStripe static handling).
*
* @param string $className
* @return mixed
*/
function singleton($className)
{
if ($className === Config::class) {
throw new InvalidArgumentException("Don't pass Config to singleton()");
}
if (!isset($className)) {
throw new InvalidArgumentException("singleton() Called without a class");
}
if (!is_string($className)) {
throw new InvalidArgumentException(
"singleton() passed bad class_name: " . var_export($className, true)
);
}
return Injector::inst()->get($className);
}
function project()
{
global $project;
// Set default project
if (empty($project) && file_exists(BASE_PATH . '/mysite')) {
$project = 'mysite';
}
return $project;
}
/**
* This is the main translator function. Returns the string defined by $entity according to the
* currently set locale.
*
* Also supports pluralisation of strings. Pass in a `count` argument, as well as a
* default value with `|` pipe-delimited options for each plural form.
*
* @param string $entity Entity that identifies the string. It must be in the form
* "Namespace.Entity" where Namespace will be usually the class name where this
* string is used and Entity identifies the string inside the namespace.
* @param mixed $arg,... Additional arguments are parsed as such:
* - Next string argument is a default. Pass in a `|` pipe-delimeted value with `{count}`
* to do pluralisation.
* - Any other string argument after default is context for i18nTextCollector
* - Any array argument in any order is an injection parameter list. Pass in a `count`
* injection parameter to pluralise.
* @return string
*/
function _t($entity, $arg = null)
{
// Pass args directly to handle deprecation
return call_user_func_array([i18n::class, '_t'], func_get_args());
}

View File

@ -2,4 +2,3 @@
require __DIR__ . '/../bootstrap/init.php'; require __DIR__ . '/../bootstrap/init.php';
require __DIR__ . '/../bootstrap/environment.php'; require __DIR__ . '/../bootstrap/environment.php';
require __DIR__ . '/../bootstrap/mysite.php';

View File

@ -3,4 +3,3 @@
require __DIR__ . '/bootstrap/init.php'; require __DIR__ . '/bootstrap/init.php';
require __DIR__ . '/bootstrap/cli.php'; require __DIR__ . '/bootstrap/cli.php';
require __DIR__ . '/bootstrap/environment.php'; require __DIR__ . '/bootstrap/environment.php';
require __DIR__ . '/bootstrap/phpunit.php';

View File

@ -1,10 +0,0 @@
<?php
// Default database settings
global $project;
$project = 'mysite';
global $database;
$database = '';
require_once('conf/ConfigureFromEnv.php');

View File

@ -1,24 +0,0 @@
<?php
// Bootstrap for running SapphireTests
// Connect to database
use SilverStripe\ORM\DB;
require_once __DIR__ . '/../../src/Core/Core.php';
require_once __DIR__ . '/../php/Control/FakeController.php';
// Bootstrap a mock project configuration
require __DIR__ . '/mysite.php';
global $databaseConfig;
DB::connect($databaseConfig);
// Now set a fake REQUEST_URI
$_SERVER['REQUEST_URI'] = BASE_URL;
// Fake a session
$_SESSION = null;
// Remove the error handler so that PHPUnit can add its own
restore_error_handler();

View File

@ -3,8 +3,8 @@
namespace SilverStripe\Control\Tests; namespace SilverStripe\Control\Tests;
use InvalidArgumentException; use InvalidArgumentException;
use PHPUnit_Framework_Error; use SilverStripe\Control\Controller;
use SilverStripe\Control\RequestHandler; use SilverStripe\Control\Director;
use SilverStripe\Control\Tests\ControllerTest\AccessBaseController; use SilverStripe\Control\Tests\ControllerTest\AccessBaseController;
use SilverStripe\Control\Tests\ControllerTest\AccessSecuredController; use SilverStripe\Control\Tests\ControllerTest\AccessSecuredController;
use SilverStripe\Control\Tests\ControllerTest\AccessWildcardSecuredController; use SilverStripe\Control\Tests\ControllerTest\AccessWildcardSecuredController;
@ -15,13 +15,8 @@ use SilverStripe\Control\Tests\ControllerTest\IndexSecuredController;
use SilverStripe\Control\Tests\ControllerTest\SubController; use SilverStripe\Control\Tests\ControllerTest\SubController;
use SilverStripe\Control\Tests\ControllerTest\TestController; use SilverStripe\Control\Tests\ControllerTest\TestController;
use SilverStripe\Control\Tests\ControllerTest\UnsecuredController; use SilverStripe\Control\Tests\ControllerTest\UnsecuredController;
use SilverStripe\Core\Config\Config;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
use SilverStripe\Dev\FunctionalTest; use SilverStripe\Dev\FunctionalTest;
use SilverStripe\ORM\DataModel;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
@ -526,22 +521,12 @@ class ControllerTest extends FunctionalTest
public function testSubActions() public function testSubActions()
{ {
/* If a controller action returns another controller, ensure that the $action variable is correctly forwarded */ // If a controller action returns another controller, ensure that the $action variable is correctly forwarded
$response = $this->get("ContainerController/subcontroller/subaction"); $response = $this->get("ContainerController/subcontroller/subaction");
$this->assertEquals('subaction', $response->getBody()); $this->assertEquals('subaction', $response->getBody());
$request = new HTTPRequest( // Handle nested action
'GET', $response = $this->get('ContainerController/subcontroller/substring/subvieweraction');
'ContainerController/subcontroller/substring/subvieweraction' $this->assertEquals('Hope this works', $response->getBody());
);
/* Shift to emulate the director selecting the controller */
$request->shift();
/* Handle the request to create conditions where improperly passing the action to the viewer might fail */
$controller = new ControllerTest\ContainerController();
try {
$controller->handleRequest($request, DataModel::inst());
} catch (ControllerTest\SubController_Exception $e) {
$this->fail($e->getMessage());
}
} }
} }

View File

@ -34,10 +34,6 @@ class SubController extends Controller implements TestOnly
public function subvieweraction() public function subvieweraction()
{ {
return $this->customise( return 'Hope this works';
array(
'Thoughts' => 'Hope this works',
)
);
} }
} }

View File

@ -10,6 +10,14 @@ use SilverStripe\Dev\TestOnly;
*/ */
class TestController extends Controller implements TestOnly class TestController extends Controller implements TestOnly
{ {
public function __construct()
{
parent::__construct();
if (Controller::has_curr()) {
$this->setRequest(Controller::curr()->getRequest());
}
}
private static $url_segment = 'TestController'; private static $url_segment = 'TestController';
public $Content = "default content"; public $Content = "default content";

View File

@ -3,31 +3,22 @@
namespace SilverStripe\Control\Tests; namespace SilverStripe\Control\Tests;
use SilverStripe\Control\Cookie_Backend; use SilverStripe\Control\Cookie_Backend;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPRequestBuilder;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\Tests\DirectorTest\TestController;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Director;
use SilverStripe\Control\RequestProcessor; use SilverStripe\Control\RequestProcessor;
use SilverStripe\Security\Security; use SilverStripe\Control\Tests\DirectorTest\TestController;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Kernel;
use SilverStripe\Dev\SapphireTest;
/** /**
* @todo test Director::alternateBaseFolder() * @todo test Director::alternateBaseFolder()
*/ */
class DirectorTest extends SapphireTest class DirectorTest extends SapphireTest
{ {
protected static $originalRequestURI;
protected $originalProtocolHeaders = array();
protected $originalGet = array();
protected $originalSession = array();
protected static $extra_controllers = [ protected static $extra_controllers = [
TestController::class, TestController::class,
]; ];
@ -35,28 +26,8 @@ class DirectorTest extends SapphireTest
protected function setUp() protected function setUp()
{ {
parent::setUp(); parent::setUp();
Director::config()->set('alternate_base_url', 'http://www.mysite.com/');
// Hold the original request URI once so it doesn't get overwritten $this->expectedRedirect = null;
if (!self::$originalRequestURI) {
self::$originalRequestURI = $_SERVER['REQUEST_URI'];
}
$_SERVER['REQUEST_URI'] = 'http://www.mysite.com';
$this->originalGet = $_GET;
$this->originalSession = $_SESSION;
$_SESSION = array();
$headers = array(
'HTTP_X_FORWARDED_PROTOCOL', 'HTTPS', 'SSL'
);
foreach ($headers as $header) {
if (isset($_SERVER[$header])) {
$this->originalProtocolHeaders[$header] = $_SERVER[$header];
}
}
Config::modify()->set(Director::class, 'alternate_base_url', '/');
} }
protected function getExtraRoutes() protected function getExtraRoutes()
@ -77,24 +48,6 @@ class DirectorTest extends SapphireTest
Director::config()->set('rules', $this->getExtraRoutes()); Director::config()->set('rules', $this->getExtraRoutes());
} }
protected function tearDown()
{
$_GET = $this->originalGet;
$_SESSION = $this->originalSession;
// Reinstate the original REQUEST_URI after it was modified by some tests
$_SERVER['REQUEST_URI'] = self::$originalRequestURI;
if ($this->originalProtocolHeaders) {
foreach ($this->originalProtocolHeaders as $header => $value) {
$_SERVER[$header] = $value;
}
}
parent::tearDown();
}
public function testFileExists() public function testFileExists()
{ {
$tempFileName = 'DirectorTest_testFileExists.tmp'; $tempFileName = 'DirectorTest_testFileExists.tmp';
@ -118,55 +71,53 @@ class DirectorTest extends SapphireTest
public function testAbsoluteURL() public function testAbsoluteURL()
{ {
Director::config()->set('alternate_base_url', 'http://www.mysite.com/mysite/');
$rootURL = Director::protocolAndHost(); $_SERVER['REQUEST_URI'] = "http://www.mysite.com/mysite/sub-page/";
$_SERVER['REQUEST_URI'] = "$rootURL/mysite/sub-page/";
Director::config()->set('alternate_base_url', '/mysite/');
//test empty / local urls //test empty / local urls
foreach (array('', './', '.') as $url) { foreach (array('', './', '.') as $url) {
$this->assertEquals("$rootURL/mysite/", Director::absoluteURL($url, Director::BASE)); $this->assertEquals("http://www.mysite.com/mysite/", Director::absoluteURL($url, Director::BASE));
$this->assertEquals("$rootURL/", Director::absoluteURL($url, Director::ROOT)); $this->assertEquals("http://www.mysite.com/", Director::absoluteURL($url, Director::ROOT));
$this->assertEquals("$rootURL/mysite/sub-page/", Director::absoluteURL($url, Director::REQUEST)); $this->assertEquals("http://www.mysite.com/mysite/sub-page/", Director::absoluteURL($url, Director::REQUEST));
} }
// Test site root url // Test site root url
$this->assertEquals("$rootURL/", Director::absoluteURL('/')); $this->assertEquals("http://www.mysite.com/", Director::absoluteURL('/'));
// Test Director::BASE // Test Director::BASE
$this->assertEquals($rootURL, Director::absoluteURL($rootURL, Director::BASE)); $this->assertEquals('http://www.mysite.com/', Director::absoluteURL('http://www.mysite.com/', Director::BASE));
$this->assertEquals('http://www.mytest.com', Director::absoluteURL('http://www.mytest.com', Director::BASE)); $this->assertEquals('http://www.mytest.com', Director::absoluteURL('http://www.mytest.com', Director::BASE));
$this->assertEquals("$rootURL/test", Director::absoluteURL("$rootURL/test", Director::BASE)); $this->assertEquals("http://www.mysite.com/test", Director::absoluteURL("http://www.mysite.com/test", Director::BASE));
$this->assertEquals("$rootURL/root", Director::absoluteURL("/root", Director::BASE)); $this->assertEquals("http://www.mysite.com/root", Director::absoluteURL("/root", Director::BASE));
$this->assertEquals("$rootURL/root/url", Director::absoluteURL("/root/url", Director::BASE)); $this->assertEquals("http://www.mysite.com/root/url", Director::absoluteURL("/root/url", Director::BASE));
// Test Director::ROOT // Test Director::ROOT
$this->assertEquals($rootURL, Director::absoluteURL($rootURL, Director::ROOT)); $this->assertEquals('http://www.mysite.com/', Director::absoluteURL('http://www.mysite.com/', Director::ROOT));
$this->assertEquals('http://www.mytest.com', Director::absoluteURL('http://www.mytest.com', Director::ROOT)); $this->assertEquals('http://www.mytest.com', Director::absoluteURL('http://www.mytest.com', Director::ROOT));
$this->assertEquals("$rootURL/test", Director::absoluteURL("$rootURL/test", Director::ROOT)); $this->assertEquals("http://www.mysite.com/test", Director::absoluteURL("http://www.mysite.com/test", Director::ROOT));
$this->assertEquals("$rootURL/root", Director::absoluteURL("/root", Director::ROOT)); $this->assertEquals("http://www.mysite.com/root", Director::absoluteURL("/root", Director::ROOT));
$this->assertEquals("$rootURL/root/url", Director::absoluteURL("/root/url", Director::ROOT)); $this->assertEquals("http://www.mysite.com/root/url", Director::absoluteURL("/root/url", Director::ROOT));
// Test Director::REQUEST // Test Director::REQUEST
$this->assertEquals($rootURL, Director::absoluteURL($rootURL, Director::REQUEST)); $this->assertEquals('http://www.mysite.com/', Director::absoluteURL('http://www.mysite.com/', Director::REQUEST));
$this->assertEquals('http://www.mytest.com', Director::absoluteURL('http://www.mytest.com', Director::REQUEST)); $this->assertEquals('http://www.mytest.com', Director::absoluteURL('http://www.mytest.com', Director::REQUEST));
$this->assertEquals("$rootURL/test", Director::absoluteURL("$rootURL/test", Director::REQUEST)); $this->assertEquals("http://www.mysite.com/test", Director::absoluteURL("http://www.mysite.com/test", Director::REQUEST));
$this->assertEquals("$rootURL/root", Director::absoluteURL("/root", Director::REQUEST)); $this->assertEquals("http://www.mysite.com/root", Director::absoluteURL("/root", Director::REQUEST));
$this->assertEquals("$rootURL/root/url", Director::absoluteURL("/root/url", Director::REQUEST)); $this->assertEquals("http://www.mysite.com/root/url", Director::absoluteURL("/root/url", Director::REQUEST));
// Test evaluating relative urls relative to base (default) // Test evaluating relative urls relative to base (default)
$this->assertEquals("$rootURL/mysite/test", Director::absoluteURL("test")); $this->assertEquals("http://www.mysite.com/mysite/test", Director::absoluteURL("test"));
$this->assertEquals("$rootURL/mysite/test/url", Director::absoluteURL("test/url")); $this->assertEquals("http://www.mysite.com/mysite/test/url", Director::absoluteURL("test/url"));
$this->assertEquals("$rootURL/mysite/test", Director::absoluteURL("test", Director::BASE)); $this->assertEquals("http://www.mysite.com/mysite/test", Director::absoluteURL("test", Director::BASE));
$this->assertEquals("$rootURL/mysite/test/url", Director::absoluteURL("test/url", Director::BASE)); $this->assertEquals("http://www.mysite.com/mysite/test/url", Director::absoluteURL("test/url", Director::BASE));
// Test evaluting relative urls relative to root // Test evaluting relative urls relative to root
$this->assertEquals("$rootURL/test", Director::absoluteURL("test", Director::ROOT)); $this->assertEquals("http://www.mysite.com/test", Director::absoluteURL("test", Director::ROOT));
$this->assertEquals("$rootURL/test/url", Director::absoluteURL("test/url", Director::ROOT)); $this->assertEquals("http://www.mysite.com/test/url", Director::absoluteURL("test/url", Director::ROOT));
// Test relative to requested page // Test relative to requested page
$this->assertEquals("$rootURL/mysite/sub-page/test", Director::absoluteURL("test", Director::REQUEST)); $this->assertEquals("http://www.mysite.com/mysite/sub-page/test", Director::absoluteURL("test", Director::REQUEST));
$this->assertEquals("$rootURL/mysite/sub-page/test/url", Director::absoluteURL("test/url", Director::REQUEST)); $this->assertEquals("http://www.mysite.com/mysite/sub-page/test/url", Director::absoluteURL("test/url", Director::REQUEST));
// Test that javascript links are not left intact // Test that javascript links are not left intact
$this->assertStringStartsNotWith('javascript', Director::absoluteURL('javascript:alert("attack")')); $this->assertStringStartsNotWith('javascript', Director::absoluteURL('javascript:alert("attack")'));
@ -177,17 +128,15 @@ class DirectorTest extends SapphireTest
public function testAlternativeBaseURL() public function testAlternativeBaseURL()
{ {
// Get original protocol and hostname
$rootURL = Director::protocolAndHost();
// relative base URLs - you should end them in a / // relative base URLs - you should end them in a /
Director::config()->set('alternate_base_url', '/relativebase/'); Director::config()->set('alternate_base_url', '/relativebase/');
$_SERVER['REQUEST_URI'] = "$rootURL/relativebase/sub-page/"; $_SERVER['HTTP_HOST'] = 'www.somesite.com';
$_SERVER['REQUEST_URI'] = "/relativebase/sub-page/";
$this->assertEquals('/relativebase/', Director::baseURL()); $this->assertEquals('/relativebase/', Director::baseURL());
$this->assertEquals($rootURL . '/relativebase/', Director::absoluteBaseURL()); $this->assertEquals('http://www.somesite.com/relativebase/', Director::absoluteBaseURL());
$this->assertEquals( $this->assertEquals(
$rootURL . '/relativebase/subfolder/test', 'http://www.somesite.com/relativebase/subfolder/test',
Director::absoluteURL('subfolder/test') Director::absoluteURL('subfolder/test')
); );
@ -336,6 +285,10 @@ class DirectorTest extends SapphireTest
unset($_GET['isTest']); unset($_GET['isTest']);
unset($_GET['isDev']); unset($_GET['isDev']);
/** @var Kernel $kernel */
$kernel = Injector::inst()->get(Kernel::class);
$kernel->setEnvironment(null);
// Test isDev=1 // Test isDev=1
$_GET['isDev'] = '1'; $_GET['isDev'] = '1';
$this->assertTrue(Director::isDev()); $this->assertTrue(Director::isDev());
@ -399,14 +352,28 @@ class DirectorTest extends SapphireTest
); );
} }
public function testTestRequestCarriesGlobals() public function providerTestTestRequestCarriesGlobals()
{ {
$fixture = array('somekey' => 'sometestvalue'); $tests = [];
$fixture = [ 'somekey' => 'sometestvalue' ];
foreach (array('get', 'post') as $method) { foreach (array('get', 'post') as $method) {
foreach (array('return%sValue', 'returnRequestValue', 'returnCookieValue') as $testfunction) { foreach (array('return%sValue', 'returnRequestValue', 'returnCookieValue') as $testfunction) {
$url = 'TestController/' . sprintf($testfunction, ucfirst($method)) $url = 'TestController/' . sprintf($testfunction, ucfirst($method))
. '?' . http_build_query($fixture); . '?' . http_build_query($fixture);
$tests[] = [$url, $fixture, $method];
}
}
return $tests;
}
/**
* @dataProvider providerTestTestRequestCarriesGlobals
* @param $url
* @param $fixture
* @param $method
*/
public function testTestRequestCarriesGlobals($url, $fixture, $method)
{
$getresponse = Director::test( $getresponse = Director::test(
$url, $url,
$fixture, $fixture,
@ -418,9 +385,7 @@ class DirectorTest extends SapphireTest
); );
$this->assertInstanceOf(HTTPResponse::class, $getresponse, 'Director::test() returns HTTPResponse'); $this->assertInstanceOf(HTTPResponse::class, $getresponse, 'Director::test() returns HTTPResponse');
$this->assertEquals($fixture['somekey'], $getresponse->getBody(), 'Director::test() ' . $testfunction); $this->assertEquals($fixture['somekey'], $getresponse->getBody(), "Director::test({$url}, {$method})");
}
}
} }
/** /**
@ -446,46 +411,87 @@ class DirectorTest extends SapphireTest
public function testForceSSLProtectsEntireSite() public function testForceSSLProtectsEntireSite()
{ {
$_SERVER['REQUEST_URI'] = '/admin'; $this->expectExceptionRedirect('https://www.mysite.com/some-url');
$output = Director::forceSSL(); Director::mockRequest(function () {
$this->assertEquals($output, 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']); Director::forceSSL();
}, '/some-url');
$_SERVER['REQUEST_URI'] = Director::baseURL() . 'some-url';
$output = Director::forceSSL();
$this->assertEquals($output, 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
} }
public function testForceSSLOnTopLevelPagePattern() public function testForceSSLOnTopLevelPagePattern()
{ {
$_SERVER['REQUEST_URI'] = Director::baseURL() . 'admin'; // Expect admin to trigger redirect
$output = Director::forceSSL(array('/^admin/')); $this->expectExceptionRedirect('https://www.mysite.com/admin');
$this->assertEquals($output, 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']); Director::mockRequest(function () {
Director::forceSSL(array('/^admin/'));
}, '/admin');
} }
public function testForceSSLOnSubPagesPattern() public function testForceSSLOnSubPagesPattern()
{ {
$_SERVER['REQUEST_URI'] = Director::baseURL() . Config::inst()->get(Security::class, 'login_url'); // Expect to redirect to security login page
$output = Director::forceSSL(array('/^Security/')); $this->expectExceptionRedirect('https://www.mysite.com/Security/login');
$this->assertEquals($output, 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']); Director::mockRequest(function () {
Director::forceSSL(array('/^Security/'));
}, '/Security/login');
} }
public function testForceSSLWithPatternDoesNotMatchOtherPages() public function testForceSSLWithPatternDoesNotMatchOtherPages()
{ {
$_SERVER['REQUEST_URI'] = Director::baseURL() . 'normal-page'; // Not on same url should not trigger redirect
$output = Director::forceSSL(array('/^admin/')); Director::mockRequest(function () {
$this->assertFalse($output); $this->assertFalse(Director::forceSSL(array('/^admin/')));
}, Director::baseURL() . 'normal-page');
$_SERVER['REQUEST_URI'] = Director::baseURL() . 'just-another-page/sub-url'; // nested url should not triger redirect either
$output = Director::forceSSL(array('/^admin/', '/^Security/')); Director::mockRequest(function () {
$this->assertFalse($output); $this->assertFalse(Director::forceSSL(array('/^admin/', '/^Security/')));
}, Director::baseURL() . 'just-another-page/sub-url');
} }
public function testForceSSLAlternateDomain() public function testForceSSLAlternateDomain()
{ {
Director::config()->set('alternate_base_url', '/'); // Ensure that forceSSL throws the appropriate exception
$_SERVER['REQUEST_URI'] = Director::baseURL() . 'admin'; $this->expectExceptionRedirect('https://secure.mysite.com/admin');
$output = Director::forceSSL(array('/^admin/'), 'secure.mysite.com'); Director::mockRequest(function (HTTPRequest $request) {
$this->assertEquals($output, 'https://secure.mysite.com/admin'); return Director::forceSSL(array('/^admin/'), 'secure.mysite.com');
}, Director::baseURL() . 'admin');
}
/**
* Set url to redirect to
*
* @var string
*/
protected $expectedRedirect = null;
/**
* Expects this test to throw a HTTPResponse_Exception with the given redirect
*
* @param string $url
*/
protected function expectExceptionRedirect($url)
{
$this->expectedRedirect = $url;
}
protected function runTest()
{
try {
$result = parent::runTest();
if ($this->expectedRedirect) {
$this->fail("Expected to redirect to {$this->expectedRedirect} but no redirect found");
}
return $result;
} catch (HTTPResponse_Exception $exception) {
// Check URL
if ($this->expectedRedirect) {
$url = $exception->getResponse()->getHeader('Location');
$this->assertEquals($this->expectedRedirect, $url, "Expected to redirect to {$this->expectedRedirect}");
return null;
} else {
throw $exception;
}
}
} }
/** /**
@ -518,7 +524,7 @@ class DirectorTest extends SapphireTest
'Content-Length' => '10' 'Content-Length' => '10'
); );
$this->assertEquals($headers, Director::extract_request_headers($request)); $this->assertEquals($headers, HTTPRequestBuilder::extractRequestHeaders($request));
} }
public function testUnmatchedRequestReturns404() public function testUnmatchedRequestReturns404()
@ -614,28 +620,30 @@ class DirectorTest extends SapphireTest
$processor = new RequestProcessor(array($filter)); $processor = new RequestProcessor(array($filter));
Injector::inst()->registerService($processor, RequestProcessor::class); Injector::inst()->registerService($processor, RequestProcessor::class);
$response = Director::test('some-dummy-url');
Director::test('some-dummy-url'); $this->assertEquals(404, $response->getStatusCode());
$this->assertEquals(1, $filter->preCalls); $this->assertEquals(1, $filter->preCalls);
$this->assertEquals(1, $filter->postCalls); $this->assertEquals(1, $filter->postCalls);
$filter->failPost = true; $filter->failPost = true;
$this->setExpectedException(HTTPResponse_Exception::class); $response = Director::test('some-dummy-url');
$this->assertEquals(500, $response->getStatusCode());
Director::test('some-dummy-url'); $this->assertEquals(_t(Director::class.'.REQUEST_ABORTED', 'Request aborted'), $response->getBody());
$this->assertEquals(2, $filter->preCalls); $this->assertEquals(2, $filter->preCalls);
$this->assertEquals(2, $filter->postCalls); $this->assertEquals(2, $filter->postCalls);
$filter->failPre = true; $filter->failPre = true;
Director::test('some-dummy-url'); $response = Director::test('some-dummy-url');
$this->assertEquals(400, $response->getStatusCode());
$this->assertEquals(_t(Director::class.'.INVALID_REQUEST', 'Invalid request'), $response->getBody());
$this->assertEquals(3, $filter->preCalls); $this->assertEquals(3, $filter->preCalls);
// preCall 'false' will trigger an exception and prevent post call execution // preCall 'true' will trigger an exception and prevent post call execution
$this->assertEquals(2, $filter->postCalls); $this->assertEquals(2, $filter->postCalls);
} }
} }

View File

@ -7,6 +7,14 @@ use SilverStripe\Dev\TestOnly;
class TestController extends Controller implements TestOnly class TestController extends Controller implements TestOnly
{ {
public function __construct()
{
parent::__construct();
if (Controller::has_curr()) {
$this->setRequest(Controller::curr()->getRequest());
}
}
private static $url_segment = 'TestController'; private static $url_segment = 'TestController';
private static $allowed_actions = array( private static $allowed_actions = array(
@ -18,21 +26,33 @@ class TestController extends Controller implements TestOnly
public function returnGetValue($request) public function returnGetValue($request)
{ {
if (isset($_GET['somekey'])) {
return $_GET['somekey']; return $_GET['somekey'];
} }
return null;
}
public function returnPostValue($request) public function returnPostValue($request)
{ {
if (isset($_POST['somekey'])) {
return $_POST['somekey']; return $_POST['somekey'];
} }
return null;
}
public function returnRequestValue($request) public function returnRequestValue($request)
{ {
if (isset($_REQUEST['somekey'])) {
return $_REQUEST['somekey']; return $_REQUEST['somekey'];
} }
return null;
}
public function returnCookieValue($request) public function returnCookieValue($request)
{ {
if (isset($_COOKIE['somekey'])) {
return $_COOKIE['somekey']; return $_COOKIE['somekey'];
} }
return null;
}
} }

View File

@ -5,9 +5,7 @@ namespace SilverStripe\Control\Tests\DirectorTest;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\RequestFilter; use SilverStripe\Control\RequestFilter;
use SilverStripe\Control\Session;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataModel;
class TestRequestFilter implements RequestFilter, TestOnly class TestRequestFilter implements RequestFilter, TestOnly
{ {
@ -17,22 +15,24 @@ class TestRequestFilter implements RequestFilter, TestOnly
public $failPre = false; public $failPre = false;
public $failPost = false; public $failPost = false;
public function preRequest(HTTPRequest $request, Session $session, DataModel $model) public function preRequest(HTTPRequest $request)
{ {
++$this->preCalls; ++$this->preCalls;
if ($this->failPre) { if ($this->failPre) {
return false; return false;
} }
return true;
} }
public function postRequest(HTTPRequest $request, HTTPResponse $response, DataModel $model) public function postRequest(HTTPRequest $request, HTTPResponse $response)
{ {
++$this->postCalls; ++$this->postCalls;
if ($this->failPost) { if ($this->failPost) {
return false; return false;
} }
return true;
} }
public function reset() public function reset()

View File

@ -1,31 +0,0 @@
<?php
namespace SilverStripe\Control\Tests;
use SilverStripe\Control\Session;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\Controller;
// Fake a current controller. Way harder than it should be
class FakeController extends Controller
{
public function __construct()
{
parent::__construct();
$session = Injector::inst()->create(Session::class, isset($_SESSION) ? $_SESSION : array());
$this->setSession($session);
$this->pushCurrent();
$request = new HTTPRequest('GET', '/');
$this->setRequest($request);
$this->setResponse(new HTTPResponse());
$this->doInit();
}
}

View File

@ -13,9 +13,7 @@ class FlushRequestFilterTest extends FunctionalTest
public function testImplementorsAreCalled() public function testImplementorsAreCalled()
{ {
TestFlushable::$flushed = false; TestFlushable::$flushed = false;
$this->get('?flush=1'); $this->get('?flush=1');
$this->assertTrue(TestFlushable::$flushed); $this->assertTrue(TestFlushable::$flushed);
} }
} }

View File

@ -7,7 +7,6 @@ use SilverStripe\Dev\TestOnly;
class TestFlushable implements Flushable, TestOnly class TestFlushable implements Flushable, TestOnly
{ {
public static $flushed = false; public static $flushed = false;
public static function flush() public static function flush()

View File

@ -2,10 +2,14 @@
namespace SilverStripe\Control\Tests; namespace SilverStripe\Control\Tests;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTP; use SilverStripe\Control\HTTP;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Kernel;
use SilverStripe\Dev\FunctionalTest;
/** /**
* Tests the {@link HTTP} class * Tests the {@link HTTP} class
@ -27,13 +31,15 @@ class HTTPTest extends FunctionalTest
$this->assertNotEmpty($response->getHeader('Cache-Control')); $this->assertNotEmpty($response->getHeader('Cache-Control'));
// Ensure max-age is zero for development. // Ensure max-age is zero for development.
Director::set_environment_type('dev'); /** @var Kernel $kernel */
$kernel = Injector::inst()->get(Kernel::class);
$kernel->setEnvironment(Kernel::DEV);
$response = new HTTPResponse($body, 200); $response = new HTTPResponse($body, 200);
HTTP::add_cache_headers($response); HTTP::add_cache_headers($response);
$this->assertContains('max-age=0', $response->getHeader('Cache-Control')); $this->assertContains('max-age=0', $response->getHeader('Cache-Control'));
// Ensure max-age setting is respected in production. // Ensure max-age setting is respected in production.
Director::set_environment_type('live'); $kernel->setEnvironment(Kernel::LIVE);
$response = new HTTPResponse($body, 200); $response = new HTTPResponse($body, 200);
HTTP::add_cache_headers($response); HTTP::add_cache_headers($response);
$this->assertContains('max-age=30', explode(', ', $response->getHeader('Cache-Control'))); $this->assertContains('max-age=30', explode(', ', $response->getHeader('Cache-Control')));
@ -57,9 +63,11 @@ class HTTPTest extends FunctionalTest
public function testConfigVary() public function testConfigVary()
{ {
/** @var Kernel $kernel */
$kernel = Injector::inst()->get(Kernel::class);
$body = "<html><head></head><body><h1>Mysite</h1></body></html>"; $body = "<html><head></head><body><h1>Mysite</h1></body></html>";
$response = new HTTPResponse($body, 200); $response = new HTTPResponse($body, 200);
Director::set_environment_type('live'); $kernel->setEnvironment(Kernel::LIVE);
HTTP::set_cache_age(30); HTTP::set_cache_age(30);
HTTP::add_cache_headers($response); HTTP::add_cache_headers($response);
@ -124,14 +132,20 @@ class HTTPTest extends FunctionalTest
{ {
// Hackery to work around volatile URL formats in test invocation, // Hackery to work around volatile URL formats in test invocation,
// and the inability of Director::absoluteBaseURL() to produce consistent URLs. // and the inability of Director::absoluteBaseURL() to produce consistent URLs.
$origURI = $_SERVER['REQUEST_URI']; Director::mockRequest(function (HTTPRequest $request) {
$_SERVER['REQUEST_URI'] = 'relative/url/'; $controller = new Controller();
$controller->setRequest($request);
$controller->pushCurrent();
try {
$this->assertContains( $this->assertContains(
'relative/url/?foo=bar', 'relative/url?foo=bar',
HTTP::setGetVar('foo', 'bar'), HTTP::setGetVar('foo', 'bar'),
'Omitting a URL falls back to current URL' 'Omitting a URL falls back to current URL'
); );
$_SERVER['REQUEST_URI'] = $origURI; } finally {
$controller->popCurrent();
}
}, 'relative/url/');
$this->assertEquals( $this->assertEquals(
'relative/url?foo=bar', 'relative/url?foo=bar',
@ -206,30 +220,30 @@ class HTTPTest extends FunctionalTest
{ {
$this->withBaseURL( $this->withBaseURL(
'http://www.silverstripe.org/', 'http://www.silverstripe.org/',
function ($test) { function () {
// background-image // background-image
// Note that using /./ in urls is absolutely acceptable // Note that using /./ in urls is absolutely acceptable
$test->assertEquals( $this->assertEquals(
'<div style="background-image: url(\'http://www.silverstripe.org/./images/mybackground.gif\');">'. '<div style="background-image: url(\'http://www.silverstripe.org/./images/mybackground.gif\');">'.
'Content</div>', 'Content</div>',
HTTP::absoluteURLs('<div style="background-image: url(\'./images/mybackground.gif\');">Content</div>') HTTP::absoluteURLs('<div style="background-image: url(\'./images/mybackground.gif\');">Content</div>')
); );
// background // background
$test->assertEquals( $this->assertEquals(
'<div style="background: url(\'http://www.silverstripe.org/images/mybackground.gif\');">Content</div>', '<div style="background: url(\'http://www.silverstripe.org/images/mybackground.gif\');">Content</div>',
HTTP::absoluteURLs('<div style="background: url(\'images/mybackground.gif\');">Content</div>') HTTP::absoluteURLs('<div style="background: url(\'images/mybackground.gif\');">Content</div>')
); );
// list-style-image // list-style-image
$test->assertEquals( $this->assertEquals(
'<div style=\'background: url(http://www.silverstripe.org/list.png);\'>Content</div>', '<div style=\'background: url(http://www.silverstripe.org/list.png);\'>Content</div>',
HTTP::absoluteURLs('<div style=\'background: url(list.png);\'>Content</div>') HTTP::absoluteURLs('<div style=\'background: url(list.png);\'>Content</div>')
); );
// list-style // list-style
$test->assertEquals( $this->assertEquals(
'<div style=\'background: url("http://www.silverstripe.org/./assets/list.png");\'>Content</div>', '<div style=\'background: url("http://www.silverstripe.org/./assets/list.png");\'>Content</div>',
HTTP::absoluteURLs('<div style=\'background: url("./assets/list.png");\'>Content</div>') HTTP::absoluteURLs('<div style=\'background: url("./assets/list.png");\'>Content</div>')
); );
@ -244,37 +258,37 @@ class HTTPTest extends FunctionalTest
{ {
$this->withBaseURL( $this->withBaseURL(
'http://www.silverstripe.org/', 'http://www.silverstripe.org/',
function ($test) { function () {
//empty links //empty links
$test->assertEquals( $this->assertEquals(
'<a href="http://www.silverstripe.org/">test</a>', '<a href="http://www.silverstripe.org/">test</a>',
HTTP::absoluteURLs('<a href="">test</a>') HTTP::absoluteURLs('<a href="">test</a>')
); );
$test->assertEquals( $this->assertEquals(
'<a href="http://www.silverstripe.org/">test</a>', '<a href="http://www.silverstripe.org/">test</a>',
HTTP::absoluteURLs('<a href="/">test</a>') HTTP::absoluteURLs('<a href="/">test</a>')
); );
//relative //relative
$test->assertEquals( $this->assertEquals(
'<a href="http://www.silverstripe.org/">test</a>', '<a href="http://www.silverstripe.org/">test</a>',
HTTP::absoluteURLs('<a href="./">test</a>') HTTP::absoluteURLs('<a href="./">test</a>')
); );
$test->assertEquals( $this->assertEquals(
'<a href="http://www.silverstripe.org/">test</a>', '<a href="http://www.silverstripe.org/">test</a>',
HTTP::absoluteURLs('<a href=".">test</a>') HTTP::absoluteURLs('<a href=".">test</a>')
); );
// links // links
$test->assertEquals( $this->assertEquals(
'<a href=\'http://www.silverstripe.org/blog/\'>SS Blog</a>', '<a href=\'http://www.silverstripe.org/blog/\'>SS Blog</a>',
HTTP::absoluteURLs('<a href=\'/blog/\'>SS Blog</a>') HTTP::absoluteURLs('<a href=\'/blog/\'>SS Blog</a>')
); );
// background // background
// Note that using /./ in urls is absolutely acceptable // Note that using /./ in urls is absolutely acceptable
$test->assertEquals( $this->assertEquals(
'<div background="http://www.silverstripe.org/./themes/silverstripe/images/nav-bg-repeat-2.png">'. '<div background="http://www.silverstripe.org/./themes/silverstripe/images/nav-bg-repeat-2.png">'.
'SS Blog</div>', 'SS Blog</div>',
HTTP::absoluteURLs('<div background="./themes/silverstripe/images/nav-bg-repeat-2.png">SS Blog</div>') HTTP::absoluteURLs('<div background="./themes/silverstripe/images/nav-bg-repeat-2.png">SS Blog</div>')
@ -283,25 +297,25 @@ class HTTPTest extends FunctionalTest
//check dot segments //check dot segments
// Assumption: dots are not removed // Assumption: dots are not removed
//if they were, the url should be: http://www.silverstripe.org/abc //if they were, the url should be: http://www.silverstripe.org/abc
$test->assertEquals( $this->assertEquals(
'<a href="http://www.silverstripe.org/test/page/../../abc">Test</a>', '<a href="http://www.silverstripe.org/test/page/../../abc">Test</a>',
HTTP::absoluteURLs('<a href="test/page/../../abc">Test</a>') HTTP::absoluteURLs('<a href="test/page/../../abc">Test</a>')
); );
// image // image
$test->assertEquals( $this->assertEquals(
'<img src=\'http://www.silverstripe.org/themes/silverstripe/images/logo-org.png\' />', '<img src=\'http://www.silverstripe.org/themes/silverstripe/images/logo-org.png\' />',
HTTP::absoluteURLs('<img src=\'themes/silverstripe/images/logo-org.png\' />') HTTP::absoluteURLs('<img src=\'themes/silverstripe/images/logo-org.png\' />')
); );
// link // link
$test->assertEquals( $this->assertEquals(
'<link href=http://www.silverstripe.org/base.css />', '<link href=http://www.silverstripe.org/base.css />',
HTTP::absoluteURLs('<link href=base.css />') HTTP::absoluteURLs('<link href=base.css />')
); );
// Test special characters are retained // Test special characters are retained
$test->assertEquals( $this->assertEquals(
'<a href="http://www.silverstripe.org/Security/changepassword?m=3&amp;t=7214fdfde">password reset link</a>', '<a href="http://www.silverstripe.org/Security/changepassword?m=3&amp;t=7214fdfde">password reset link</a>',
HTTP::absoluteURLs('<a href="/Security/changepassword?m=3&amp;t=7214fdfde">password reset link</a>') HTTP::absoluteURLs('<a href="/Security/changepassword?m=3&amp;t=7214fdfde">password reset link</a>')
); );
@ -319,14 +333,14 @@ class HTTPTest extends FunctionalTest
function ($test) { function ($test) {
// mailto // mailto
$test->assertEquals( $this->assertEquals(
'<a href=\'mailto:admin@silverstripe.org\'>Email Us</a>', '<a href=\'mailto:admin@silverstripe.org\'>Email Us</a>',
HTTP::absoluteURLs('<a href=\'mailto:admin@silverstripe.org\'>Email Us</a>'), HTTP::absoluteURLs('<a href=\'mailto:admin@silverstripe.org\'>Email Us</a>'),
'Email links are not rewritten' 'Email links are not rewritten'
); );
// data uri // data uri
$test->assertEquals( $this->assertEquals(
'<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38'. '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38'.
'GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot" />', 'GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot" />',
HTTP::absoluteURLs( HTTP::absoluteURLs(
@ -337,7 +351,7 @@ class HTTPTest extends FunctionalTest
); );
// call // call
$test->assertEquals( $this->assertEquals(
'<a href="callto:12345678" />', '<a href="callto:12345678" />',
HTTP::absoluteURLs('<a href="callto:12345678" />'), HTTP::absoluteURLs('<a href="callto:12345678" />'),
'Call to links are not rewritten' 'Call to links are not rewritten'
@ -352,7 +366,7 @@ class HTTPTest extends FunctionalTest
'http://www.silverstripe.org/', 'http://www.silverstripe.org/',
function ($test) { function ($test) {
$frameworkTests = ltrim(FRAMEWORK_DIR . '/tests', '/'); $frameworkTests = ltrim(FRAMEWORK_DIR . '/tests', '/');
$test->assertEquals( $this->assertEquals(
"http://www.silverstripe.org/$frameworkTests/php/Control/HTTPTest.php", "http://www.silverstripe.org/$frameworkTests/php/Control/HTTPTest.php",
HTTP::filename2url(__FILE__) HTTP::filename2url(__FILE__)
); );

View File

@ -2,9 +2,10 @@
namespace SilverStripe\Control\Tests; namespace SilverStripe\Control\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\PjaxResponseNegotiator;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\PjaxResponseNegotiator;
use SilverStripe\Control\Session;
use SilverStripe\Dev\SapphireTest;
class PjaxResponseNegotiatorTest extends SapphireTest class PjaxResponseNegotiatorTest extends SapphireTest
{ {
@ -19,6 +20,7 @@ class PjaxResponseNegotiatorTest extends SapphireTest
) )
); );
$request = new HTTPRequest('GET', '/'); // not setting pjax header $request = new HTTPRequest('GET', '/'); // not setting pjax header
$request->setSession(new Session([]));
$response = $negotiator->respond($request); $response = $negotiator->respond($request);
$this->assertEquals('default response', $response->getBody()); $this->assertEquals('default response', $response->getBody());
} }
@ -36,6 +38,7 @@ class PjaxResponseNegotiatorTest extends SapphireTest
) )
); );
$request = new HTTPRequest('GET', '/'); $request = new HTTPRequest('GET', '/');
$request->setSession(new Session([]));
$request->addHeader('X-Pjax', 'myfragment'); $request->addHeader('X-Pjax', 'myfragment');
$response = $negotiator->respond($request); $response = $negotiator->respond($request);
$this->assertEquals('{"myfragment":"myfragment response"}', $response->getBody()); $this->assertEquals('{"myfragment":"myfragment response"}', $response->getBody());
@ -57,6 +60,7 @@ class PjaxResponseNegotiatorTest extends SapphireTest
) )
); );
$request = new HTTPRequest('GET', '/'); $request = new HTTPRequest('GET', '/');
$request->setSession(new Session([]));
$request->addHeader('X-Pjax', 'myfragment,otherfragment'); $request->addHeader('X-Pjax', 'myfragment,otherfragment');
$request->addHeader('Accept', 'text/json'); $request->addHeader('Accept', 'text/json');
$response = $negotiator->respond($request); $response = $negotiator->respond($request);
@ -81,6 +85,7 @@ class PjaxResponseNegotiatorTest extends SapphireTest
); );
$request = new HTTPRequest('GET', '/'); $request = new HTTPRequest('GET', '/');
$request->setSession(new Session([]));
$request->addHeader('X-Pjax', 'alpha'); $request->addHeader('X-Pjax', 'alpha');
$request->addHeader('Accept', 'text/json'); $request->addHeader('Accept', 'text/json');

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Control\Tests\RequestHandlingTest; namespace SilverStripe\Control\Tests\RequestHandlingTest;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
@ -41,25 +42,28 @@ class TestController extends Controller implements TestOnly
{ {
$this->failover = new ControllerFailover(); $this->failover = new ControllerFailover();
parent::__construct(); parent::__construct();
if (Controller::has_curr()) {
$this->setRequest(Controller::curr()->getRequest());
}
} }
public function index($request) public function index(HTTPRequest $request)
{ {
return "This is the controller"; return "This is the controller";
} }
public function method($request) public function method(HTTPRequest $request)
{ {
return "This is a method on the controller: " . $request->param('ID') . ', ' . $request->param('OtherID'); return "This is a method on the controller: " . $request->param('ID') . ', ' . $request->param('OtherID');
} }
public function legacymethod($request) public function legacymethod(HTTPRequest $request)
{ {
return "\$this->urlParams can be used, for backward compatibility: " . $this->urlParams['ID'] . ', ' return "\$this->urlParams can be used, for backward compatibility: " . $this->urlParams['ID'] . ', '
. $this->urlParams['OtherID']; . $this->urlParams['OtherID'];
} }
public function virtualfile($request) public function virtualfile(HTTPRequest $request)
{ {
return "This is the virtualfile method"; return "This is the virtualfile method";
} }

View File

@ -2,53 +2,61 @@
namespace SilverStripe\Control\Tests; namespace SilverStripe\Control\Tests;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Dev\SapphireTest;
/** /**
* Tests to cover the {@link Session} class * Tests to cover the {@link Session} class
*/ */
class SessionTest extends SapphireTest class SessionTest extends SapphireTest
{ {
/**
* @var Session
*/
protected $session = null;
protected function setUp()
{
$this->session = new Session([]);
return parent::setUp();
}
public function testGetSetBasics() public function testGetSetBasics()
{ {
Session::set('Test', 'Test'); $this->session->set('Test', 'Test');
$this->assertEquals(Session::get('Test'), 'Test'); $this->assertEquals($this->session->get('Test'), 'Test');
} }
public function testClearElement() public function testClearElement()
{ {
Session::set('Test', 'Test'); $this->session->set('Test', 'Test');
Session::clear('Test'); $this->session->clear('Test');
$this->assertEquals(Session::get('Test'), ''); $this->assertEquals($this->session->get('Test'), '');
} }
public function testClearAllElements() public function testClearAllElements()
{ {
Session::set('Test', 'Test'); $this->session->set('Test', 'Test');
Session::set('Test-1', 'Test-1'); $this->session->set('Test-1', 'Test-1');
Session::clear_all(); $this->session->clearAll();
// should session get return null? The array key should probably be // should session get return null? The array key should probably be
// unset from the data array // unset from the data array
$this->assertEquals(Session::get('Test'), ''); $this->assertEquals($this->session->get('Test'), '');
$this->assertEquals(Session::get('Test-1'), ''); $this->assertEquals($this->session->get('Test-1'), '');
} }
public function testGetAllElements() public function testGetAllElements()
{ {
Session::clear_all(); // Remove all session that might've been set by the test harness $this->session->clearAll(); // Remove all session that might've been set by the test harness
Session::set('Test', 'Test'); $this->session->set('Test', 'Test');
Session::set('Test-2', 'Test-2'); $this->session->set('Test-2', 'Test-2');
$session = Session::get_all(); $session = $this->session->getAll();
unset($session['HTTP_USER_AGENT']); unset($session['HTTP_USER_AGENT']);
$this->assertEquals($session, array('Test' => 'Test', 'Test-2' => 'Test-2')); $this->assertEquals($session, array('Test' => 'Test', 'Test-2' => 'Test-2'));
@ -56,10 +64,10 @@ class SessionTest extends SapphireTest
public function testSettingExistingDoesntClear() public function testSettingExistingDoesntClear()
{ {
$s = Injector::inst()->create('SilverStripe\\Control\\Session', array('something' => array('does' => 'exist'))); $s = new Session(array('something' => array('does' => 'exist')));
$s->inst_set('something.does', 'exist'); $s->set('something.does', 'exist');
$result = $s->inst_changedData(); $result = $s->changedData();
unset($result['HTTP_USER_AGENT']); unset($result['HTTP_USER_AGENT']);
$this->assertEquals(array(), $result); $this->assertEquals(array(), $result);
} }
@ -69,16 +77,16 @@ class SessionTest extends SapphireTest
*/ */
public function testClearElementThatDoesntExist() public function testClearElementThatDoesntExist()
{ {
$s = Injector::inst()->create('SilverStripe\\Control\\Session', array('something' => array('does' => 'exist'))); $s = new Session(array('something' => array('does' => 'exist')));
$s->inst_clear('something.doesnt.exist'); $s->clear('something.doesnt.exist');
$result = $s->inst_changedData(); $result = $s->changedData();
unset($result['HTTP_USER_AGENT']); unset($result['HTTP_USER_AGENT']);
$this->assertEquals(array(), $result); $this->assertEquals(array(), $result);
$s->inst_set('something-else', 'val'); $s->set('something-else', 'val');
$s->inst_clear('something-new'); $s->clear('something-new');
$result = $s->inst_changedData(); $result = $s->changedData();
unset($result['HTTP_USER_AGENT']); unset($result['HTTP_USER_AGENT']);
$this->assertEquals(array('something-else' => 'val'), $result); $this->assertEquals(array('something-else' => 'val'), $result);
} }
@ -88,37 +96,31 @@ class SessionTest extends SapphireTest
*/ */
public function testClearElementThatDoesExist() public function testClearElementThatDoesExist()
{ {
$s = Injector::inst()->create('SilverStripe\\Control\\Session', array('something' => array('does' => 'exist'))); $s = new Session(array('something' => array('does' => 'exist')));
$s->inst_clear('something.does'); $s->clear('something.does');
$result = $s->inst_changedData(); $result = $s->changedData();
unset($result['HTTP_USER_AGENT']); unset($result['HTTP_USER_AGENT']);
$this->assertEquals(array('something' => array('does' => null)), $result); $this->assertEquals(array('something' => array('does' => null)), $result);
} }
public function testNonStandardPath()
{
Config::inst()->update('SilverStripe\\Control\\Session', 'store_path', (realpath(dirname($_SERVER['DOCUMENT_ROOT']) . '/../session')));
Session::start();
$this->assertEquals(Config::inst()->get('SilverStripe\\Control\\Session', 'store_path'), '');
}
public function testUserAgentLockout() public function testUserAgentLockout()
{ {
// Set a user agent // Set a user agent
$_SERVER['HTTP_USER_AGENT'] = 'Test Agent'; $_SERVER['HTTP_USER_AGENT'] = 'Test Agent';
// Generate our session // Generate our session
$s = Injector::inst()->create('SilverStripe\\Control\\Session', array()); $s = new Session(array());
$s->inst_set('val', 123); $s->init();
$s->inst_finalize(); $s->set('val', 123);
$s->finalize();
// Change our UA // Change our UA
$_SERVER['HTTP_USER_AGENT'] = 'Fake Agent'; $_SERVER['HTTP_USER_AGENT'] = 'Fake Agent';
// Verify the new session reset our values // Verify the new session reset our values
$s2 = Injector::inst()->create('SilverStripe\\Control\\Session', $s); $s2 = new Session($s);
$this->assertNotEquals($s2->inst_get('val'), 123); $s2->init();
$this->assertNotEquals($s2->get('val'), 123);
} }
} }

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Core\Tests; namespace SilverStripe\Core\Tests;
use SilverStripe\Core\TempFolder;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
@ -22,31 +23,31 @@ class CoreTest extends SapphireTest
public function testGetTempPathInProject() public function testGetTempPathInProject()
{ {
$user = getTempFolderUsername(); $user = TempFolder::getTempFolderUsername();
if (file_exists($this->tempPath)) { if (file_exists($this->tempPath)) {
$this->assertEquals(getTempFolder(BASE_PATH), $this->tempPath . DIRECTORY_SEPARATOR . $user); $this->assertEquals(TempFolder::getTempFolder(BASE_PATH), $this->tempPath . DIRECTORY_SEPARATOR . $user);
} else { } else {
$user = getTempFolderUsername(); $user = TempFolder::getTempFolderUsername();
$base = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'silverstripe-cache-php' . $base = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'silverstripe-cache-php' .
preg_replace('/[^\w-\.+]+/', '-', PHP_VERSION); preg_replace('/[^\w-\.+]+/', '-', PHP_VERSION);
// A typical Windows location for where sites are stored on IIS // A typical Windows location for where sites are stored on IIS
$this->assertEquals( $this->assertEquals(
$base . 'C--inetpub-wwwroot-silverstripe-test-project' . DIRECTORY_SEPARATOR . $user, $base . 'C--inetpub-wwwroot-silverstripe-test-project' . DIRECTORY_SEPARATOR . $user,
getTempFolder('C:\\inetpub\\wwwroot\\silverstripe-test-project') TempFolder::getTempFolder('C:\\inetpub\\wwwroot\\silverstripe-test-project')
); );
// A typical Mac OS X location for where sites are stored // A typical Mac OS X location for where sites are stored
$this->assertEquals( $this->assertEquals(
$base . '-Users-joebloggs-Sites-silverstripe-test-project' . DIRECTORY_SEPARATOR . $user, $base . '-Users-joebloggs-Sites-silverstripe-test-project' . DIRECTORY_SEPARATOR . $user,
getTempFolder('/Users/joebloggs/Sites/silverstripe-test-project') TempFolder::getTempFolder('/Users/joebloggs/Sites/silverstripe-test-project')
); );
// A typical Linux location for where sites are stored // A typical Linux location for where sites are stored
$this->assertEquals( $this->assertEquals(
$base . '-var-www-silverstripe-test-project' . DIRECTORY_SEPARATOR . $user, $base . '-var-www-silverstripe-test-project' . DIRECTORY_SEPARATOR . $user,
getTempFolder('/var/www/silverstripe-test-project') TempFolder::getTempFolder('/var/www/silverstripe-test-project')
); );
} }
} }
@ -54,7 +55,7 @@ class CoreTest extends SapphireTest
protected function tearDown() protected function tearDown()
{ {
parent::tearDown(); parent::tearDown();
$user = getTempFolderUsername(); $user = TempFolder::getTempFolderUsername();
$base = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'silverstripe-cache-php' . $base = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'silverstripe-cache-php' .
preg_replace('/[^\w-\.+]+/', '-', PHP_VERSION); preg_replace('/[^\w-\.+]+/', '-', PHP_VERSION);
foreach (array( foreach (array(

View File

@ -967,7 +967,10 @@ class InjectorTest extends SapphireTest
// Test that nested injector values can be overridden // Test that nested injector values can be overridden
Injector::nest(); Injector::nest();
$this->nestingLevel++; $this->nestingLevel++;
Injector::inst()->unregisterAllObjects(); Injector::inst()->unregisterObjects([
TestStaticInjections::class,
MyParentClass::class,
]);
$newsi = Injector::inst()->get(TestStaticInjections::class); $newsi = Injector::inst()->get(TestStaticInjections::class);
$newsi->backend = new InjectorTest\OriginalRequirementsBackend(); $newsi->backend = new InjectorTest\OriginalRequirementsBackend();
Injector::inst()->registerService($newsi, TestStaticInjections::class); Injector::inst()->registerService($newsi, TestStaticInjections::class);
@ -990,7 +993,10 @@ class InjectorTest extends SapphireTest
$this->assertInstanceOf(MyChildClass::class, Injector::inst()->get(MyChildClass::class)); $this->assertInstanceOf(MyChildClass::class, Injector::inst()->get(MyChildClass::class));
// Test reset of cache // Test reset of cache
Injector::inst()->unregisterAllObjects(); Injector::inst()->unregisterObjects([
TestStaticInjections::class,
MyParentClass::class,
]);
$si = Injector::inst()->get(TestStaticInjections::class); $si = Injector::inst()->get(TestStaticInjections::class);
$this->assertInstanceOf(TestStaticInjections::class, $si); $this->assertInstanceOf(TestStaticInjections::class, $si);
$this->assertInstanceOf(NewRequirementsBackend::class, $si->backend); $this->assertInstanceOf(NewRequirementsBackend::class, $si->backend);

View File

@ -6,11 +6,9 @@ use SilverStripe\Dev\TestOnly;
class TestStaticInjections implements TestOnly class TestStaticInjections implements TestOnly
{ {
public $backend; public $backend;
/**
* @config /** @config */
*/
private static $dependencies = array( private static $dependencies = array(
'backend' => '%$SilverStripe\\Core\\Tests\\Injector\\InjectorTest\\NewRequirementsBackend' 'backend' => '%$SilverStripe\\Core\\Tests\\Injector\\InjectorTest\\NewRequirementsBackend'
); );

View File

@ -0,0 +1,81 @@
<?php
namespace SilverStripe\Core\Tests;
use BadMethodCallException;
use SilverStripe\Control\Director;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\ConfigLoader;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Injector\InjectorLoader;
use SilverStripe\Core\Kernel;
use SilverStripe\Dev\SapphireTest;
class KernelTest extends SapphireTest
{
public function testNesting()
{
/** @var Kernel $kernel */
$kernel = Injector::inst()->get(Kernel::class);
$nested1 = $kernel->nest();
Director::config()->set('alternate_base_url', '/mysite/');
$this->assertEquals($nested1->getConfigLoader(), ConfigLoader::inst());
$this->assertEquals($nested1->getInjectorLoader(), InjectorLoader::inst());
$this->assertEquals(1, ConfigLoader::inst()->countManifests());
$this->assertEquals(1, InjectorLoader::inst()->countManifests());
// Re-nest
$nested2 = $nested1->nest();
// Nesting config / injector should increase this count
Injector::nest();
Config::nest();
$this->assertEquals($nested2->getConfigLoader(), ConfigLoader::inst());
$this->assertEquals($nested2->getInjectorLoader(), InjectorLoader::inst());
$this->assertEquals(2, ConfigLoader::inst()->countManifests());
$this->assertEquals(2, InjectorLoader::inst()->countManifests());
Director::config()->set('alternate_base_url', '/anothersite/');
// Nesting always resets sub-loaders to 1
$nested2->nest();
$this->assertEquals(1, ConfigLoader::inst()->countManifests());
$this->assertEquals(1, InjectorLoader::inst()->countManifests());
// Calling ->activate() on a previous kernel restores
$nested1->activate();
$this->assertEquals($nested1->getConfigLoader(), ConfigLoader::inst());
$this->assertEquals($nested1->getInjectorLoader(), InjectorLoader::inst());
$this->assertEquals('/mysite/', Director::config()->get('alternate_base_url'));
$this->assertEquals(1, ConfigLoader::inst()->countManifests());
$this->assertEquals(1, InjectorLoader::inst()->countManifests());
}
public function testInvalidInjectorDetection()
{
$this->expectException(BadMethodCallException::class);
$this->expectExceptionMessage(
"Non-current injector manifest cannot be accessed. Please call ->activate() first"
);
/** @var Kernel $kernel */
$kernel = Injector::inst()->get(Kernel::class);
$kernel->nest(); // $kernel is no longer current kernel
$kernel->getInjectorLoader()->getManifest();
}
public function testInvalidConfigDetection()
{
$this->expectException(BadMethodCallException::class);
$this->expectExceptionMessage(
"Non-current config manifest cannot be accessed. Please call ->activate() first"
);
/** @var Kernel $kernel */
$kernel = Injector::inst()->get(Kernel::class);
$kernel->nest(); // $kernel is no longer current kernel
$kernel->getConfigLoader()->getManifest();
}
}

View File

@ -36,10 +36,12 @@ class ClassLoaderTest extends SapphireTest
{ {
parent::setUp(); parent::setUp();
$this->baseManifest1 = dirname(__FILE__) . '/fixtures/classmanifest'; $this->baseManifest1 = __DIR__ . '/fixtures/classmanifest';
$this->baseManifest2 = dirname(__FILE__) . '/fixtures/classmanifest_other'; $this->baseManifest2 = __DIR__ . '/fixtures/classmanifest_other';
$this->testManifest1 = new ClassManifest($this->baseManifest1, false); $this->testManifest1 = new ClassManifest($this->baseManifest1);
$this->testManifest2 = new ClassManifest($this->baseManifest2, false); $this->testManifest2 = new ClassManifest($this->baseManifest2);
$this->testManifest1->init();
$this->testManifest2->init();
} }
public function testExclusive() public function testExclusive()

View File

@ -32,8 +32,10 @@ class ClassManifestTest extends SapphireTest
parent::setUp(); parent::setUp();
$this->base = dirname(__FILE__) . '/fixtures/classmanifest'; $this->base = dirname(__FILE__) . '/fixtures/classmanifest';
$this->manifest = new ClassManifest($this->base, false); $this->manifest = new ClassManifest($this->base);
$this->manifestTests = new ClassManifest($this->base, true); $this->manifest->init(false);
$this->manifestTests = new ClassManifest($this->base);
$this->manifestTests->init(true);
} }
public function testGetItemPath() public function testGetItemPath()
@ -155,6 +157,7 @@ class ClassManifestTest extends SapphireTest
public function testManifestWarnsAboutDuplicateClasses() public function testManifestWarnsAboutDuplicateClasses()
{ {
$this->expectException(Exception::class); $this->expectException(Exception::class);
new ClassManifest(dirname(__FILE__) . '/fixtures/classmanifest_duplicates', false); $manifest = new ClassManifest(dirname(__FILE__) . '/fixtures/classmanifest_duplicates');
$manifest->init();
} }
} }

View File

@ -4,8 +4,9 @@ namespace SilverStripe\Core\Tests\Manifest;
use Dotenv\Loader; use Dotenv\Loader;
use SilverStripe\Config\Collections\MemoryConfigCollection; use SilverStripe\Config\Collections\MemoryConfigCollection;
use SilverStripe\Control\Director;
use SilverStripe\Core\Config\CoreConfigFactory; use SilverStripe\Core\Config\CoreConfigFactory;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Kernel;
use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Core\Manifest\ModuleManifest; use SilverStripe\Core\Manifest\ModuleManifest;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
@ -17,6 +18,7 @@ class ConfigManifestTest extends SapphireTest
parent::setUp(); parent::setUp();
$moduleManifest = new ModuleManifest(dirname(__FILE__) . '/fixtures/configmanifest'); $moduleManifest = new ModuleManifest(dirname(__FILE__) . '/fixtures/configmanifest');
$moduleManifest->init();
ModuleLoader::inst()->pushManifest($moduleManifest); ModuleLoader::inst()->pushManifest($moduleManifest);
} }
@ -45,7 +47,8 @@ class ConfigManifestTest extends SapphireTest
public function getTestConfig() public function getTestConfig()
{ {
$config = new MemoryConfigCollection(); $config = new MemoryConfigCollection();
$transformer = CoreConfigFactory::inst()->buildYamlTransformerForPath(dirname(__FILE__) . '/fixtures/configmanifest'); $factory = new CoreConfigFactory();
$transformer = $factory->buildYamlTransformerForPath(dirname(__FILE__) . '/fixtures/configmanifest');
$config->transform([$transformer]); $config->transform([$transformer]);
return $config; return $config;
} }
@ -173,8 +176,10 @@ class ConfigManifestTest extends SapphireTest
public function testEnvironmentRules() public function testEnvironmentRules()
{ {
/** @var Kernel $kernel */
$kernel = Injector::inst()->get(Kernel::class);
foreach (array('dev', 'test', 'live') as $env) { foreach (array('dev', 'test', 'live') as $env) {
Director::set_environment_type($env); $kernel->setEnvironment($env);
$config = $this->getConfigFixtureValue('Environment'); $config = $this->getConfigFixtureValue('Environment');
foreach (array('dev', 'test', 'live') as $check) { foreach (array('dev', 'test', 'live') as $check) {

View File

@ -1,6 +1,5 @@
<?php <?php
namespace SilverStripe\Core\Tests\Manifest; namespace SilverStripe\Core\Tests\Manifest;
use SilverStripe\Core\Manifest\ModuleManifest; use SilverStripe\Core\Manifest\ModuleManifest;
@ -23,7 +22,8 @@ class ModuleManifestTest extends SapphireTest
parent::setUp(); parent::setUp();
$this->base = dirname(__FILE__) . '/fixtures/classmanifest'; $this->base = dirname(__FILE__) . '/fixtures/classmanifest';
$this->manifest = new ModuleManifest($this->base, false); $this->manifest = new ModuleManifest($this->base);
$this->manifest->init();
} }
public function testGetModules() public function testGetModules()

View File

@ -28,7 +28,8 @@ class NamespacedClassManifestTest extends SapphireTest
parent::setUp(); parent::setUp();
$this->base = dirname(__FILE__) . '/fixtures/namespaced_classmanifest'; $this->base = dirname(__FILE__) . '/fixtures/namespaced_classmanifest';
$this->manifest = new ClassManifest($this->base, false); $this->manifest = new ClassManifest($this->base);
$this->manifest->init();
ClassLoader::inst()->pushManifest($this->manifest, false); ClassLoader::inst()->pushManifest($this->manifest, false);
} }

View File

@ -36,7 +36,8 @@ class ThemeResourceLoaderTest extends SapphireTest
// Fake project root // Fake project root
$this->base = dirname(__FILE__) . '/fixtures/templatemanifest'; $this->base = dirname(__FILE__) . '/fixtures/templatemanifest';
// New ThemeManifest for that root // New ThemeManifest for that root
$this->manifest = new ThemeManifest($this->base, 'myproject', false); $this->manifest = new ThemeManifest($this->base, 'myproject');
$this->manifest->init();
// New Loader for that root // New Loader for that root
$this->loader = new ThemeResourceLoader($this->base); $this->loader = new ThemeResourceLoader($this->base);
$this->loader->addSet('$default', $this->manifest); $this->loader->addSet('$default', $this->manifest);

View File

@ -1,6 +1 @@
<?php <?php
use SilverStripe\Control\Director;
// Dynamically change environment
Director::set_environment_type('dev');

View File

@ -2,117 +2,96 @@
namespace SilverStripe\Core\Tests; namespace SilverStripe\Core\Tests;
use SilverStripe\Core\Environment;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
class MemoryLimitTest extends SapphireTest class MemoryLimitTest extends SapphireTest
{ {
protected $origMemLimitMax;
public function testIncreaseMemoryLimitTo() protected $origTimeLimitMax;
{ protected $origMemLimit;
if (!$this->canChangeMemory()) { protected $origTimeLimit;
return;
}
ini_set('memory_limit', '64M');
// It can go up
increase_memory_limit_to('128M');
$this->assertEquals('128M', ini_get('memory_limit'));
// But not down
increase_memory_limit_to('64M');
$this->assertEquals('128M', ini_get('memory_limit'));
// Test the different kinds of syntaxes
increase_memory_limit_to(1024*1024*200);
$this->assertEquals(1024*1024*200, ini_get('memory_limit'));
increase_memory_limit_to('409600K');
$this->assertEquals('409600K', ini_get('memory_limit'));
increase_memory_limit_to('1G');
// If memory limit was left at 409600K, that means that the current testbox doesn't have
// 1G of memory available. That's okay; let's not report a failure for that.
if (ini_get('memory_limit') != '409600K') {
$this->assertEquals('1G', ini_get('memory_limit'));
}
// No argument means unlimited
increase_memory_limit_to();
$this->assertEquals(-1, ini_get('memory_limit'));
}
public function testIncreaseTimeLimitTo()
{
if (!$this->canChangeMemory()) {
return;
}
// Can't change time limit
if (!set_time_limit(6000)) {
return;
}
// It can go up
$this->assertTrue(increase_time_limit_to(7000));
$this->assertEquals(7000, ini_get('max_execution_time'));
// But not down
$this->assertTrue(increase_time_limit_to(5000));
$this->assertEquals(7000, ini_get('max_execution_time'));
// 0/nothing means infinity
$this->assertTrue(increase_time_limit_to());
$this->assertEquals(0, ini_get('max_execution_time'));
// Can't go down from there
$this->assertTrue(increase_time_limit_to(10000));
$this->assertEquals(0, ini_get('max_execution_time'));
}
///////////////////
private $origMemLimit, $origTimeLimit;
protected function setUp() protected function setUp()
{ {
parent::setUp();
// see http://www.hardened-php.net/suhosin/configuration.html#suhosin.memory_limit
if (in_array('suhosin', get_loaded_extensions())) {
$this->markTestSkipped("This test cannot be run with suhosin installed");
} else {
$this->origMemLimit = ini_get('memory_limit'); $this->origMemLimit = ini_get('memory_limit');
$this->origTimeLimit = ini_get('max_execution_time'); $this->origTimeLimit = ini_get('max_execution_time');
$this->origMemLimitMax = get_increase_memory_limit_max(); $this->origMemLimitMax = Environment::getMemoryLimitMax();
$this->origTimeLimitMax = get_increase_time_limit_max(); $this->origTimeLimitMax = Environment::getTimeLimitMax();
set_increase_memory_limit_max(-1); Environment::setMemoryLimitMax(null);
set_increase_time_limit_max(-1); Environment::setTimeLimitMax(null);
}
} }
protected function tearDown() protected function tearDown()
{ {
if (!in_array('suhosin', get_loaded_extensions())) {
ini_set('memory_limit', $this->origMemLimit); ini_set('memory_limit', $this->origMemLimit);
set_time_limit($this->origTimeLimit); set_time_limit($this->origTimeLimit);
set_increase_memory_limit_max($this->origMemLimitMax); Environment::setMemoryLimitMax($this->origMemLimitMax);
set_increase_time_limit_max($this->origTimeLimitMax); Environment::setTimeLimitMax($this->origTimeLimitMax);
}
parent::tearDown();
} }
/** public function testIncreaseMemoryLimitTo()
* Determines wether the environment generally allows
* to change the memory limits, which is not always the case.
*
* @return Boolean
*/
protected function canChangeMemory()
{ {
$exts = get_loaded_extensions(); ini_set('memory_limit', '64M');
// see http://www.hardened-php.net/suhosin/configuration.html#suhosin.memory_limit Environment::setMemoryLimitMax('256M');
if (in_array('suhosin', $exts)) {
return false; // It can go up
Environment::increaseMemoryLimitTo('128M');
$this->assertEquals('128M', ini_get('memory_limit'));
// But not down
Environment::increaseMemoryLimitTo('64M');
$this->assertEquals('128M', ini_get('memory_limit'));
// Test the different kinds of syntaxes
Environment::increaseMemoryLimitTo(1024*1024*200);
$this->assertEquals('200M', ini_get('memory_limit'));
Environment::increaseMemoryLimitTo('109600K');
$this->assertEquals('200M', ini_get('memory_limit'));
// Attempting to increase past max size only sets to max
Environment::increaseMemoryLimitTo('1G');
$this->assertEquals('256M', ini_get('memory_limit'));
// No argument means unlimited (but only if originally allowed)
if (is_numeric($this->origMemLimitMax) && $this->origMemLimitMax < 0) {
Environment::increaseMemoryLimitTo();
$this->assertEquals(-1, ini_get('memory_limit'));
}
} }
// We can't change memory limit in safe mode public function testIncreaseTimeLimitTo()
if (ini_get('safe_mode')) { {
return false; // Can't change time limit
if (!set_time_limit(6000)) {
$this->markTestSkipped("Cannot change time limit");
} }
return true; // It can go up
$this->assertTrue(Environment::increaseTimeLimitTo(7000));
$this->assertEquals(7000, ini_get('max_execution_time'));
// But not down
$this->assertTrue(Environment::increaseTimeLimitTo(5000));
$this->assertEquals(7000, ini_get('max_execution_time'));
// 0/nothing means infinity
$this->assertTrue(Environment::increaseTimeLimitTo());
$this->assertEquals(0, ini_get('max_execution_time'));
// Can't go down from there
$this->assertTrue(Environment::increaseTimeLimitTo(10000));
$this->assertEquals(0, ini_get('max_execution_time'));
} }
} }

View File

@ -2,8 +2,13 @@
namespace SilverStripe\Core\Tests; namespace SilverStripe\Core\Tests;
use SilverStripe\Control\Controller;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Extension;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Tests\ObjectTest\BaseObject;
use SilverStripe\Core\Tests\ObjectTest\ExtendTest1; use SilverStripe\Core\Tests\ObjectTest\ExtendTest1;
use SilverStripe\Core\Tests\ObjectTest\ExtendTest2; use SilverStripe\Core\Tests\ObjectTest\ExtendTest2;
use SilverStripe\Core\Tests\ObjectTest\ExtendTest3; use SilverStripe\Core\Tests\ObjectTest\ExtendTest3;
@ -16,7 +21,6 @@ use SilverStripe\Core\Tests\ObjectTest\MyObject;
use SilverStripe\Core\Tests\ObjectTest\MySubObject; use SilverStripe\Core\Tests\ObjectTest\MySubObject;
use SilverStripe\Core\Tests\ObjectTest\TestExtension; use SilverStripe\Core\Tests\ObjectTest\TestExtension;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Controller;
use SilverStripe\Versioned\Versioned; use SilverStripe\Versioned\Versioned;
/** /**
@ -30,7 +34,10 @@ class ObjectTest extends SapphireTest
protected function setUp() protected function setUp()
{ {
parent::setUp(); parent::setUp();
Injector::inst()->unregisterAllObjects(); Injector::inst()->unregisterObjects([
Extension::class,
BaseObject::class,
]);
} }
public function testHasmethodBehaviour() public function testHasmethodBehaviour()

View File

@ -2,10 +2,10 @@
namespace SilverStripe\Core\Tests\Startup; namespace SilverStripe\Core\Tests\Startup;
use Exception;
use Foo;
use SilverStripe\Core\Startup\ErrorControlChain; use SilverStripe\Core\Startup\ErrorControlChain;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use Foo;
use Exception;
class ErrorControlChainTest extends SapphireTest class ErrorControlChainTest extends SapphireTest
{ {

View File

@ -2,6 +2,9 @@
namespace SilverStripe\Core\Tests\Startup; namespace SilverStripe\Core\Tests\Startup;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Core\Startup\ParameterConfirmationToken; use SilverStripe\Core\Startup\ParameterConfirmationToken;
use SilverStripe\Core\Tests\Startup\ParameterConfirmationTokenTest\ParameterConfirmationTokenTest_Token; use SilverStripe\Core\Tests\Startup\ParameterConfirmationTokenTest\ParameterConfirmationTokenTest_Token;
use SilverStripe\Core\Tests\Startup\ParameterConfirmationTokenTest\ParameterConfirmationTokenTest_ValidToken; use SilverStripe\Core\Tests\Startup\ParameterConfirmationTokenTest\ParameterConfirmationTokenTest_ValidToken;
@ -9,56 +12,35 @@ use SilverStripe\Dev\SapphireTest;
class ParameterConfirmationTokenTest extends SapphireTest class ParameterConfirmationTokenTest extends SapphireTest
{ {
/**
private function addPart($answer, $slash, $part) * @var HTTPRequest
{ */
$bare = str_replace('/', '', $part); protected $request = null;
if ($bare) {
$answer = array_merge($answer, array($bare));
}
if ($part) {
$slash = (substr($part, -1) == '/') ? '/' : '';
}
return array($answer, $slash);
}
protected $oldHost = null;
protected function setUp() protected function setUp()
{ {
parent::setUp(); parent::setUp();
$this->oldHost = $_SERVER['HTTP_HOST']; $get = [];
$_GET['parameterconfirmationtokentest_notoken'] = 'value'; $get['parameterconfirmationtokentest_notoken'] = 'value';
$_GET['parameterconfirmationtokentest_empty'] = ''; $get['parameterconfirmationtokentest_empty'] = '';
$_GET['parameterconfirmationtokentest_withtoken'] = '1'; $get['parameterconfirmationtokentest_withtoken'] = '1';
$_GET['parameterconfirmationtokentest_withtokentoken'] = 'dummy'; $get['parameterconfirmationtokentest_withtokentoken'] = 'dummy';
$_GET['parameterconfirmationtokentest_nulltoken'] = '1'; $get['parameterconfirmationtokentest_nulltoken'] = '1';
$_GET['parameterconfirmationtokentest_nulltokentoken'] = null; $get['parameterconfirmationtokentest_nulltokentoken'] = null;
$_GET['parameterconfirmationtokentest_emptytoken'] = '1'; $get['parameterconfirmationtokentest_emptytoken'] = '1';
$_GET['parameterconfirmationtokentest_emptytokentoken'] = ''; $get['parameterconfirmationtokentest_emptytokentoken'] = '';
} $this->request = new HTTPRequest('GET', '/', $get);
$this->request->setSession(new Session([]));
protected function tearDown()
{
foreach ($_GET as $param => $value) {
if (stripos($param, 'parameterconfirmationtokentest_') === 0) {
unset($_GET[$param]);
}
}
$_SERVER['HTTP_HOST'] = $this->oldHost;
parent::tearDown();
} }
public function testParameterDetectsParameters() public function testParameterDetectsParameters()
{ {
$withoutToken = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_notoken'); $withoutToken = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_notoken', $this->request);
$emptyParameter = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_empty'); $emptyParameter = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_empty', $this->request);
$withToken = new ParameterConfirmationTokenTest_ValidToken('parameterconfirmationtokentest_withtoken'); $withToken = new ParameterConfirmationTokenTest_ValidToken('parameterconfirmationtokentest_withtoken', $this->request);
$withoutParameter = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_noparam'); $withoutParameter = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_noparam', $this->request);
$nullToken = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_nulltoken'); $nullToken = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_nulltoken', $this->request);
$emptyToken = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_emptytoken'); $emptyToken = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_emptytoken', $this->request);
// Check parameter // Check parameter
$this->assertTrue($withoutToken->parameterProvided()); $this->assertTrue($withoutToken->parameterProvided());
@ -85,27 +67,27 @@ class ParameterConfirmationTokenTest extends SapphireTest
$this->assertTrue($emptyToken->reloadRequired()); $this->assertTrue($emptyToken->reloadRequired());
// Check suppression // Check suppression
$this->assertTrue(isset($_GET['parameterconfirmationtokentest_notoken'])); $this->assertEquals('value', $this->request->getVar('parameterconfirmationtokentest_notoken'));
$withoutToken->suppress(); $withoutToken->suppress();
$this->assertFalse(isset($_GET['parameterconfirmationtokentest_notoken'])); $this->assertNull($this->request->getVar('parameterconfirmationtokentest_notoken'));
} }
public function testPrepareTokens() public function testPrepareTokens()
{ {
// Test priority ordering // Test priority ordering
$token = ParameterConfirmationToken::prepare_tokens( $token = ParameterConfirmationToken::prepare_tokens(
array( [
'parameterconfirmationtokentest_notoken', 'parameterconfirmationtokentest_notoken',
'parameterconfirmationtokentest_empty', 'parameterconfirmationtokentest_empty',
'parameterconfirmationtokentest_noparam' 'parameterconfirmationtokentest_noparam'
) ],
$this->request
); );
// Test no invalid tokens // Test no invalid tokens
$this->assertEquals('parameterconfirmationtokentest_empty', $token->getName()); $this->assertEquals('parameterconfirmationtokentest_empty', $token->getName());
$token = ParameterConfirmationToken::prepare_tokens( $token = ParameterConfirmationToken::prepare_tokens(
array( [ 'parameterconfirmationtokentest_noparam' ],
'parameterconfirmationtokentest_noparam' $this->request
)
); );
$this->assertEmpty($token); $this->assertEmpty($token);
} }
@ -118,25 +100,15 @@ class ParameterConfirmationTokenTest extends SapphireTest
*/ */
public function testCurrentAbsoluteURLHandlesSlashes() public function testCurrentAbsoluteURLHandlesSlashes()
{ {
global $url; $token = new ParameterConfirmationTokenTest_Token(
'parameterconfirmationtokentest_parameter',
$this->request
);
$token = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_parameter'); foreach (array('', '/', 'bar', 'bar/', '/bar', '/bar/') as $url) {
$this->request->setUrl($url);
foreach (array('foo','foo/') as $host) { $expected = rtrim(Controller::join_links(BASE_URL, '/', $url), '/') ?: '/';
list($hostAnswer, $hostSlash) = $this->addPart(array(), '', $host); $this->assertEquals($expected, $token->currentURL(), "Invalid redirect for request url $url");
foreach (array('', '/', 'bar', 'bar/', '/bar', '/bar/') as $base) {
list($baseAnswer, $baseSlash) = $this->addPart($hostAnswer, $hostSlash, $base);
foreach (array('', '/', 'baz', 'baz/', '/baz', '/baz/') as $url) {
list($urlAnswer, $urlSlash) = $this->addPart($baseAnswer, $baseSlash, $url);
$_SERVER['HTTP_HOST'] = $host;
ParameterConfirmationToken::$alternateBaseURL = $base;
$this->assertEquals('http://'.implode('/', $urlAnswer) . $urlSlash, $token->currentAbsoluteURL());
}
}
} }
} }
} }

View File

@ -11,8 +11,8 @@ use SilverStripe\Dev\TestOnly;
class ParameterConfirmationTokenTest_Token extends ParameterConfirmationToken implements TestOnly class ParameterConfirmationTokenTest_Token extends ParameterConfirmationToken implements TestOnly
{ {
public function currentAbsoluteURL() public function currentURL()
{ {
return parent::currentAbsoluteURL(); return parent::currentURL();
} }
} }

View File

@ -2,8 +2,6 @@
namespace SilverStripe\Core\Tests\Startup\ParameterConfirmationTokenTest; namespace SilverStripe\Core\Tests\Startup\ParameterConfirmationTokenTest;
use SilverStripe\Core\Tests\Startup\ParameterConfirmationTokenTest\ParameterConfirmationTokenTest_Token;
/** /**
* A token that always validates a given token * A token that always validates a given token
*/ */

View File

@ -169,17 +169,14 @@ class CheckboxSetFieldTest extends SapphireTest
public function testLoadDataFromObject() public function testLoadDataFromObject()
{ {
$article = $this->objFromFixture(Article::class, 'articlewithouttags');
$articleWithTags = $this->objFromFixture(Article::class, 'articlewithtags'); $articleWithTags = $this->objFromFixture(Article::class, 'articlewithtags');
$tag1 = $this->objFromFixture(Tag::class, 'tag1'); $tag1 = $this->objFromFixture(Tag::class, 'tag1');
$tag2 = $this->objFromFixture(Tag::class, 'tag2'); $tag2 = $this->objFromFixture(Tag::class, 'tag2');
$field = new CheckboxSetField("Tags", "Test field", DataObject::get(Tag::class)->map()); $field = new CheckboxSetField("Tags", "Test field", DataObject::get(Tag::class)->map());
/** /** @skipUpgrade */
* @skipUpgrade
*/
$form = new Form( $form = new Form(
new Controller(), Controller::curr(),
'Form', 'Form',
new FieldList($field), new FieldList($field),
new FieldList() new FieldList()

View File

@ -2,14 +2,13 @@
namespace SilverStripe\Forms\Tests; namespace SilverStripe\Forms\Tests;
use SilverStripe\Control\Tests\ControllerTest\TestController;
use SilverStripe\Security\Member;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\ConfirmedPasswordField; use SilverStripe\Forms\ConfirmedPasswordField;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Forms\RequiredFields; use SilverStripe\Forms\RequiredFields;
use SilverStripe\Security\Member;
class ConfirmedPasswordFieldTest extends SapphireTest class ConfirmedPasswordFieldTest extends SapphireTest
{ {
@ -39,10 +38,8 @@ class ConfirmedPasswordFieldTest extends SapphireTest
$member->Password = "valueB"; $member->Password = "valueB";
$member->write(); $member->write();
/** /** @skipUpgrade */
* @skipUpgrade $form = new Form(Controller::curr(), 'Form', new FieldList($field), new FieldList());
*/
$form = new Form(new TestController(), 'Form', new FieldList($field), new FieldList());
$form->loadDataFrom($member); $form->loadDataFrom($member);
$this->assertEquals('', $field->Value()); $this->assertEquals('', $field->Value());
@ -92,10 +89,8 @@ class ConfirmedPasswordFieldTest extends SapphireTest
) )
); );
$validator = new RequiredFields(); $validator = new RequiredFields();
/** /** @skipUpgrade */
* @skipUpgrade new Form(Controller::curr(), 'Form', new FieldList($field), new FieldList(), $validator);
*/
$form = new Form(new TestController(), 'Form', new FieldList($field), new FieldList(), $validator);
$this->assertTrue( $this->assertTrue(
$field->validate($validator), $field->validate($validator),
"Validates when both passwords are the same" "Validates when both passwords are the same"
@ -120,11 +115,9 @@ class ConfirmedPasswordFieldTest extends SapphireTest
public function testFormValidation() public function testFormValidation()
{ {
/** /** @skipUpgrade */
* @skipUpgrade
*/
$form = new Form( $form = new Form(
new Controller(), Controller::curr(),
'Form', 'Form',
new FieldList($field = new ConfirmedPasswordField('Password')), new FieldList($field = new ConfirmedPasswordField('Password')),
new FieldList() new FieldList()

View File

@ -2,16 +2,14 @@
namespace SilverStripe\Forms\Tests; namespace SilverStripe\Forms\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\DatetimeField; use SilverStripe\Forms\DatetimeField;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\DateField;
use SilverStripe\Forms\Tests\DatetimeFieldTest\Model;
use SilverStripe\Forms\TimeField;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\Tests\DatetimeFieldTest\Model;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
@ -452,7 +450,7 @@ class DatetimeFieldTest extends SapphireTest
{ {
/** @skipUpgrade */ /** @skipUpgrade */
return new Form( return new Form(
new Controller(), Controller::curr(),
'Form', 'Form',
new FieldList(), new FieldList(),
new FieldList( new FieldList(

View File

@ -479,7 +479,7 @@ class DropdownFieldTest extends SapphireTest
) )
); );
$validator = new RequiredFields(); $validator = new RequiredFields();
$form = new Form(null, 'Form', new FieldList($field), new FieldList(), $validator); new Form(null, 'Form', new FieldList($field), new FieldList(), $validator);
$field->setValue("One"); $field->setValue("One");
$this->assertTrue($field->validate($validator)); $this->assertTrue($field->validate($validator));
$field->setName("TestNew"); //try changing name of field $field->setName("TestNew"); //try changing name of field

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Forms\Tests\EmailFieldTest; namespace SilverStripe\Forms\Tests\EmailFieldTest;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
use SilverStripe\Forms\EmailField; use SilverStripe\Forms\EmailField;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
@ -16,6 +17,13 @@ use SilverStripe\View\SSViewer;
*/ */
class TestController extends Controller implements TestOnly class TestController extends Controller implements TestOnly
{ {
public function __construct()
{
parent::__construct();
if (Controller::has_curr()) {
$this->setRequest(Controller::curr()->getRequest());
}
}
private static $allowed_actions = array('Form'); private static $allowed_actions = array('Form');
@ -25,11 +33,9 @@ class TestController extends Controller implements TestOnly
protected $template = 'BlankPage'; protected $template = 'BlankPage';
function Link($action = null) public function Link($action = null)
{ {
/** /** @skipUpgrade */
* @skipUpgrade
*/
return Controller::join_links( return Controller::join_links(
'EmailFieldTest_Controller', 'EmailFieldTest_Controller',
$this->getRequest()->latestParam('Action'), $this->getRequest()->latestParam('Action'),
@ -38,7 +44,10 @@ class TestController extends Controller implements TestOnly
); );
} }
function Form() /**
* @return Form
*/
public function Form()
{ {
$form = new Form( $form = new Form(
$this, $this,
@ -60,13 +69,13 @@ class TestController extends Controller implements TestOnly
return $form; return $form;
} }
function doSubmit($data, $form, $request) public function doSubmit($data, Form $form, HTTPRequest $request)
{ {
$form->sessionMessage('Test save was successful', 'good'); $form->sessionMessage('Test save was successful', 'good');
return $this->redirectBack(); return $this->redirectBack();
} }
function getViewer($action = null) public function getViewer($action = null)
{ {
return new SSViewer('BlankPage'); return new SSViewer('BlankPage');
} }

View File

@ -20,7 +20,7 @@ class FileFieldTest extends FunctionalTest
public function testUploadRequiredFile() public function testUploadRequiredFile()
{ {
$form = new Form( $form = new Form(
new Controller(), Controller::curr(),
'Form', 'Form',
new FieldList( new FieldList(
$fileField = new FileField('cv', 'Upload your CV') $fileField = new FileField('cv', 'Upload your CV')
@ -46,7 +46,7 @@ class FileFieldTest extends FunctionalTest
public function testUploadMissingRequiredFile() public function testUploadMissingRequiredFile()
{ {
$form = new Form( $form = new Form(
new Controller(), Controller::curr(),
'Form', 'Form',
new FieldList( new FieldList(
$fileField = new FileField('cv', 'Upload your CV') $fileField = new FileField('cv', 'Upload your CV')

View File

@ -19,7 +19,7 @@ class FormFactoryTest extends SapphireTest
protected static $fixture_file = 'FormFactoryTest.yml'; protected static $fixture_file = 'FormFactoryTest.yml';
protected static function getExtraDataObjects() public static function getExtraDataObjects()
{ {
// Prevent setup breaking if versioned module absent // Prevent setup breaking if versioned module absent
if (class_exists(Versioned::class)) { if (class_exists(Versioned::class)) {
@ -51,7 +51,6 @@ class FormFactoryTest extends SapphireTest
$this->assertInstanceOf(HiddenField::class, $form->Fields()->fieldByName('ID')); $this->assertInstanceOf(HiddenField::class, $form->Fields()->fieldByName('ID'));
$this->assertInstanceOf(HiddenField::class, $form->Fields()->fieldByName('SecurityID')); $this->assertInstanceOf(HiddenField::class, $form->Fields()->fieldByName('SecurityID'));
// Check preview link // Check preview link
/** @var LiteralField $previewLink */ /** @var LiteralField $previewLink */
$previewLink = $form->Fields()->fieldByName('PreviewLink'); $previewLink = $form->Fields()->fieldByName('PreviewLink');

View File

@ -12,6 +12,14 @@ use SilverStripe\Versioned\Versioned;
*/ */
class TestController extends Controller class TestController extends Controller
{ {
public function __construct()
{
parent::__construct();
if (Controller::has_curr()) {
$this->setRequest(Controller::curr()->getRequest());
}
}
private static $extensions = [ private static $extensions = [
ControllerExtension::class, ControllerExtension::class,
]; ];
@ -25,6 +33,9 @@ class TestController extends Controller
); );
} }
/**
* @return Form
*/
public function Form() public function Form()
{ {
// Simple example; Just get the first draft record // Simple example; Just get the first draft record

View File

@ -2,16 +2,18 @@
namespace SilverStripe\Forms\Tests; namespace SilverStripe\Forms\Tests;
use SilverStripe\Core\Config\Config; use ReflectionClass;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\FormField; use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\Tests\FormFieldTest\TestExtension;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use ReflectionClass; use SilverStripe\Forms\FormField;
use SilverStripe\Forms\NullableField;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\Tests\FormFieldTest\TestExtension;
use SilverStripe\Forms\TextField;
class FormFieldTest extends SapphireTest class FormFieldTest extends SapphireTest
{ {
@ -205,7 +207,7 @@ class FormFieldTest extends SapphireTest
public function testEveryFieldTransformsReadonlyAsClone() public function testEveryFieldTransformsReadonlyAsClone()
{ {
$fieldClasses = ClassInfo::subclassesFor('SilverStripe\\Forms\\FormField'); $fieldClasses = ClassInfo::subclassesFor(FormField::class);
foreach ($fieldClasses as $fieldClass) { foreach ($fieldClasses as $fieldClass) {
$reflectionClass = new ReflectionClass($fieldClass); $reflectionClass = new ReflectionClass($fieldClass);
if (!$reflectionClass->isInstantiable()) { if (!$reflectionClass->isInstantiable()) {
@ -215,12 +217,13 @@ class FormFieldTest extends SapphireTest
if ($constructor->getNumberOfRequiredParameters() > 1) { if ($constructor->getNumberOfRequiredParameters() > 1) {
continue; continue;
} }
if (is_a($fieldClass, 'SilverStripe\\Forms\\CompositeField', true)) { if (is_a($fieldClass, CompositeField::class, true)) {
continue; continue;
} }
$fieldName = $reflectionClass->getShortName() . '_instance'; $fieldName = $reflectionClass->getShortName() . '_instance';
if ($fieldClass = 'SilverStripe\\Forms\\NullableField') { /** @var FormField $instance */
if ($fieldClass = NullableField::class) {
$instance = new $fieldClass(new TextField($fieldName)); $instance = new $fieldClass(new TextField($fieldName));
} else { } else {
$instance = new $fieldClass($fieldName); $instance = new $fieldClass($fieldName);
@ -246,7 +249,7 @@ class FormFieldTest extends SapphireTest
public function testEveryFieldTransformsDisabledAsClone() public function testEveryFieldTransformsDisabledAsClone()
{ {
$fieldClasses = ClassInfo::subclassesFor('SilverStripe\\Forms\\FormField'); $fieldClasses = ClassInfo::subclassesFor(FormField::class);
foreach ($fieldClasses as $fieldClass) { foreach ($fieldClasses as $fieldClass) {
$reflectionClass = new ReflectionClass($fieldClass); $reflectionClass = new ReflectionClass($fieldClass);
if (!$reflectionClass->isInstantiable()) { if (!$reflectionClass->isInstantiable()) {
@ -256,12 +259,13 @@ class FormFieldTest extends SapphireTest
if ($constructor->getNumberOfRequiredParameters() > 1) { if ($constructor->getNumberOfRequiredParameters() > 1) {
continue; continue;
} }
if (is_a($fieldClass, 'SilverStripe\\Forms\\CompositeField', true)) { if (is_a($fieldClass, CompositeField::class, true)) {
continue; continue;
} }
$fieldName = $reflectionClass->getShortName() . '_instance'; $fieldName = $reflectionClass->getShortName() . '_instance';
if ($fieldClass = 'SilverStripe\\Forms\\NullableField') { /** @var FormField $instance */
if ($fieldClass = NullableField::class) {
$instance = new $fieldClass(new TextField($fieldName)); $instance = new $fieldClass(new TextField($fieldName));
} else { } else {
$instance = new $fieldClass($fieldName); $instance = new $fieldClass($fieldName);
@ -324,7 +328,7 @@ class FormFieldTest extends SapphireTest
$field = new FormField('MyField'); $field = new FormField('MyField');
// Make sure the user can update values. // Make sure the user can update values.
$field = $field->setSchemaData(['name' => 'MyUpdatedField']); $field->setSchemaData(['name' => 'MyUpdatedField']);
$schema = $field->getSchemaData(); $schema = $field->getSchemaData();
$this->assertEquals($schema['name'], 'MyUpdatedField'); $this->assertEquals($schema['name'], 'MyUpdatedField');
@ -347,12 +351,12 @@ class FormFieldTest extends SapphireTest
$field = new FormField('MyField'); $field = new FormField('MyField');
// Make sure the user can update values. // Make sure the user can update values.
$field = $field->setSchemaState(['value' => 'My custom value']); $field->setSchemaState(['value' => 'My custom value']);
$schema = $field->getSchemaState(); $schema = $field->getSchemaState();
$this->assertEquals($schema['value'], 'My custom value'); $this->assertEquals($schema['value'], 'My custom value');
// Make user the user can't define custom keys on the schema. // Make user the user can't define custom keys on the schema.
$field = $field->setSchemaState(['myCustomKey' => 'yolo']); $field->setSchemaState(['myCustomKey' => 'yolo']);
$schema = $field->getSchemaState(); $schema = $field->getSchemaState();
$this->assertEquals(array_key_exists('myCustomKey', $schema), false); $this->assertEquals(array_key_exists('myCustomKey', $schema), false);
} }

View File

@ -4,6 +4,7 @@ namespace SilverStripe\Forms\Tests;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
@ -16,7 +17,7 @@ class FormRequestHandlerTest extends SapphireTest
public function testCallsActionOnFormHandler() public function testCallsActionOnFormHandler()
{ {
$form = new TestForm( $form = new TestForm(
new Controller(), Controller::curr(),
'Form', 'Form',
new FieldList(), new FieldList(),
new FieldList(new FormAction('mySubmitOnFormHandler')) new FieldList(new FormAction('mySubmitOnFormHandler'))
@ -24,6 +25,7 @@ class FormRequestHandlerTest extends SapphireTest
$form->disableSecurityToken(); $form->disableSecurityToken();
$handler = new TestFormRequestHandler($form); $handler = new TestFormRequestHandler($form);
$request = new HTTPRequest('POST', '/', null, ['action_mySubmitOnFormHandler' => 1]); $request = new HTTPRequest('POST', '/', null, ['action_mySubmitOnFormHandler' => 1]);
$request->setSession(new Session([]));
$response = $handler->httpSubmission($request); $response = $handler->httpSubmission($request);
$this->assertFalse($response->isError()); $this->assertFalse($response->isError());
} }
@ -31,7 +33,7 @@ class FormRequestHandlerTest extends SapphireTest
public function testCallsActionOnForm() public function testCallsActionOnForm()
{ {
$form = new TestForm( $form = new TestForm(
new Controller(), Controller::curr(),
'Form', 'Form',
new FieldList(), new FieldList(),
new FieldList(new FormAction('mySubmitOnForm')) new FieldList(new FormAction('mySubmitOnForm'))
@ -39,6 +41,7 @@ class FormRequestHandlerTest extends SapphireTest
$form->disableSecurityToken(); $form->disableSecurityToken();
$handler = new FormRequestHandler($form); $handler = new FormRequestHandler($form);
$request = new HTTPRequest('POST', '/', null, ['action_mySubmitOnForm' => 1]); $request = new HTTPRequest('POST', '/', null, ['action_mySubmitOnForm' => 1]);
$request->setSession(new Session([]));
$response = $handler->httpSubmission($request); $response = $handler->httpSubmission($request);
$this->assertFalse($response->isError()); $this->assertFalse($response->isError());
} }

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Forms\Tests; namespace SilverStripe\Forms\Tests;
use SilverStripe\Control\Controller;
use SilverStripe\Forms\CurrencyField; use SilverStripe\Forms\CurrencyField;
use SilverStripe\Forms\DateField; use SilverStripe\Forms\DateField;
use SilverStripe\Forms\NumericField; use SilverStripe\Forms\NumericField;
@ -16,6 +17,16 @@ use SilverStripe\Forms\PopoverField;
class FormSchemaTest extends SapphireTest class FormSchemaTest extends SapphireTest
{ {
protected function setUp()
{
parent::setUp();
// Clear old messages
$session = Controller::curr()->getRequest()->getSession();
$session
->clear("FormInfo.TestForm.result")
->clear("FormInfo.TestForm.data");
}
public function testGetSchema() public function testGetSchema()
{ {
@ -86,6 +97,7 @@ class FormSchemaTest extends SapphireTest
$actions = new FieldList(); $actions = new FieldList();
$validator = new RequiredFields('Title'); $validator = new RequiredFields('Title');
$form = new Form(null, 'TestForm', $fields, $actions, $validator); $form = new Form(null, 'TestForm', $fields, $actions, $validator);
$form->clearMessage();
$form->loadDataFrom( $form->loadDataFrom(
[ [
'Title' => null, 'Title' => null,

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Forms\Tests; namespace SilverStripe\Forms\Tests;
use SilverStripe\Control\Session;
use SilverStripe\Forms\Tests\FormTest\TestController; use SilverStripe\Forms\Tests\FormTest\TestController;
use SilverStripe\Forms\Tests\FormTest\ControllerWithSecurityToken; use SilverStripe\Forms\Tests\FormTest\ControllerWithSecurityToken;
use SilverStripe\Forms\Tests\FormTest\ControllerWithStrictPostCheck; use SilverStripe\Forms\Tests\FormTest\ControllerWithStrictPostCheck;
@ -61,7 +62,7 @@ class FormTest extends FunctionalTest
public function testLoadDataFromRequest() public function testLoadDataFromRequest()
{ {
$form = new Form( $form = new Form(
new Controller(), Controller::curr(),
'Form', 'Form',
new FieldList( new FieldList(
new TextField('key1'), new TextField('key1'),
@ -130,7 +131,7 @@ class FormTest extends FunctionalTest
public function testLoadDataFromUnchangedHandling() public function testLoadDataFromUnchangedHandling()
{ {
$form = new Form( $form = new Form(
new Controller(), Controller::curr(),
'Form', 'Form',
new FieldList( new FieldList(
new TextField('key1'), new TextField('key1'),
@ -158,7 +159,7 @@ class FormTest extends FunctionalTest
public function testLoadDataFromObject() public function testLoadDataFromObject()
{ {
$form = new Form( $form = new Form(
new Controller(), Controller::curr(),
'Form', 'Form',
new FieldList( new FieldList(
new HeaderField('MyPlayerHeader', 'My Player'), new HeaderField('MyPlayerHeader', 'My Player'),
@ -200,7 +201,7 @@ class FormTest extends FunctionalTest
public function testLoadDataFromClearMissingFields() public function testLoadDataFromClearMissingFields()
{ {
$form = new Form( $form = new Form(
new Controller(), Controller::curr(),
'Form', 'Form',
new FieldList( new FieldList(
new HeaderField('MyPlayerHeader', 'My Player'), new HeaderField('MyPlayerHeader', 'My Player'),
@ -250,7 +251,7 @@ class FormTest extends FunctionalTest
{ {
$object = new Team(); $object = new Team();
$form = new Form( $form = new Form(
new Controller(), Controller::curr(),
'Form', 'Form',
new FieldList( new FieldList(
new LookupField('Players', 'Players') new LookupField('Players', 'Players')
@ -280,7 +281,7 @@ class FormTest extends FunctionalTest
public function testLoadDataFromIgnoreFalseish() public function testLoadDataFromIgnoreFalseish()
{ {
$form = new Form( $form = new Form(
new Controller(), Controller::curr(),
'Form', 'Form',
new FieldList( new FieldList(
new TextField('Biography', 'Biography', 'Custom Default') new TextField('Biography', 'Biography', 'Custom Default')
@ -447,7 +448,7 @@ class FormTest extends FunctionalTest
{ {
$this->get('FormTest_Controller'); $this->get('FormTest_Controller');
$result = $this->post( $this->post(
'FormTest_Controller/Form', 'FormTest_Controller/Form',
array( array(
'Email' => 'test@test.com', 'Email' => 'test@test.com',
@ -778,6 +779,7 @@ class FormTest extends FunctionalTest
'action_doSubmit' => 1 'action_doSubmit' => 1
) )
); );
$request->setSession(new Session([]));
$form->getRequestHandler()->httpSubmission($request); $form->getRequestHandler()->httpSubmission($request);
$button = $form->getRequestHandler()->buttonClicked(); $button = $form->getRequestHandler()->buttonClicked();
@ -798,6 +800,7 @@ class FormTest extends FunctionalTest
'action_doSubmit' => 1 'action_doSubmit' => 1
) )
); );
$request->setSession(new Session([]));
$form->getRequestHandler()->httpSubmission($request); $form->getRequestHandler()->httpSubmission($request);
$button = $form->getRequestHandler()->buttonClicked(); $button = $form->getRequestHandler()->buttonClicked();

View File

@ -21,6 +21,13 @@ use SilverStripe\View\SSViewer;
*/ */
class TestController extends Controller implements TestOnly class TestController extends Controller implements TestOnly
{ {
public function __construct()
{
parent::__construct();
if (Controller::has_curr()) {
$this->setRequest(Controller::curr()->getRequest());
}
}
private static $allowed_actions = array('Form'); private static $allowed_actions = array('Form');

View File

@ -2,17 +2,16 @@
namespace SilverStripe\Forms\Tests\GridField; namespace SilverStripe\Forms\Tests\GridField;
use SilverStripe\Dev\Debug; use SilverStripe\Core\Convert;
use SilverStripe\Dev\CSSContentParser;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter;
use SilverStripe\Forms\Tests\GridField\GridFieldAddExistingAutocompleterTest\TestController; use SilverStripe\Forms\Tests\GridField\GridFieldAddExistingAutocompleterTest\TestController;
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Cheerleader; use SilverStripe\Forms\Tests\GridField\GridFieldTest\Cheerleader;
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Permissions; use SilverStripe\Forms\Tests\GridField\GridFieldTest\Permissions;
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Player; use SilverStripe\Forms\Tests\GridField\GridFieldTest\Player;
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Team; use SilverStripe\Forms\Tests\GridField\GridFieldTest\Team;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\CSSContentParser;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter;
class GridFieldAddExistingAutocompleterTest extends FunctionalTest class GridFieldAddExistingAutocompleterTest extends FunctionalTest
{ {

View File

@ -17,6 +17,13 @@ use SilverStripe\Forms\Tests\GridField\GridFieldTest\Player;
*/ */
class TestController extends Controller implements TestOnly class TestController extends Controller implements TestOnly
{ {
public function __construct()
{
parent::__construct();
if (Controller::has_curr()) {
$this->setRequest(Controller::curr()->getRequest());
}
}
private static $allowed_actions = array('Form'); private static $allowed_actions = array('Form');
@ -29,9 +36,7 @@ class TestController extends Controller implements TestOnly
public function Form() public function Form()
{ {
/** /** @var Player $player */
* @var Player $player
*/
$player = Player::get()->find('Email', 'player1@test.com'); $player = Player::get()->find('Email', 'player1@test.com');
$config = GridFieldConfig::create()->addComponents( $config = GridFieldConfig::create()->addComponents(
$relationComponent = new GridFieldAddExistingAutocompleter('before'), $relationComponent = new GridFieldAddExistingAutocompleter('before'),

View File

@ -2,7 +2,17 @@
namespace SilverStripe\Forms\Tests\GridField; namespace SilverStripe\Forms\Tests\GridField;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\Session;
use SilverStripe\Dev\CSSContentParser;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldConfig;
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Cheerleader; use SilverStripe\Forms\Tests\GridField\GridFieldTest\Cheerleader;
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Permissions; use SilverStripe\Forms\Tests\GridField\GridFieldTest\Permissions;
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Player; use SilverStripe\Forms\Tests\GridField\GridFieldTest\Player;
@ -12,15 +22,6 @@ use SilverStripe\ORM\DataList;
use SilverStripe\ORM\ValidationException; use SilverStripe\ORM\ValidationException;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Security\SecurityToken; use SilverStripe\Security\SecurityToken;
use SilverStripe\Dev\CSSContentParser;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\GridField\GridFieldConfig;
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
use SilverStripe\Forms\GridField\GridField;
class GridFieldDeleteActionTest extends SapphireTest class GridFieldDeleteActionTest extends SapphireTest
{ {
@ -91,15 +92,13 @@ class GridFieldDeleteActionTest extends SapphireTest
public function testActionsRequireCSRF() public function testActionsRequireCSRF()
{ {
$this->logInWithPermission('ADMIN'); $this->logInWithPermission('ADMIN');
$this->setExpectedException( $this->expectException(HTTPResponse_Exception::class);
HTTPResponse_Exception::class, $this->expectExceptionMessage(_t(
_t(
"SilverStripe\\Forms\\Form.CSRF_FAILED_MESSAGE", "SilverStripe\\Forms\\Form.CSRF_FAILED_MESSAGE",
"There seems to have been a technical problem. Please click the back button, ". "There seems to have been a technical problem. Please click the back button, ".
"refresh your browser, and try again." "refresh your browser, and try again."
), ));
400 $this->expectExceptionCode(400);
);
$stateID = 'testGridStateActionField'; $stateID = 'testGridStateActionField';
$request = new HTTPRequest( $request = new HTTPRequest(
'POST', 'POST',
@ -110,6 +109,7 @@ class GridFieldDeleteActionTest extends SapphireTest
'SecurityID' => null, 'SecurityID' => null,
) )
); );
$request->setSession(new Session([]));
$this->gridField->gridFieldAlterAction(array('StateID'=>$stateID), $this->form, $request); $this->gridField->gridFieldAlterAction(array('StateID'=>$stateID), $this->form, $request);
} }
@ -118,10 +118,11 @@ class GridFieldDeleteActionTest extends SapphireTest
if (Security::getCurrentUser()) { if (Security::getCurrentUser()) {
Security::setCurrentUser(null); Security::setCurrentUser(null);
} }
$this->setExpectedException(ValidationException::class); $this->expectException(ValidationException::class);
$stateID = 'testGridStateActionField'; $stateID = 'testGridStateActionField';
Session::set( $session = Controller::curr()->getRequest()->getSession();
$session->set(
$stateID, $stateID,
array( array(
'grid' => '', 'grid' => '',
@ -141,6 +142,7 @@ class GridFieldDeleteActionTest extends SapphireTest
$token->getName() => $token->getValue(), $token->getName() => $token->getValue(),
) )
); );
$request->setSession($session);
$this->gridField->gridFieldAlterAction(array('StateID'=>$stateID), $this->form, $request); $this->gridField->gridFieldAlterAction(array('StateID'=>$stateID), $this->form, $request);
$this->assertEquals( $this->assertEquals(
3, 3,
@ -153,7 +155,8 @@ class GridFieldDeleteActionTest extends SapphireTest
{ {
$this->logInWithPermission('ADMIN'); $this->logInWithPermission('ADMIN');
$stateID = 'testGridStateActionField'; $stateID = 'testGridStateActionField';
Session::set( $session = Controller::curr()->getRequest()->getSession();
$session->set(
$stateID, $stateID,
array( array(
'grid'=>'', 'grid'=>'',
@ -173,6 +176,7 @@ class GridFieldDeleteActionTest extends SapphireTest
$token->getName() => $token->getValue(), $token->getName() => $token->getValue(),
) )
); );
$request->setSession($session);
$this->gridField->gridFieldAlterAction(array('StateID'=>$stateID), $this->form, $request); $this->gridField->gridFieldAlterAction(array('StateID'=>$stateID), $this->form, $request);
$this->assertEquals(2, $this->list->count(), 'User should be able to delete records with ADMIN permission.'); $this->assertEquals(2, $this->list->count(), 'User should be able to delete records with ADMIN permission.');
} }
@ -183,11 +187,11 @@ class GridFieldDeleteActionTest extends SapphireTest
$config = GridFieldConfig::create()->addComponent(new GridFieldDeleteAction(true)); $config = GridFieldConfig::create()->addComponent(new GridFieldDeleteAction(true));
$session = Controller::curr()->getRequest()->getSession();
$gridField = new GridField('testfield', 'testfield', $this->list, $config); $gridField = new GridField('testfield', 'testfield', $this->list, $config);
$form = new Form(null, 'mockform', new FieldList(array($this->gridField)), new FieldList()); new Form(null, 'mockform', new FieldList(array($gridField)), new FieldList());
$stateID = 'testGridStateActionField'; $stateID = 'testGridStateActionField';
Session::set( $session->set(
$stateID, $stateID,
array( array(
'grid'=>'', 'grid'=>'',
@ -207,7 +211,8 @@ class GridFieldDeleteActionTest extends SapphireTest
$token->getName() => $token->getValue(), $token->getName() => $token->getValue(),
) )
); );
$this->gridField->gridFieldAlterAction(array('StateID'=>$stateID), $this->form, $request); $request->setSession($session);
$gridField->gridFieldAlterAction(array('StateID'=>$stateID), $this->form, $request);
$this->assertEquals(2, $this->list->count(), 'User should be able to delete records with ADMIN permission.'); $this->assertEquals(2, $this->list->count(), 'User should be able to delete records with ADMIN permission.');
} }
} }

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Forms\Tests\GridField; namespace SilverStripe\Forms\Tests\GridField;
use SilverStripe\Dev\CSSContentParser; use SilverStripe\Dev\CSSContentParser;
use SilverStripe\Dev\Debug;
use SilverStripe\Dev\FunctionalTest; use SilverStripe\Dev\FunctionalTest;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\HiddenField;
@ -244,7 +245,6 @@ class GridFieldDetailFormTest extends FunctionalTest
) )
); );
$this->assertFalse($response->isError()); $this->assertFalse($response->isError());
$person = Person::get()->sort('FirstName')->First(); $person = Person::get()->sort('FirstName')->First();
$category = $person->Categories()->filter(array('Name' => 'Updated Category'))->First(); $category = $person->Categories()->filter(array('Name' => 'Updated Category'))->First();
$this->assertEquals( $this->assertEquals(
@ -363,18 +363,17 @@ class GridFieldDetailFormTest extends FunctionalTest
} }
); );
// Note: A lot of scaffolding to execute the tested logic, // Note: A lot of scaffolding to execute the tested logic,
// due to the coupling of form creation with request handling (and its context) // due to the coupling of form creation with itemRequest handling (and its context)
/** /** @skipUpgrade */
* @skipUpgrade $itemRequest = new GridFieldDetailForm_ItemRequest(
*/
$request = new GridFieldDetailForm_ItemRequest(
GridField::create('Categories', 'Categories'), GridField::create('Categories', 'Categories'),
$component, $component,
$category, $category,
new Controller(), Controller::curr(),
'Form' 'Form'
); );
$form = $request->ItemEditForm(); $itemRequest->setRequest(Controller::curr()->getRequest());
$form = $itemRequest->ItemEditForm();
$this->assertNotNull($form->Fields()->fieldByName('Callback')); $this->assertNotNull($form->Fields()->fieldByName('Callback'));
} }

View File

@ -50,18 +50,14 @@ class CategoryController extends Controller implements TestOnly
$categoriesField->getConfig()->addComponent(new GridFieldEditButton()); $categoriesField->getConfig()->addComponent(new GridFieldEditButton());
$favGroupsField = new GridField('testgroupsfield', 'testgroupsfield', $person->FavouriteGroups()); $favGroupsField = new GridField('testgroupsfield', 'testgroupsfield', $person->FavouriteGroups());
/** /** @skipUpgrade */
* @skipUpgrade
*/
$favGroupsField->getConfig()->addComponent(new GridFieldDetailForm($this, 'Form')); $favGroupsField->getConfig()->addComponent(new GridFieldDetailForm($this, 'Form'));
$favGroupsField->getConfig()->addComponent(new GridFieldToolbarHeader()); $favGroupsField->getConfig()->addComponent(new GridFieldToolbarHeader());
$favGroupsField->getConfig()->addComponent(new GridFieldAddNewButton('toolbar-header-right')); $favGroupsField->getConfig()->addComponent(new GridFieldAddNewButton('toolbar-header-right'));
$favGroupsField->getConfig()->addComponent(new GridFieldEditButton()); $favGroupsField->getConfig()->addComponent(new GridFieldEditButton());
$fields = new FieldList($categoriesField, $favGroupsField); $fields = new FieldList($categoriesField, $favGroupsField);
/** /** @skipUpgrade */
* @skipUpgrade
*/
return new Form($this, 'Form', $fields, new FieldList()); return new Form($this, 'Form', $fields, new FieldList());
} }
} }

View File

@ -15,6 +15,13 @@ use SilverStripe\Forms\GridField\GridFieldViewButton;
class TestController extends Controller implements TestOnly class TestController extends Controller implements TestOnly
{ {
public function __construct()
{
parent::__construct();
if (Controller::has_curr()) {
$this->setRequest(Controller::curr()->getRequest());
}
}
public function Link($action = null) public function Link($action = null)
{ {

View File

@ -43,11 +43,8 @@ class GridFieldPrintButtonTest extends SapphireTest
->addComponent(new GridFieldPaginator(10)) ->addComponent(new GridFieldPaginator(10))
->addComponent($button); ->addComponent($button);
$gridField = new GridField('testfield', 'testfield', $list, $config); $gridField = new GridField('testfield', 'testfield', $list, $config);
$controller = new Controller(); /** @skipUpgrade */
/** new Form(Controller::curr(), 'Form', new FieldList($gridField), new FieldList());
* @skipUpgrade
*/
new Form($controller, 'Form', new FieldList($gridField), new FieldList());
// Printed data should ignore pagination limit // Printed data should ignore pagination limit
$printData = $button->generatePrintData($gridField); $printData = $button->generatePrintData($gridField);

View File

@ -25,6 +25,7 @@ use SilverStripe\Forms\Tests\GridField\GridFieldTest\Permissions;
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Player; use SilverStripe\Forms\Tests\GridField\GridFieldTest\Player;
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Team; use SilverStripe\Forms\Tests\GridField\GridFieldTest\Team;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\Security\Group;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
class GridFieldTest extends SapphireTest class GridFieldTest extends SapphireTest
@ -115,9 +116,9 @@ class GridFieldTest extends SapphireTest
public function testGridFieldModelClass() public function testGridFieldModelClass()
{ {
$obj = new GridField('testfield', 'testfield', Member::get()); $obj = new GridField('testfield', 'testfield', Member::get());
$this->assertEquals('SilverStripe\\Security\\Member', $obj->getModelClass(), 'Should return Member'); $this->assertEquals(Member::class, $obj->getModelClass(), 'Should return Member');
$obj->setModelClass('SilverStripe\\ORM\\DataModel'); $obj->setModelClass(Group::class);
$this->assertEquals('SilverStripe\\ORM\\DataModel', $obj->getModelClass(), 'Should return Member'); $this->assertEquals(Group::class, $obj->getModelClass(), 'Should return Group');
} }
/** /**

View File

@ -17,7 +17,7 @@ class GridField_URLHandlerTest extends FunctionalTest
public function testFormSubmission() public function testFormSubmission()
{ {
$result = $this->get("GridField_URLHandlerTest_Controller/Form/field/Grid/showform"); $this->get("GridField_URLHandlerTest_Controller/Form/field/Grid/showform");
$formResult = $this->submitForm('Form_Form', 'action_doAction', array('Test' => 'foo bar')); $formResult = $this->submitForm('Form_Form', 'action_doAction', array('Test' => 'foo bar'));
$this->assertEquals("Submitted foo bar to component", $formResult->getBody()); $this->assertEquals("Submitted foo bar to component", $formResult->getBody());
} }

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Forms\Tests\GridField\GridField_URLHandlerTest; namespace SilverStripe\Forms\Tests\GridField\GridField_URLHandlerTest;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\RequestHandler; use SilverStripe\Control\RequestHandler;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
@ -38,8 +39,9 @@ class TestComponent extends RequestHandler implements GridField_URLHandler
); );
} }
public function handleItem($gridField, $request) public function handleItem(GridField $gridField, HTTPRequest $request)
{ {
$this->setRequest($request);
$id = $request->param("ID"); $id = $request->param("ID");
return new TestComponent_ItemRequest( return new TestComponent_ItemRequest(
$gridField, $gridField,
@ -53,16 +55,21 @@ class TestComponent extends RequestHandler implements GridField_URLHandler
return $this->gridField->Link(); return $this->gridField->Link();
} }
public function showform($gridField, $request) public function showform(GridField $gridField, HTTPRequest $request)
{ {
$this->setRequest($request);
return "<head>" . SSViewer::get_base_tag("") . "</head>" . $this->Form($gridField, $request)->forTemplate(); return "<head>" . SSViewer::get_base_tag("") . "</head>" . $this->Form($gridField, $request)->forTemplate();
} }
/** /**
* @skipUpgrade * @skipUpgrade
* @param GridField $gridField
* @param HTTPRequest $request
* @return Form
*/ */
public function Form($gridField, $request) public function Form(GridField $gridField, HTTPRequest $request)
{ {
$this->setRequest($request);
$this->gridField = $gridField; $this->gridField = $gridField;
return new Form( return new Form(
$this, $this,
@ -81,8 +88,9 @@ class TestComponent extends RequestHandler implements GridField_URLHandler
return "Submitted " . $data['Test'] . " to component"; return "Submitted " . $data['Test'] . " to component";
} }
public function testpage($gridField, $request) public function testpage(GridField $gridField, HTTPRequest $request)
{ {
$this->setRequest($request);
return "Test page for component"; return "Test page for component";
} }
} }

View File

@ -15,6 +15,14 @@ use SilverStripe\ORM\ArrayList;
*/ */
class TestController extends Controller implements TestOnly class TestController extends Controller implements TestOnly
{ {
public function __construct()
{
parent::__construct();
if (Controller::has_curr()) {
$this->setRequest(Controller::curr()->getRequest());
}
}
public function Link($action = null) public function Link($action = null)
{ {
return Controller::join_links('GridField_URLHandlerTest_Controller', $action, '/'); return Controller::join_links('GridField_URLHandlerTest_Controller', $action, '/');

View File

@ -2,16 +2,17 @@
namespace SilverStripe\Forms\Tests\HTMLEditor; namespace SilverStripe\Forms\Tests\HTMLEditor;
use Exception;
use PHPUnit_Framework_MockObject_MockObject;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Core\Manifest\ModuleManifest; use SilverStripe\Core\Manifest\ModuleManifest;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
use SilverStripe\Forms\HTMLEditor\HTMLEditorField; use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
use SilverStripe\Forms\HTMLEditor\TinyMCEConfig; use SilverStripe\Forms\HTMLEditor\TinyMCEConfig;
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
use \Exception;
class HTMLEditorConfigTest extends SapphireTest class HTMLEditorConfigTest extends SapphireTest
{ {
@ -48,7 +49,7 @@ class HTMLEditorConfigTest extends SapphireTest
public function testEnablePluginsByArrayWithPaths() public function testEnablePluginsByArrayWithPaths()
{ {
Config::inst()->update(Director::class, 'alternate_base_url', 'http://mysite.com/subdir'); Config::modify()->set(Director::class, 'alternate_base_url', 'http://mysite.com/subdir');
$c = new TinyMCEConfig(); $c = new TinyMCEConfig();
$c->setTheme('modern'); $c->setTheme('modern');
$c->setOption('language', 'es'); $c->setOption('language', 'es');
@ -106,7 +107,7 @@ class HTMLEditorConfigTest extends SapphireTest
$this->markTestSkipped('No silverstripe/admin module loaded'); $this->markTestSkipped('No silverstripe/admin module loaded');
} }
TinyMCEConfig::config()->remove('base_dir'); TinyMCEConfig::config()->remove('base_dir');
Config::inst()->update(Director::class, 'alternate_base_url', 'http://mysite.com/subdir'); Config::modify()->set(Director::class, 'alternate_base_url', 'http://mysite.com/subdir');
$c = new TinyMCEConfig(); $c = new TinyMCEConfig();
$c->setTheme('modern'); $c->setTheme('modern');
$c->setOption('language', 'es'); $c->setOption('language', 'es');
@ -191,36 +192,30 @@ class HTMLEditorConfigTest extends SapphireTest
public function testExceptionThrownWhenTinyMCEPathCannotBeComputed() public function testExceptionThrownWhenTinyMCEPathCannotBeComputed()
{ {
TinyMCEConfig::config()->remove('base_dir'); TinyMCEConfig::config()->remove('base_dir');
ModuleLoader::inst()->pushManifest(new ModuleManifest( ModuleLoader::inst()->pushManifest(new ModuleManifest(__DIR__));
dirname(__FILE__),
false
));
$c = new TinyMCEConfig();
$this->setExpectedExceptionRegExp(
Exception::class,
'/module is not installed/'
);
$c->getScriptURL();
try {
$config = new TinyMCEConfig();
$this->expectException(Exception::class);
$this->expectExceptionMessageRegExp('/module is not installed/');
$config->getScriptURL();
} finally {
ModuleLoader::inst()->popManifest(); ModuleLoader::inst()->popManifest();
} }
}
public function testExceptionThrownWhenTinyMCEGZipPathDoesntExist() public function testExceptionThrownWhenTinyMCEGZipPathDoesntExist()
{ {
HTMLEditorField::config()->set('use_gzip', true); HTMLEditorField::config()->set('use_gzip', true);
/** @var TinyMCEConfig|PHPUnit_Framework_MockObject_MockObject $stub */
$stub = $this->getMockBuilder(TinyMCEConfig::class) $stub = $this->getMockBuilder(TinyMCEConfig::class)
->setMethods(['getTinyMCEPath']) ->setMethods(['getTinyMCEPath'])
->getMock(); ->getMock();
$stub->method('getTinyMCEPath') $stub->method('getTinyMCEPath')
->willReturn('fail'); ->willReturn('fail');
$this->setExpectedExceptionRegExp( $this->expectException(Exception::class);
Exception::class, $this->expectExceptionMessageRegExp('/does not exist/');
'/does not exist/'
);
$stub->getScriptURL(); $stub->getScriptURL();
} }
} }

View File

@ -4,6 +4,7 @@ namespace SilverStripe\Forms\Tests;
use SilverStripe\Assets\File; use SilverStripe\Assets\File;
use SilverStripe\Assets\Folder; use SilverStripe\Assets\Folder;
use SilverStripe\Control\Session;
use SilverStripe\Dev\CSSContentParser; use SilverStripe\Dev\CSSContentParser;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
@ -21,6 +22,7 @@ class TreeDropdownFieldTest extends SapphireTest
// case insensitive search against keyword 'sub' for folders // case insensitive search against keyword 'sub' for folders
$request = new HTTPRequest('GET', 'url', array('search'=>'sub')); $request = new HTTPRequest('GET', 'url', array('search'=>'sub'));
$request->setSession(new Session([]));
$response = $field->tree($request); $response = $field->tree($request);
$tree = $response->getBody(); $tree = $response->getBody();
@ -58,6 +60,7 @@ class TreeDropdownFieldTest extends SapphireTest
// case insensitive search against keyword 'sub' for files // case insensitive search against keyword 'sub' for files
$request = new HTTPRequest('GET', 'url', array('search'=>'sub')); $request = new HTTPRequest('GET', 'url', array('search'=>'sub'));
$request->setSession(new Session([]));
$response = $field->tree($request); $response = $field->tree($request);
$tree = $response->getBody(); $tree = $response->getBody();

View File

@ -30,7 +30,7 @@ class ValidatorTest extends SapphireTest
$fieldList->add(new TextField($name)); $fieldList->add(new TextField($name));
} }
return new Form(new Controller(), "testForm", $fieldList, new FieldList([/* no actions */])); return new Form(Controller::curr(), "testForm", $fieldList, new FieldList([/* no actions */]));
} }

View File

@ -3,6 +3,8 @@
namespace SilverStripe\ORM\Tests; namespace SilverStripe\ORM\Tests;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Kernel;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
@ -11,19 +13,20 @@ class DBTest extends SapphireTest
public function testValidAlternativeDatabaseName() public function testValidAlternativeDatabaseName()
{ {
/** @var Kernel $kernel */
$kernel = Injector::inst()->get(Kernel::class);
$prefix = getenv('SS_DATABASE_PREFIX') ?: 'ss_'; $prefix = getenv('SS_DATABASE_PREFIX') ?: 'ss_';
Director::set_environment_type('dev'); $kernel->setEnvironment(Kernel::DEV);
$this->assertTrue(DB::valid_alternative_database_name($prefix.'tmpdb1234567')); $this->assertTrue(DB::valid_alternative_database_name($prefix.'tmpdb1234567'));
$this->assertFalse(DB::valid_alternative_database_name($prefix.'tmpdb12345678')); $this->assertFalse(DB::valid_alternative_database_name($prefix.'tmpdb12345678'));
$this->assertFalse(DB::valid_alternative_database_name('tmpdb1234567')); $this->assertFalse(DB::valid_alternative_database_name('tmpdb1234567'));
$this->assertFalse(DB::valid_alternative_database_name('random')); $this->assertFalse(DB::valid_alternative_database_name('random'));
$this->assertFalse(DB::valid_alternative_database_name('')); $this->assertFalse(DB::valid_alternative_database_name(''));
Director::set_environment_type('live'); $kernel->setEnvironment(Kernel::LIVE);
$this->assertFalse(DB::valid_alternative_database_name($prefix.'tmpdb1234567')); $this->assertFalse(DB::valid_alternative_database_name($prefix.'tmpdb1234567'));
Director::set_environment_type('dev'); $kernel->setEnvironment(Kernel::DEV);
} }
} }

View File

@ -27,7 +27,7 @@ class DataListTest extends SapphireTest
// Borrow the model from DataObjectTest // Borrow the model from DataObjectTest
protected static $fixture_file = 'DataObjectTest.yml'; protected static $fixture_file = 'DataObjectTest.yml';
protected static function getExtraDataObjects() public static function getExtraDataObjects()
{ {
return array_merge( return array_merge(
DataObjectTest::$extra_data_objects, DataObjectTest::$extra_data_objects,

View File

@ -15,7 +15,7 @@ class DataObjectLazyLoadingTest extends SapphireTest
'DataObjectTest.yml', 'DataObjectTest.yml',
); );
protected static function getExtraDataObjects() public static function getExtraDataObjects()
{ {
return array_merge( return array_merge(
DataObjectTest::$extra_data_objects, DataObjectTest::$extra_data_objects,

View File

@ -57,7 +57,7 @@ class DataObjectTest extends SapphireTest
DataObjectTest\RelationChildSecond::class, DataObjectTest\RelationChildSecond::class,
); );
protected static function getExtraDataObjects() public static function getExtraDataObjects()
{ {
return array_merge( return array_merge(
DataObjectTest::$extra_data_objects, DataObjectTest::$extra_data_objects,

View File

@ -11,7 +11,7 @@ class HasManyListTest extends SapphireTest
// Borrow the model from DataObjectTest // Borrow the model from DataObjectTest
protected static $fixture_file = 'DataObjectTest.yml'; protected static $fixture_file = 'DataObjectTest.yml';
protected static function getExtraDataObjects() public static function getExtraDataObjects()
{ {
return array_merge( return array_merge(
DataObjectTest::$extra_data_objects, DataObjectTest::$extra_data_objects,

View File

@ -16,7 +16,7 @@ class HierarchyTest extends SapphireTest
HierarchyTest\HideTestSubObject::class, HierarchyTest\HideTestSubObject::class,
); );
protected static function getExtraDataObjects() public static function getExtraDataObjects()
{ {
// Prevent setup breaking if versioned module absent // Prevent setup breaking if versioned module absent
if (class_exists(Versioned::class)) { if (class_exists(Versioned::class)) {

View File

@ -20,7 +20,7 @@ class ManyManyListTest extends SapphireTest
ManyManyListTest\Product::class, ManyManyListTest\Product::class,
]; ];
protected static function getExtraDataObjects() public static function getExtraDataObjects()
{ {
return array_merge( return array_merge(
DataObjectTest::$extra_data_objects, DataObjectTest::$extra_data_objects,

View File

@ -14,7 +14,7 @@ class MapTest extends SapphireTest
// Borrow the model from DataObjectTest // Borrow the model from DataObjectTest
protected static $fixture_file = 'DataObjectTest.yml'; protected static $fixture_file = 'DataObjectTest.yml';
protected static function getExtraDataObjects() public static function getExtraDataObjects()
{ {
return array_merge( return array_merge(
DataObjectTest::$extra_data_objects, DataObjectTest::$extra_data_objects,

View File

@ -22,7 +22,7 @@ class MarkedSetTest extends SapphireTest
HierarchyTest\HideTestSubObject::class, HierarchyTest\HideTestSubObject::class,
); );
protected static function getExtraDataObjects() public static function getExtraDataObjects()
{ {
// Prevent setup breaking if versioned module absent // Prevent setup breaking if versioned module absent
if (class_exists(Versioned::class)) { if (class_exists(Versioned::class)) {

View File

@ -18,7 +18,7 @@ class PaginatedListTest extends SapphireTest
protected static $fixture_file = 'DataObjectTest.yml'; protected static $fixture_file = 'DataObjectTest.yml';
protected static function getExtraDataObjects() public static function getExtraDataObjects()
{ {
return array_merge( return array_merge(
DataObjectTest::$extra_data_objects, DataObjectTest::$extra_data_objects,

View File

@ -19,7 +19,7 @@ class PolymorphicHasManyListTest extends SapphireTest
// Borrow the model from DataObjectTest // Borrow the model from DataObjectTest
protected static $fixture_file = 'DataObjectTest.yml'; protected static $fixture_file = 'DataObjectTest.yml';
protected static function getExtraDataObjects() public static function getExtraDataObjects()
{ {
return array_merge( return array_merge(
DataObjectTest::$extra_data_objects, DataObjectTest::$extra_data_objects,

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Security\Tests; namespace SilverStripe\Security\Tests;
use SilverStripe\Security\BasicAuth;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Dev\FunctionalTest; use SilverStripe\Dev\FunctionalTest;
@ -29,24 +30,21 @@ class BasicAuthTest extends FunctionalTest
parent::setUp(); parent::setUp();
// Fixtures assume Email is the field used to identify the log in identity // Fixtures assume Email is the field used to identify the log in identity
Member::config()->unique_identifier_field = 'Email'; Member::config()->set('unique_identifier_field', 'Email');
Security::force_database_is_ready(true); // Prevents Member test subclasses breaking ready test Security::force_database_is_ready(true); // Prevents Member test subclasses breaking ready test
Member::config()->lock_out_after_incorrect_logins = 10; Member::config()->set('lock_out_after_incorrect_logins', 10);
// Temp disable is_cli() exemption for tests
BasicAuth::config()->set('ignore_cli', false);
} }
public function testBasicAuthEnabledWithoutLogin() public function testBasicAuthEnabledWithoutLogin()
{ {
$origUser = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null;
$origPw = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null;
unset($_SERVER['PHP_AUTH_USER']); unset($_SERVER['PHP_AUTH_USER']);
unset($_SERVER['PHP_AUTH_PW']); unset($_SERVER['PHP_AUTH_PW']);
$response = Director::test('BasicAuthTest_ControllerSecuredWithPermission', null, $_SESSION, null, null, $_SERVER); $response = Director::test('BasicAuthTest_ControllerSecuredWithPermission');
$this->assertEquals(401, $response->getStatusCode()); $this->assertEquals(401, $response->getStatusCode());
$_SERVER['PHP_AUTH_USER'] = $origUser;
$_SERVER['PHP_AUTH_PW'] = $origPw;
} }
public function testBasicAuthDoesntCallActionOrFurtherInitOnAuthFailure() public function testBasicAuthDoesntCallActionOrFurtherInitOnAuthFailure()

View File

@ -29,6 +29,7 @@ class ControllerSecuredWithPermission extends Controller implements TestOnly
public function index() public function index()
{ {
self::$index_called = true; self::$index_called = true;
return "index";
} }
public function Link($action = null) public function Link($action = null)

View File

@ -2,18 +2,18 @@
namespace SilverStripe\Security\Tests; namespace SilverStripe\Security\Tests;
use InvalidArgumentException;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Group; use SilverStripe\Security\Group;
use SilverStripe\Dev\FunctionalTest; use SilverStripe\Security\Member;
use SilverStripe\Control\Session;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\Security\Tests\GroupTest\TestMember; use SilverStripe\Security\Tests\GroupTest\TestMember;
class GroupTest extends FunctionalTest class GroupTest extends FunctionalTest
{ {
protected static $fixture_file = 'GroupTest.yml'; protected static $fixture_file = 'GroupTest.yml';
protected static $extra_dataobjects = [ protected static $extra_dataobjects = [
@ -44,13 +44,14 @@ class GroupTest extends FunctionalTest
*/ */
public function testMemberGroupRelationForm() public function testMemberGroupRelationForm()
{ {
Session::set('loggedInAs', $this->idFromFixture(TestMember::class, 'admin')); $this->logInAs($this->idFromFixture(TestMember::class, 'admin'));
$adminGroup = $this->objFromFixture(Group::class, 'admingroup'); $adminGroup = $this->objFromFixture(Group::class, 'admingroup');
$parentGroup = $this->objFromFixture(Group::class, 'parentgroup'); $parentGroup = $this->objFromFixture(Group::class, 'parentgroup');
// Test single group relation through checkboxsetfield // Test single group relation through checkboxsetfield
$form = new GroupTest\MemberForm(new Controller(), 'Form'); $form = new GroupTest\MemberForm(Controller::curr(), 'Form');
/** @var Member $member */
$member = $this->objFromFixture(TestMember::class, 'admin'); $member = $this->objFromFixture(TestMember::class, 'admin');
$form->loadDataFrom($member); $form->loadDataFrom($member);
$checkboxSetField = $form->Fields()->fieldByName('Groups'); $checkboxSetField = $form->Fields()->fieldByName('Groups');
@ -102,13 +103,17 @@ class GroupTest extends FunctionalTest
// Persists after writing to DB // Persists after writing to DB
$group->write(); $group->write();
/** @var Group $group */
$group = Group::get()->byID($group->ID); $group = Group::get()->byID($group->ID);
$this->assertEquals(array($member->ID), array_values($group->Members()->getIDList())); $this->assertEquals(array($member->ID), array_values($group->Members()->getIDList()));
} }
public function testCollateAncestorIDs() public function testCollateAncestorIDs()
{ {
/** @var Group $parentGroup */
$parentGroup = $this->objFromFixture(Group::class, 'parentgroup'); $parentGroup = $this->objFromFixture(Group::class, 'parentgroup');
/** @var Group $childGroup */
$childGroup = $this->objFromFixture(Group::class, 'childgroup'); $childGroup = $this->objFromFixture(Group::class, 'childgroup');
$orphanGroup = new Group(); $orphanGroup = new Group();
$orphanGroup->ParentID = 99999; $orphanGroup->ParentID = 99999;
@ -142,6 +147,7 @@ class GroupTest extends FunctionalTest
*/ */
public function testCollateFamilyIds() public function testCollateFamilyIds()
{ {
/** @var Group $group */
$group = $this->objFromFixture(Group::class, 'parentgroup'); $group = $this->objFromFixture(Group::class, 'parentgroup');
$groupIds = $this->allFixtureIDs(Group::class); $groupIds = $this->allFixtureIDs(Group::class);
$ids = array_intersect_key($groupIds, array_flip(['parentgroup', 'childgroup', 'grandchildgroup'])); $ids = array_intersect_key($groupIds, array_flip(['parentgroup', 'childgroup', 'grandchildgroup']));
@ -150,11 +156,11 @@ class GroupTest extends FunctionalTest
/** /**
* Test that an exception is thrown if collateFamilyIDs is called on an unsaved Group * Test that an exception is thrown if collateFamilyIDs is called on an unsaved Group
* @expectedException InvalidArgumentException
* @expectedExceptionMessage Cannot call collateFamilyIDs on unsaved Group.
*/ */
public function testCannotCollateUnsavedGroupFamilyIds() public function testCannotCollateUnsavedGroupFamilyIds()
{ {
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Cannot call collateFamilyIDs on unsaved Group.');
$group = new Group; $group = new Group;
$group->collateFamilyIDs(); $group->collateFamilyIDs();
} }
@ -164,8 +170,8 @@ class GroupTest extends FunctionalTest
*/ */
public function testGetAllChildren() public function testGetAllChildren()
{ {
/** @var Group $group */
$group = $this->objFromFixture(Group::class, 'parentgroup'); $group = $this->objFromFixture(Group::class, 'parentgroup');
$children = $group->getAllChildren(); $children = $group->getAllChildren();
$this->assertInstanceOf(ArrayList::class, $children); $this->assertInstanceOf(ArrayList::class, $children);
$this->assertSame(['childgroup', 'grandchildgroup'], $children->column('Code')); $this->assertSame(['childgroup', 'grandchildgroup'], $children->column('Code'));
@ -202,7 +208,9 @@ class GroupTest extends FunctionalTest
public function testValidatesPrivilegeLevelOfParent() public function testValidatesPrivilegeLevelOfParent()
{ {
/** @var Group $nonAdminGroup */
$nonAdminGroup = $this->objFromFixture(Group::class, 'childgroup'); $nonAdminGroup = $this->objFromFixture(Group::class, 'childgroup');
/** @var Group $adminGroup */
$adminGroup = $this->objFromFixture(Group::class, 'admingroup'); $adminGroup = $this->objFromFixture(Group::class, 'admingroup');
// Making admin group parent of a non-admin group, effectively expanding is privileges // Making admin group parent of a non-admin group, effectively expanding is privileges
@ -224,6 +232,7 @@ class GroupTest extends FunctionalTest
$nonAdminGroup->write(); $nonAdminGroup->write();
$this->logInWithPermission('ADMIN'); $this->logInWithPermission('ADMIN');
/** @var Group $inheritedAdminGroup */
$inheritedAdminGroup = $this->objFromFixture(Group::class, 'group1'); $inheritedAdminGroup = $this->objFromFixture(Group::class, 'group1');
$inheritedAdminGroup->ParentID = $adminGroup->ID; $inheritedAdminGroup->ParentID = $adminGroup->ID;
$inheritedAdminGroup->write(); // only works with ADMIN login $inheritedAdminGroup->write(); // only works with ADMIN login

View File

@ -2,22 +2,21 @@
namespace SilverStripe\Security\Tests; namespace SilverStripe\Security\Tests;
use SilverStripe\Control\Controller;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\DataModel; use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Authenticator; use SilverStripe\Security\Authenticator;
use SilverStripe\Security\DefaultAdminService;
use SilverStripe\Security\IdentityStore;
use SilverStripe\Security\Member;
use SilverStripe\Security\MemberAuthenticator\CMSMemberAuthenticator; use SilverStripe\Security\MemberAuthenticator\CMSMemberAuthenticator;
use SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm; use SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm;
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator; use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
use SilverStripe\Security\Security;
use SilverStripe\Security\Member;
use SilverStripe\Security\MemberAuthenticator\MemberLoginForm; use SilverStripe\Security\MemberAuthenticator\MemberLoginForm;
use SilverStripe\Security\IdentityStore; use SilverStripe\Security\Security;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Security\DefaultAdminService;
class MemberAuthenticatorTest extends SapphireTest class MemberAuthenticatorTest extends SapphireTest
{ {
@ -71,7 +70,7 @@ class MemberAuthenticatorTest extends SapphireTest
// Create basic login form // Create basic login form
$frontendResponse = $authenticator $frontendResponse = $authenticator
->getLoginHandler($controller->link()) ->getLoginHandler($controller->link())
->handleRequest(new HTTPRequest('get', '/'), DataModel::inst()); ->handleRequest(Controller::curr()->getRequest());
$this->assertTrue(is_array($frontendResponse)); $this->assertTrue(is_array($frontendResponse));
$this->assertTrue(isset($frontendResponse['Form'])); $this->assertTrue(isset($frontendResponse['Form']));
@ -116,10 +115,11 @@ class MemberAuthenticatorTest extends SapphireTest
// Test correct login // Test correct login
/** @var ValidationResult $message */ /** @var ValidationResult $message */
$result = $authenticator->authenticate( $result = $authenticator->authenticate(
array( [
'tempid' => $tempID, 'tempid' => $tempID,
'Password' => 'mypassword' 'Password' => 'mypassword'
), ],
Controller::curr()->getRequest(),
$message $message
); );
@ -129,10 +129,11 @@ class MemberAuthenticatorTest extends SapphireTest
// Test incorrect login // Test incorrect login
$result = $authenticator->authenticate( $result = $authenticator->authenticate(
array( [
'tempid' => $tempID, 'tempid' => $tempID,
'Password' => 'notmypassword' 'Password' => 'notmypassword'
), ],
Controller::curr()->getRequest(),
$message $message
); );
@ -154,10 +155,11 @@ class MemberAuthenticatorTest extends SapphireTest
// Test correct login // Test correct login
/** @var ValidationResult $message */ /** @var ValidationResult $message */
$result = $authenticator->authenticate( $result = $authenticator->authenticate(
array( [
'Email' => 'admin', 'Email' => 'admin',
'Password' => 'password' 'Password' => 'password'
), ],
Controller::curr()->getRequest(),
$message $message
); );
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
@ -166,10 +168,11 @@ class MemberAuthenticatorTest extends SapphireTest
// Test incorrect login // Test incorrect login
$result = $authenticator->authenticate( $result = $authenticator->authenticate(
array( [
'Email' => 'admin', 'Email' => 'admin',
'Password' => 'notmypassword' 'Password' => 'notmypassword'
), ],
Controller::curr()->getRequest(),
$message $message
); );
$messages = $message->getMessages(); $messages = $message->getMessages();
@ -193,7 +196,8 @@ class MemberAuthenticatorTest extends SapphireTest
[ [
'Email' => 'admin', 'Email' => 'admin',
'Password' => 'wrongpassword' 'Password' => 'wrongpassword'
] ],
Controller::curr()->getRequest()
); );
$defaultAdmin = DefaultAdminService::singleton()->findOrCreateDefaultAdmin(); $defaultAdmin = DefaultAdminService::singleton()->findOrCreateDefaultAdmin();

View File

@ -34,9 +34,9 @@ class MemberTest extends FunctionalTest
Member::class => '*', Member::class => '*',
]; ];
public function __construct() public static function setUpBeforeClass()
{ {
parent::__construct(); parent::setUpBeforeClass();
//Setting the locale has to happen in the constructor (using the setUp and tearDown methods doesn't work) //Setting the locale has to happen in the constructor (using the setUp and tearDown methods doesn't work)
//This is because the test relies on the yaml file being interpreted according to a particular date format //This is because the test relies on the yaml file being interpreted according to a particular date format

View File

@ -21,10 +21,10 @@ class SecurityDefaultAdminTest extends SapphireTest
// TODO Workaround to force database clearing with no fixture present, // TODO Workaround to force database clearing with no fixture present,
// and avoid sideeffects from other tests // and avoid sideeffects from other tests
if (!self::using_temp_db()) { if (!static::$tempDB->isUsed()) {
self::create_temp_db(); static::$tempDB->build();
} }
self::empty_temp_db(); static::$tempDB->clearAllData();
if (DefaultAdminService::hasDefaultAdmin()) { if (DefaultAdminService::hasDefaultAdmin()) {
$this->defaultUsername = DefaultAdminService::getDefaultAdminUsername(); $this->defaultUsername = DefaultAdminService::getDefaultAdminUsername();

View File

@ -2,11 +2,17 @@
namespace SilverStripe\Security\Tests; namespace SilverStripe\Security\Tests;
use SilverStripe\Dev\Debug; use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\i18n\i18n;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBClassName;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBClassName;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\LoginAttempt; use SilverStripe\Security\LoginAttempt;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
@ -46,11 +52,11 @@ class SecurityTest extends FunctionalTest
/** /**
* @skipUpgrade * @skipUpgrade
*/ */
Member::config()->unique_identifier_field = 'Email'; Member::config()->set('unique_identifier_field', 'Email');
parent::setUp(); parent::setUp();
Config::modify()->merge('SilverStripe\\Control\\Director', 'alternate_base_url', '/'); Director::config()->set('alternate_base_url', '/');
} }
public function testAccessingAuthenticatedPageRedirectsToLoginForm() public function testAccessingAuthenticatedPageRedirectsToLoginForm()
@ -74,21 +80,21 @@ class SecurityTest extends FunctionalTest
public function testPermissionFailureSetsCorrectFormMessages() public function testPermissionFailureSetsCorrectFormMessages()
{ {
Config::nest();
// Controller that doesn't attempt redirections // Controller that doesn't attempt redirections
$controller = new SecurityTest\NullController(); $controller = new SecurityTest\NullController();
$controller->setRequest(Controller::curr()->getRequest());
$controller->setResponse(new HTTPResponse()); $controller->setResponse(new HTTPResponse());
$session = Controller::curr()->getRequest()->getSession();
Security::permissionFailure($controller, array('default' => 'Oops, not allowed')); Security::permissionFailure($controller, array('default' => 'Oops, not allowed'));
$this->assertEquals('Oops, not allowed', Session::get('Security.Message.message')); $this->assertEquals('Oops, not allowed', $session->get('Security.Message.message'));
// Test that config values are used correctly // Test that config values are used correctly
Config::inst()->update(Security::class, 'default_message_set', 'stringvalue'); Config::modify()->set(Security::class, 'default_message_set', 'stringvalue');
Security::permissionFailure($controller); Security::permissionFailure($controller);
$this->assertEquals( $this->assertEquals(
'stringvalue', 'stringvalue',
Session::get('Security.Message.message'), $session->get('Security.Message.message'),
'Default permission failure message value was not present' 'Default permission failure message value was not present'
); );
@ -97,7 +103,7 @@ class SecurityTest extends FunctionalTest
Security::permissionFailure($controller); Security::permissionFailure($controller);
$this->assertEquals( $this->assertEquals(
'arrayvalue', 'arrayvalue',
Session::get('Security.Message.message'), $session->get('Security.Message.message'),
'Default permission failure message value was not present' 'Default permission failure message value was not present'
); );
@ -106,7 +112,7 @@ class SecurityTest extends FunctionalTest
// been fetched and output as part of it, so has been removed from the session // been fetched and output as part of it, so has been removed from the session
$this->logInWithPermission('EDITOR'); $this->logInWithPermission('EDITOR');
Config::inst()->update( Config::modify()->set(
Security::class, Security::class,
'default_message_set', 'default_message_set',
array('default' => 'default', 'alreadyLoggedIn' => 'You are already logged in!') array('default' => 'default', 'alreadyLoggedIn' => 'You are already logged in!')
@ -127,8 +133,6 @@ class SecurityTest extends FunctionalTest
$controller->getResponse()->getBody(), $controller->getResponse()->getBody(),
"Message set passed to Security::permissionFailure() didn't override Config values" "Message set passed to Security::permissionFailure() didn't override Config values"
); );
Config::unnest();
} }
/** /**
@ -200,7 +204,7 @@ class SecurityTest extends FunctionalTest
Security::setCurrentUser($member); Security::setCurrentUser($member);
/* View the Security/login page */ /* View the Security/login page */
$response = $this->get(Config::inst()->get(Security::class, 'login_url')); $this->get(Config::inst()->get(Security::class, 'login_url'));
$items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm input.action'); $items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm input.action');
@ -229,12 +233,12 @@ class SecurityTest extends FunctionalTest
public function testMemberIDInSessionDoesntExistInDatabaseHasToLogin() public function testMemberIDInSessionDoesntExistInDatabaseHasToLogin()
{ {
/* Log in with a Member ID that doesn't exist in the DB */ /* Log in with a Member ID that doesn't exist in the DB */
$this->session()->inst_set('loggedInAs', 500); $this->session()->set('loggedInAs', 500);
$this->autoFollowRedirection = true; $this->autoFollowRedirection = true;
/* Attempt to get into the admin section */ /* Attempt to get into the admin section */
$response = $this->get(Config::inst()->get(Security::class, 'login_url')); $this->get(Config::inst()->get(Security::class, 'login_url'));
$items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm input.text'); $items = $this->cssParser()->getBySelector('#MemberLoginForm_LoginForm input.text');
@ -244,14 +248,14 @@ class SecurityTest extends FunctionalTest
$this->autoFollowRedirection = false; $this->autoFollowRedirection = false;
/* Log the user out */ /* Log the user out */
$this->session()->inst_set('loggedInAs', null); $this->session()->set('loggedInAs', null);
} }
public function testLoginUsernamePersists() public function testLoginUsernamePersists()
{ {
// Test that username does not persist // Test that username does not persist
$this->session()->inst_set('SessionForms.MemberLoginForm.Email', 'myuser@silverstripe.com'); $this->session()->set('SessionForms.MemberLoginForm.Email', 'myuser@silverstripe.com');
Security::config()->remember_username = false; Security::config()->set('remember_username', false);
$this->get(Config::inst()->get(Security::class, 'login_url')); $this->get(Config::inst()->get(Security::class, 'login_url'));
$items = $this $items = $this
->cssParser() ->cssParser()
@ -264,8 +268,8 @@ class SecurityTest extends FunctionalTest
$this->assertEquals('off', (string)$form[0]->attributes()->autocomplete); $this->assertEquals('off', (string)$form[0]->attributes()->autocomplete);
// Test that username does persist when necessary // Test that username does persist when necessary
$this->session()->inst_set('SessionForms.MemberLoginForm.Email', 'myuser@silverstripe.com'); $this->session()->set('SessionForms.MemberLoginForm.Email', 'myuser@silverstripe.com');
Security::config()->remember_username = true; Security::config()->set('remember_username', true);
$this->get(Config::inst()->get(Security::class, 'login_url')); $this->get(Config::inst()->get(Security::class, 'login_url'));
$items = $this $items = $this
->cssParser() ->cssParser()
@ -343,7 +347,7 @@ class SecurityTest extends FunctionalTest
"Internal relative BackURLs work when passed through to login form" "Internal relative BackURLs work when passed through to login form"
); );
// Log the user out // Log the user out
$this->session()->inst_set('loggedInAs', null); $this->session()->set('loggedInAs', null);
// Test internal absolute redirect // Test internal absolute redirect
$response = $this->doTestLoginForm( $response = $this->doTestLoginForm(
@ -358,7 +362,7 @@ class SecurityTest extends FunctionalTest
"Internal absolute BackURLs work when passed through to login form" "Internal absolute BackURLs work when passed through to login form"
); );
// Log the user out // Log the user out
$this->session()->inst_set('loggedInAs', null); $this->session()->set('loggedInAs', null);
// Test external redirect // Test external redirect
$response = $this->doTestLoginForm('noexpiry@silverstripe.com', '1nitialPassword', 'http://myspoofedhost.com'); $response = $this->doTestLoginForm('noexpiry@silverstripe.com', '1nitialPassword', 'http://myspoofedhost.com');
@ -378,7 +382,7 @@ class SecurityTest extends FunctionalTest
); );
// Log the user out // Log the user out
$this->session()->inst_set('loggedInAs', null); $this->session()->set('loggedInAs', null);
} }
/** /**
@ -390,7 +394,7 @@ class SecurityTest extends FunctionalTest
$badResponse = $this->doTestLoginForm('testuser@example.com', 'badpassword'); $badResponse = $this->doTestLoginForm('testuser@example.com', 'badpassword');
$this->assertEquals(302, $badResponse->getStatusCode()); $this->assertEquals(302, $badResponse->getStatusCode());
$this->assertRegExp('/Security\/login/', $badResponse->getHeader('Location')); $this->assertRegExp('/Security\/login/', $badResponse->getHeader('Location'));
$this->assertNull($this->session()->inst_get('loggedInAs')); $this->assertNull($this->session()->get('loggedInAs'));
/* UNEXPIRED PASSWORD GO THROUGH WITHOUT A HITCH */ /* UNEXPIRED PASSWORD GO THROUGH WITHOUT A HITCH */
$goodResponse = $this->doTestLoginForm('testuser@example.com', '1nitialPassword'); $goodResponse = $this->doTestLoginForm('testuser@example.com', '1nitialPassword');
@ -399,7 +403,7 @@ class SecurityTest extends FunctionalTest
Controller::join_links(Director::absoluteBaseURL(), 'test/link'), Controller::join_links(Director::absoluteBaseURL(), 'test/link'),
$goodResponse->getHeader('Location') $goodResponse->getHeader('Location')
); );
$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs')); $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->get('loggedInAs'));
$this->logOut(); $this->logOut();
@ -412,7 +416,7 @@ class SecurityTest extends FunctionalTest
); );
$this->assertEquals( $this->assertEquals(
$this->idFromFixture(Member::class, 'expiredpassword'), $this->idFromFixture(Member::class, 'expiredpassword'),
$this->session()->inst_get('loggedInAs') $this->session()->get('loggedInAs')
); );
// Make sure it redirects correctly after the password has been changed // Make sure it redirects correctly after the password has been changed
@ -427,7 +431,7 @@ class SecurityTest extends FunctionalTest
public function testChangePasswordForLoggedInUsers() public function testChangePasswordForLoggedInUsers()
{ {
$goodResponse = $this->doTestLoginForm('testuser@example.com', '1nitialPassword'); $this->doTestLoginForm('testuser@example.com', '1nitialPassword');
// Change the password // Change the password
$this->get('Security/changepassword?BackURL=test/back'); $this->get('Security/changepassword?BackURL=test/back');
@ -437,7 +441,7 @@ class SecurityTest extends FunctionalTest
Controller::join_links(Director::absoluteBaseURL(), 'test/back'), Controller::join_links(Director::absoluteBaseURL(), 'test/back'),
$changedResponse->getHeader('Location') $changedResponse->getHeader('Location')
); );
$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs')); $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->get('loggedInAs'));
// Check if we can login with the new password // Check if we can login with the new password
$this->logOut(); $this->logOut();
@ -447,11 +451,12 @@ class SecurityTest extends FunctionalTest
Controller::join_links(Director::absoluteBaseURL(), 'test/link'), Controller::join_links(Director::absoluteBaseURL(), 'test/link'),
$goodResponse->getHeader('Location') $goodResponse->getHeader('Location')
); );
$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs')); $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->get('loggedInAs'));
} }
public function testChangePasswordFromLostPassword() public function testChangePasswordFromLostPassword()
{ {
/** @var Member $admin */
$admin = $this->objFromFixture(Member::class, 'test'); $admin = $this->objFromFixture(Member::class, 'test');
$admin->FailedLoginCount = 99; $admin->FailedLoginCount = 99;
$admin->LockedOutUntil = DBDatetime::now()->getValue(); $admin->LockedOutUntil = DBDatetime::now()->getValue();
@ -460,8 +465,8 @@ class SecurityTest extends FunctionalTest
$this->assertNull($admin->AutoLoginHash, 'Hash is empty before lost password'); $this->assertNull($admin->AutoLoginHash, 'Hash is empty before lost password');
// Request new password by email // Request new password by email
$response = $this->get('Security/lostpassword'); $this->get('Security/lostpassword');
$response = $this->post('Security/lostpassword/LostPasswordForm', array('Email' => 'testuser@example.com')); $this->post('Security/lostpassword/LostPasswordForm', array('Email' => 'testuser@example.com'));
$this->assertEmailSent('testuser@example.com'); $this->assertEmailSent('testuser@example.com');
@ -481,15 +486,15 @@ class SecurityTest extends FunctionalTest
); );
// Follow redirection to form without hash in GET parameter // Follow redirection to form without hash in GET parameter
$response = $this->get('Security/changepassword'); $this->get('Security/changepassword');
$changedResponse = $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword'); $this->doTestChangepasswordForm('1nitialPassword', 'changedPassword');
$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs')); $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->get('loggedInAs'));
// Check if we can login with the new password // Check if we can login with the new password
$this->logOut(); $this->logOut();
$goodResponse = $this->doTestLoginForm('testuser@example.com', 'changedPassword'); $goodResponse = $this->doTestLoginForm('testuser@example.com', 'changedPassword');
$this->assertEquals(302, $goodResponse->getStatusCode()); $this->assertEquals(302, $goodResponse->getStatusCode());
$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs')); $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->get('loggedInAs'));
$admin = DataObject::get_by_id(Member::class, $admin->ID, false); $admin = DataObject::get_by_id(Member::class, $admin->ID, false);
$this->assertNull($admin->LockedOutUntil); $this->assertNull($admin->LockedOutUntil);
@ -498,18 +503,17 @@ class SecurityTest extends FunctionalTest
public function testRepeatedLoginAttemptsLockingPeopleOut() public function testRepeatedLoginAttemptsLockingPeopleOut()
{ {
$local = i18n::get_locale();
i18n::set_locale('en_US'); i18n::set_locale('en_US');
Member::config()->set('lock_out_after_incorrect_logins', 5);
Member::config()->lock_out_after_incorrect_logins = 5; Member::config()->set('lock_out_delay_mins', 15);
Member::config()->lock_out_delay_mins = 15;
// Login with a wrong password for more than the defined threshold // Login with a wrong password for more than the defined threshold
for ($i = 1; $i <= (Member::config()->lock_out_after_incorrect_logins+1); $i++) { for ($i = 1; $i <= 6; $i++) {
$this->doTestLoginForm('testuser@example.com', 'incorrectpassword'); $this->doTestLoginForm('testuser@example.com', 'incorrectpassword');
/** @var Member $member */
$member = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'test')); $member = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'test'));
if ($i < Member::config()->get('lock_out_after_incorrect_logins')) { if ($i < 5) {
$this->assertNull( $this->assertNull(
$member->LockedOutUntil, $member->LockedOutUntil,
'User does not have a lockout time set if under threshold for failed attempts' 'User does not have a lockout time set if under threshold for failed attempts'
@ -534,14 +538,14 @@ class SecurityTest extends FunctionalTest
'Your account has been temporarily disabled because of too many failed attempts at ' . 'Your account has been temporarily disabled because of too many failed attempts at ' .
'logging in. Please try again in {count} minutes.', 'logging in. Please try again in {count} minutes.',
null, null,
array('count' => Member::config()->lock_out_delay_mins) array('count' => 15)
); );
$this->assertHasMessage($msg); $this->assertHasMessage($msg);
$this->doTestLoginForm('testuser@example.com', '1nitialPassword'); $this->doTestLoginForm('testuser@example.com', '1nitialPassword');
$this->assertNull( $this->assertNull(
$this->session()->inst_get('loggedInAs'), $this->session()->get('loggedInAs'),
'The user can\'t log in after being locked out, even with the right password' 'The user can\'t log in after being locked out, even with the right password'
); );
@ -551,7 +555,7 @@ class SecurityTest extends FunctionalTest
$member->write(); $member->write();
$this->doTestLoginForm('testuser@example.com', '1nitialPassword'); $this->doTestLoginForm('testuser@example.com', '1nitialPassword');
$this->assertEquals( $this->assertEquals(
$this->session()->inst_get('loggedInAs'), $this->session()->get('loggedInAs'),
$member->ID, $member->ID,
'After lockout expires, the user can login again' 'After lockout expires, the user can login again'
); );
@ -560,10 +564,10 @@ class SecurityTest extends FunctionalTest
$this->logOut(); $this->logOut();
// Login again with wrong password, but less attempts than threshold // Login again with wrong password, but less attempts than threshold
for ($i = 1; $i < Member::config()->lock_out_after_incorrect_logins; $i++) { for ($i = 1; $i < 5; $i++) {
$this->doTestLoginForm('testuser@example.com', 'incorrectpassword'); $this->doTestLoginForm('testuser@example.com', 'incorrectpassword');
} }
$this->assertNull($this->session()->inst_get('loggedInAs')); $this->assertNull($this->session()->get('loggedInAs'));
$this->assertHasMessage( $this->assertHasMessage(
_t('SilverStripe\\Security\\Member.ERRORWRONGCRED', 'The provided details don\'t seem to be correct. Please try again.'), _t('SilverStripe\\Security\\Member.ERRORWRONGCRED', 'The provided details don\'t seem to be correct. Please try again.'),
'The user can retry with a wrong password after the lockout expires' 'The user can retry with a wrong password after the lockout expires'
@ -571,17 +575,15 @@ class SecurityTest extends FunctionalTest
$this->doTestLoginForm('testuser@example.com', '1nitialPassword'); $this->doTestLoginForm('testuser@example.com', '1nitialPassword');
$this->assertEquals( $this->assertEquals(
$this->session()->inst_get('loggedInAs'), $this->session()->get('loggedInAs'),
$member->ID, $member->ID,
'The user can login successfully after lockout expires, if staying below the threshold' 'The user can login successfully after lockout expires, if staying below the threshold'
); );
i18n::set_locale($local);
} }
public function testAlternatingRepeatedLoginAttempts() public function testAlternatingRepeatedLoginAttempts()
{ {
Member::config()->lock_out_after_incorrect_logins = 3; Member::config()->set('lock_out_after_incorrect_logins', 3);
// ATTEMPTING LOG-IN TWICE WITH ONE ACCOUNT AND TWICE WITH ANOTHER SHOULDN'T LOCK ANYBODY OUT // ATTEMPTING LOG-IN TWICE WITH ONE ACCOUNT AND TWICE WITH ANOTHER SHOULDN'T LOCK ANYBODY OUT
@ -591,7 +593,9 @@ class SecurityTest extends FunctionalTest
$this->doTestLoginForm('noexpiry@silverstripe.com', 'incorrectpassword'); $this->doTestLoginForm('noexpiry@silverstripe.com', 'incorrectpassword');
$this->doTestLoginForm('noexpiry@silverstripe.com', 'incorrectpassword'); $this->doTestLoginForm('noexpiry@silverstripe.com', 'incorrectpassword');
/** @var Member $member1 */
$member1 = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'test')); $member1 = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'test'));
/** @var Member $member2 */
$member2 = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'noexpiry')); $member2 = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'noexpiry'));
$this->assertNull($member1->LockedOutUntil); $this->assertNull($member1->LockedOutUntil);
@ -611,17 +615,18 @@ class SecurityTest extends FunctionalTest
public function testUnsuccessfulLoginAttempts() public function testUnsuccessfulLoginAttempts()
{ {
Security::config()->login_recording = true; Security::config()->set('login_recording', true);
/* UNSUCCESSFUL ATTEMPTS WITH WRONG PASSWORD FOR EXISTING USER ARE LOGGED */ /* UNSUCCESSFUL ATTEMPTS WITH WRONG PASSWORD FOR EXISTING USER ARE LOGGED */
$this->doTestLoginForm('testuser@example.com', 'wrongpassword'); $this->doTestLoginForm('testuser@example.com', 'wrongpassword');
/** @var LoginAttempt $attempt */
$attempt = DataObject::get_one( $attempt = DataObject::get_one(
LoginAttempt::class, LoginAttempt::class,
array( array(
'"LoginAttempt"."Email"' => 'testuser@example.com' '"LoginAttempt"."Email"' => 'testuser@example.com'
) )
); );
$this->assertTrue(is_object($attempt)); $this->assertInstanceOf(LoginAttempt::class, $attempt);
$member = DataObject::get_one( $member = DataObject::get_one(
Member::class, Member::class,
array( array(
@ -648,16 +653,18 @@ class SecurityTest extends FunctionalTest
public function testSuccessfulLoginAttempts() public function testSuccessfulLoginAttempts()
{ {
Security::config()->login_recording = true; Security::config()->set('login_recording', true);
/* SUCCESSFUL ATTEMPTS ARE LOGGED */ /* SUCCESSFUL ATTEMPTS ARE LOGGED */
$this->doTestLoginForm('testuser@example.com', '1nitialPassword'); $this->doTestLoginForm('testuser@example.com', '1nitialPassword');
/** @var LoginAttempt $attempt */
$attempt = DataObject::get_one( $attempt = DataObject::get_one(
LoginAttempt::class, LoginAttempt::class,
array( array(
'"LoginAttempt"."Email"' => 'testuser@example.com' '"LoginAttempt"."Email"' => 'testuser@example.com'
) )
); );
/** @var Member $member */
$member = DataObject::get_one( $member = DataObject::get_one(
Member::class, Member::class,
array( array(
@ -700,7 +707,7 @@ class SecurityTest extends FunctionalTest
public function testDoNotSendEmptyRobotsHeaderIfNotDefined() public function testDoNotSendEmptyRobotsHeaderIfNotDefined()
{ {
Config::inst()->remove(Security::class, 'robots_tag'); Config::modify()->remove(Security::class, 'robots_tag');
$response = $this->get(Config::inst()->get(Security::class, 'login_url')); $response = $this->get(Config::inst()->get(Security::class, 'login_url'));
$robotsHeader = $response->getHeader('X-Robots-Tag'); $robotsHeader = $response->getHeader('X-Robots-Tag');
$this->assertNull($robotsHeader); $this->assertNull($robotsHeader);
@ -709,11 +716,16 @@ class SecurityTest extends FunctionalTest
/** /**
* Execute a log-in form using Director::test(). * Execute a log-in form using Director::test().
* Helper method for the tests above * Helper method for the tests above
*
* @param string $email
* @param string $password
* @param string $backURL
* @return HTTPResponse
*/ */
public function doTestLoginForm($email, $password, $backURL = 'test/link') public function doTestLoginForm($email, $password, $backURL = 'test/link')
{ {
$this->get(Config::inst()->get(Security::class, 'logout_url')); $this->get(Config::inst()->get(Security::class, 'logout_url'));
$this->session()->inst_set('BackURL', $backURL); $this->session()->set('BackURL', $backURL);
$this->get(Config::inst()->get(Security::class, 'login_url')); $this->get(Config::inst()->get(Security::class, 'login_url'));
return $this->submitForm( return $this->submitForm(
@ -730,6 +742,10 @@ class SecurityTest extends FunctionalTest
/** /**
* Helper method to execute a change password form * Helper method to execute a change password form
*
* @param string $oldPassword
* @param string $newPassword
* @return HTTPResponse
*/ */
public function doTestChangepasswordForm($oldPassword, $newPassword) public function doTestChangepasswordForm($oldPassword, $newPassword)
{ {
@ -771,7 +787,7 @@ class SecurityTest extends FunctionalTest
*/ */
protected function getValidationResult() protected function getValidationResult()
{ {
$result = $this->session()->inst_get('FormInfo.MemberLoginForm_LoginForm.result'); $result = $this->session()->get('FormInfo.MemberLoginForm_LoginForm.result');
if ($result) { if ($result) {
return unserialize($result); return unserialize($result);
} }

View File

@ -86,6 +86,7 @@ class RequirementsTest extends SapphireTest
$backend->clearCombinedFiles(); $backend->clearCombinedFiles();
$backend->setCombinedFilesFolder('_combinedfiles'); $backend->setCombinedFilesFolder('_combinedfiles');
$backend->setMinifyCombinedFiles(false); $backend->setMinifyCombinedFiles(false);
$backend->setCombinedFilesEnabled(true);
Requirements::flush(); Requirements::flush();
} }
@ -193,6 +194,7 @@ class RequirementsTest extends SapphireTest
{ {
/** @var Requirements_Backend $backend */ /** @var Requirements_Backend $backend */
$backend = Injector::inst()->create(Requirements_Backend::class); $backend = Injector::inst()->create(Requirements_Backend::class);
$backend->setCombinedFilesEnabled(true);
$this->setupCombinedRequirements($backend); $this->setupCombinedRequirements($backend);
$combinedFileName = '/_combinedfiles/RequirementsTest_bc-2a55d56.js'; $combinedFileName = '/_combinedfiles/RequirementsTest_bc-2a55d56.js';

View File

@ -3,6 +3,7 @@
namespace SilverStripe\View\Tests; namespace SilverStripe\View\Tests;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\TempFolder;
use SilverStripe\Versioned\Versioned; use SilverStripe\Versioned\Versioned;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
@ -19,7 +20,7 @@ class SSViewerCacheBlockTest extends SapphireTest
SSViewerCacheBlockTest\TestModel::class SSViewerCacheBlockTest\TestModel::class
); );
protected static function getExtraDataObjects() public static function getExtraDataObjects()
{ {
$classes = parent::getExtraDataObjects(); $classes = parent::getExtraDataObjects();
@ -41,7 +42,7 @@ class SSViewerCacheBlockTest extends SapphireTest
$cache = null; $cache = null;
if ($cacheOn) { if ($cacheOn) {
$cache = new FilesystemCache('cacheblock', 0, getTempFolder()); // cache indefinitely $cache = new FilesystemCache('cacheblock', 0, TempFolder::getTempFolder(BASE_PATH)); // cache indefinitely
} else { } else {
$cache = new NullCache(); $cache = new NullCache();
} }

View File

@ -2,35 +2,35 @@
namespace SilverStripe\View\Tests; namespace SilverStripe\View\Tests;
use Exception;
use InvalidArgumentException;
use PHPUnit_Framework_MockObject_MockObject; use PHPUnit_Framework_MockObject_MockObject;
use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
use SilverStripe\Control\ContentNegotiator;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\ContentNegotiator;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\PaginatedList; use SilverStripe\ORM\PaginatedList;
use SilverStripe\Security\Member; use SilverStripe\Security\Permission;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Security\SecurityToken; use SilverStripe\Security\SecurityToken;
use SilverStripe\Security\Permission;
use SilverStripe\View\ArrayData; use SilverStripe\View\ArrayData;
use SilverStripe\View\Requirements;
use SilverStripe\View\Requirements_Backend; use SilverStripe\View\Requirements_Backend;
use SilverStripe\View\Requirements_Minifier; use SilverStripe\View\Requirements_Minifier;
use SilverStripe\View\SSTemplateParser;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
use SilverStripe\View\Requirements; use SilverStripe\View\SSViewer_FromString;
use SilverStripe\View\Tests\SSViewerTest\SSViewerTestModel; use SilverStripe\View\Tests\SSViewerTest\SSViewerTestModel;
use SilverStripe\View\Tests\SSViewerTest\SSViewerTestModelController; use SilverStripe\View\Tests\SSViewerTest\SSViewerTestModelController;
use SilverStripe\View\ViewableData; use SilverStripe\View\ViewableData;
use SilverStripe\View\SSViewer_FromString;
use SilverStripe\View\SSTemplateParser;
use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
use Exception;
class SSViewerTest extends SapphireTest class SSViewerTest extends SapphireTest
{ {
@ -213,8 +213,11 @@ class SSViewerTest extends SapphireTest
public function testRequirementsCombine() public function testRequirementsCombine()
{ {
/** @var Requirements_Backend $testBackend */
$testBackend = Injector::inst()->create(Requirements_Backend::class); $testBackend = Injector::inst()->create(Requirements_Backend::class);
$testBackend->setSuffixRequirements(false); $testBackend->setSuffixRequirements(false);
$testBackend->setCombinedFilesEnabled(true);
//$combinedTestFilePath = BASE_PATH . '/' . $testBackend->getCombinedFilesFolder() . '/testRequirementsCombine.js'; //$combinedTestFilePath = BASE_PATH . '/' . $testBackend->getCombinedFilesFolder() . '/testRequirementsCombine.js';
$jsFile = $this->getCurrentRelativePath() . '/SSViewerTest/javascript/bad.js'; $jsFile = $this->getCurrentRelativePath() . '/SSViewerTest/javascript/bad.js';
@ -237,9 +240,12 @@ class SSViewerTest extends SapphireTest
public function testRequirementsMinification() public function testRequirementsMinification()
{ {
/** @var Requirements_Backend $testBackend */
$testBackend = Injector::inst()->create(Requirements_Backend::class); $testBackend = Injector::inst()->create(Requirements_Backend::class);
$testBackend->setSuffixRequirements(false); $testBackend->setSuffixRequirements(false);
$testBackend->setMinifyCombinedFiles(true); $testBackend->setMinifyCombinedFiles(true);
$testBackend->setCombinedFilesEnabled(true);
$testFile = $this->getCurrentRelativePath() . '/SSViewerTest/javascript/RequirementsTest_a.js'; $testFile = $this->getCurrentRelativePath() . '/SSViewerTest/javascript/RequirementsTest_a.js';
$testFileContent = file_get_contents($testFile); $testFileContent = file_get_contents($testFile);
@ -263,10 +269,8 @@ class SSViewerTest extends SapphireTest
->method('minify'); ->method('minify');
$testBackend->processCombinedFiles(); $testBackend->processCombinedFiles();
$this->setExpectedExceptionRegExp( $this->expectException(Exception::class);
Exception::class, $this->expectExceptionMessageRegExp('/^Cannot minify files without a minification service defined./');
'/minification service/'
);
$testBackend->setMinifyCombinedFiles(true); $testBackend->setMinifyCombinedFiles(true);
$testBackend->setMinifier(null); $testBackend->setMinifier(null);
@ -1614,8 +1618,8 @@ after'
); );
// Let's throw something random in there. // Let's throw something random in there.
$this->setExpectedException('InvalidArgumentException'); $this->expectException(InvalidArgumentException::class);
SSViewer::get_templates_by_class(array()); SSViewer::get_templates_by_class(null);
} }
); );
} }
@ -1732,7 +1736,6 @@ EOC;
public function testRenderWithSourceFileComments() public function testRenderWithSourceFileComments()
{ {
Director::set_environment_type('dev');
SSViewer::config()->update('source_file_comments', true); SSViewer::config()->update('source_file_comments', true);
$i = __DIR__ . '/SSViewerTest/templates/Includes'; $i = __DIR__ . '/SSViewerTest/templates/Includes';
$f = __DIR__ . '/SSViewerTest/templates/SSViewerTestComments'; $f = __DIR__ . '/SSViewerTest/templates/SSViewerTestComments';
@ -1853,13 +1856,13 @@ EOC;
Requirements::set_backend($backend); Requirements::set_backend($backend);
$this->assertEquals(1, substr_count($template->process(array()), "a.css")); $this->assertEquals(1, substr_count($template->process(new ViewableData()), "a.css"));
$this->assertEquals(1, substr_count($template->process(array()), "b.css")); $this->assertEquals(1, substr_count($template->process(new ViewableData()), "b.css"));
// if we disable the requirements then we should get nothing // if we disable the requirements then we should get nothing
$template->includeRequirements(false); $template->includeRequirements(false);
$this->assertEquals(0, substr_count($template->process(array()), "a.css")); $this->assertEquals(0, substr_count($template->process(new ViewableData()), "a.css"));
$this->assertEquals(0, substr_count($template->process(array()), "b.css")); $this->assertEquals(0, substr_count($template->process(new ViewableData()), "b.css"));
} }
public function testRequireCallInTemplateInclude() public function testRequireCallInTemplateInclude()
@ -1873,7 +1876,7 @@ EOC;
$this->assertEquals( $this->assertEquals(
1, 1,
substr_count( substr_count(
$template->process(array()), $template->process(new ViewableData()),
"tests/php/View/SSViewerTest/javascript/RequirementsTest_a.js" "tests/php/View/SSViewerTest/javascript/RequirementsTest_a.js"
) )
); );

View File

@ -5,6 +5,10 @@ namespace SilverStripe\View\Tests\SSViewerTest;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
use SilverStripe\View\ViewableData; use SilverStripe\View\ViewableData;
/**
* @property string $TextValue
* @property string $HTMLValue
*/
class TestViewableData extends ViewableData implements TestOnly class TestViewableData extends ViewableData implements TestOnly
{ {

View File

@ -50,7 +50,7 @@ trait i18nTestManifest
*/ */
protected $moduleManifests = 0; protected $moduleManifests = 0;
protected static function getExtraDataObjects() public static function getExtraDataObjects()
{ {
return [ return [
TestDataObject::class, TestDataObject::class,
@ -82,7 +82,8 @@ trait i18nTestManifest
Director::config()->update('alternate_base_folder', $this->alternateBasePath); Director::config()->update('alternate_base_folder', $this->alternateBasePath);
// New module manifest // New module manifest
$moduleManifest = new ModuleManifest($this->alternateBasePath, false); $moduleManifest = new ModuleManifest($this->alternateBasePath);
$moduleManifest->init(true);
$this->pushModuleManifest($moduleManifest); $this->pushModuleManifest($moduleManifest);
// Replace old template loader with new one with alternate base path // Replace old template loader with new one with alternate base path
@ -90,8 +91,9 @@ trait i18nTestManifest
ThemeResourceLoader::set_instance($loader = new ThemeResourceLoader($this->alternateBasePath)); ThemeResourceLoader::set_instance($loader = new ThemeResourceLoader($this->alternateBasePath));
$loader->addSet( $loader->addSet(
'$default', '$default',
new ThemeManifest($this->alternateBasePath, project(), false) $default = new ThemeManifest($this->alternateBasePath, project())
); );
$default->init(true);
SSViewer::set_themes([ SSViewer::set_themes([
'testtheme1', 'testtheme1',
@ -102,7 +104,8 @@ trait i18nTestManifest
i18n::set_locale('en_US'); i18n::set_locale('en_US');
// Set new manifest against the root // Set new manifest against the root
$classManifest = new ClassManifest($this->alternateBasePath, true); $classManifest = new ClassManifest($this->alternateBasePath);
$classManifest->init(true);
$this->pushManifest($classManifest); $this->pushManifest($classManifest);
// Setup uncached translator // Setup uncached translator