From 3873e4ba008cfc2af7e26ca86665affc289cd677 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Thu, 22 Jun 2017 22:50:45 +1200 Subject: [PATCH] API Refactor bootstrap, request handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://github.com/silverstripe/silverstripe-framework/pull/7037 and https://github.com/silverstripe/silverstripe-framework/issues/6681 Squashed commit of the following: commit 8f65e5653211240650eaa4fa65bb83b45aae6d58 Author: Ingo Schommer Date: Thu Jun 22 22:25:50 2017 +1200 Fixed upgrade guide spelling commit 76f95944fa89b0b540704b8d744329f690f9698c Author: Damian Mooyman Date: Thu Jun 22 16:38:34 2017 +1200 BUG Fix non-test class manifest including sapphiretest / functionaltest commit 9379834cb4b2e5177a2600049feec05bf111c16b Author: Damian Mooyman Date: Thu Jun 22 15:50:47 2017 +1200 BUG Fix nesting bug in Kernel commit 188ce35d82599360c40f0f2de29579c56fb90761 Author: Damian Mooyman Date: Thu Jun 22 15:14:51 2017 +1200 BUG fix db bootstrapping issues commit 7ed4660e7a63915e8e974deeaba9807bc4d38b0d Author: Damian Mooyman Date: Thu Jun 22 14:49:07 2017 +1200 BUG Fix issue in DetailedErrorFormatter commit 738f50c497166f81ccbe3f40fbcff895ce71f82f Author: Damian Mooyman Date: Thu Jun 22 11:49:19 2017 +1200 Upgrading notes on mysite/_config.php commit 6279d28e5e455916f902a2f963c014d8899f7fc7 Author: Damian Mooyman Date: Thu Jun 22 11:43:28 2017 +1200 Update developer documentation commit 5c90d53a84ef0139c729396949a7857fae60436f Author: Damian Mooyman Date: Thu Jun 22 10:48:44 2017 +1200 Update installer to not use global databaseConfig commit f9b2ba4755371f08bd95f6908ac612fcbb7ca205 Author: Damian Mooyman Date: Wed Jun 21 21:04:39 2017 +1200 Fix behat issues commit 5b59a912b60282b4dad4ef10ed3b97c5d0a761ac Author: Damian Mooyman Date: Wed Jun 21 17:07:11 2017 +1200 Move HTTPApplication to SilverStripe\Control namespace commit e2c4a18f637bdd3d276619554de60ee8b4d95ced Author: Damian Mooyman 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 5d235e64f341d6251bfe9f4833f15cc8593c5034 Author: Damian Mooyman 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 d88d4ed4e48291cb65407f222f190064b1f1deeb Author: Damian Mooyman Date: Tue Jun 20 16:39:43 2017 +1200 API Refactor AppKernel into CoreKernel commit f7946aec3391139ae1b4029c353c327a36552b36 Author: Damian Mooyman Date: Tue Jun 20 16:00:40 2017 +1200 Docs and minor cleanup commit 12bd31f9366327650b5c0c0f96cd0327d44faf0a Author: Damian Mooyman 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 bba979114624247cf463cf2a8c9e4be9a7c3a772 Author: Damian Mooyman Date: Mon Jun 19 18:07:53 2017 +1200 API Create HTTPMiddleware and standardise middleware for request handling commit 2a10c2397bdc53001013f607b5d38087ce6c0730 Author: Damian Mooyman Date: Mon Jun 19 17:42:42 2017 +1200 Fixed ORM tests commit d75a8d1d93398af4bd0432df9e4bc6295c15a3fe Author: Damian Mooyman Date: Mon Jun 19 17:15:07 2017 +1200 FIx i18n tests commit 06364af3c379c931889c4cc34dd920fee3db204a Author: Damian Mooyman Date: Mon Jun 19 16:59:34 2017 +1200 Fix controller namespace Move states to sub namespace commit 2a278e2953d2dbb19f78d91c919048e1fc935436 Author: Damian Mooyman Date: Mon Jun 19 12:49:45 2017 +1200 Fix forms namespace commit b65c21241bee019730027071d815dbf7571197a4 Author: Damian Mooyman Date: Thu Jun 15 18:56:48 2017 +1200 Update API usages commit d1d4375c95a264a6b63cbaefc2c1d12f808bfd82 Author: Damian Mooyman 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 b220534f06732db4fa940d8724c2a85c0ba2495a Author: Damian Mooyman Date: Tue Jun 13 22:05:57 2017 +1200 Move app nesting to a test state helper commit 603704165c08d0c1c81fd5e6bb9506326eeee17b Author: Damian Mooyman Date: Tue Jun 13 21:46:04 2017 +1200 Restore kernel stack to fix multi-level nesting commit 2f6336a15bf79dc8c2edd44cec1931da2dd51c28 Author: Damian Mooyman Date: Tue Jun 13 17:23:21 2017 +1200 API Implement kernel nesting commit fc7188da7d6ad6785354bab61f08700454c81d91 Author: Damian Mooyman Date: Tue Jun 13 15:43:13 2017 +1200 Fix core tests commit a0ae7235148fffd71f2f02d1fe7fe45bf3aa39eb Author: Damian Mooyman Date: Tue Jun 13 15:23:52 2017 +1200 Fix manifest tests commit ca033952513633e182040d3d13e1caa9000ca184 Author: Damian Mooyman Date: Tue Jun 13 15:00:00 2017 +1200 API Move extension management into test state commit c66d4339777663a8a04661fea32a0cf35b95d20f Author: Damian Mooyman Date: Tue Jun 13 14:10:59 2017 +1200 API Refactor SapphireTest state management into SapphireTestState API Remove Injector::unregisterAllObjects() API Remove FakeController commit f26ae75c6ecaafa0dec1093264e0187191e6764d Author: Damian Mooyman Date: Mon Jun 12 18:04:34 2017 +1200 Implement basic CLI application object commit 001d5596621404892de0a5413392379eff990641 Author: Damian Mooyman Date: Mon Jun 12 17:39:38 2017 +1200 Remove references to SapphireTest::is_running_test() Upgrade various code commit de079c041dacd96bc4f4b66421fa2b2cc4c320f8 Author: Damian Mooyman Date: Wed Jun 7 18:07:33 2017 +1200 API Implement APP object API Refactor of Session --- _config/tests.yml | 20 + cli-script.php | 159 +- composer.json | 4 +- .../00_Server_Requirements.md | 11 +- .../Mac_OSX_Homebrew.md | 4 +- .../04_Directory_Structure.md | 23 +- .../08_Performance/05_Resource_Usage.md | 15 +- .../03_App_Object_and_Kernel.md | 143 ++ .../16_Execution_Pipeline/index.md | 31 +- docs/en/04_Changelogs/4.0.0.md | 78 + main.php | 234 +-- main.php5 | 9 - src/Control/CLIRequestBuilder.php | 69 + src/Control/Controller.php | 68 +- src/Control/Director.php | 540 ++--- src/Control/Email/Email.php | 70 +- src/Control/Email/SwiftPlugin.php | 19 +- src/Control/FlushRequestFilter.php | 25 +- src/Control/HTTP.php | 11 +- src/Control/HTTPApplication.php | 127 ++ src/Control/HTTPMiddleware.php | 19 + src/Control/HTTPRequest.php | 43 +- src/Control/HTTPRequestBuilder.php | 148 ++ src/Control/RequestFilter.php | 14 +- src/Control/RequestHandler.php | 20 +- src/Control/RequestProcessor.php | 9 +- src/Control/Session.php | 303 ++- src/Core/Application.php | 29 + src/Core/Config/Config.php | 2 +- src/Core/Config/ConfigLoader.php | 38 +- src/Core/Config/CoreConfigFactory.php | 45 +- src/Core/Convert.php | 43 + src/Core/Core.php | 324 --- src/Core/CoreKernel.php | 580 ++++++ src/Core/Environment.php | 154 ++ src/Core/Extensible.php | 5 +- src/Core/Injector/Injector.php | 109 +- src/Core/Injector/InjectorLoader.php | 117 ++ ...ilverStripeServiceConfigurationLocator.php | 2 +- src/Core/Kernel.php | 135 ++ src/Core/Manifest/ClassLoader.php | 23 +- src/Core/Manifest/ClassManifest.php | 95 +- src/Core/Manifest/ClassManifestVisitor.php | 2 +- src/Core/Manifest/ModuleLoader.php | 15 +- src/Core/Manifest/ModuleManifest.php | 25 +- .../Startup/ErrorControlChainMiddleware.php | 119 ++ .../Startup/ParameterConfirmationToken.php | 121 +- src/Core/TempFolder.php | 117 ++ src/Core/TempPath.php | 111 -- src/Dev/BulkLoader.php | 5 +- src/Dev/CSSContentParser.php | 4 +- src/Dev/DevBuildController.php | 4 +- src/Dev/DevelopmentAdmin.php | 13 +- src/Dev/FixtureBlueprint.php | 14 +- src/Dev/FunctionalTest.php | 17 +- src/Dev/Install/InstallRequirements.php | 1131 +++++++++++ src/Dev/Install/Installer.php | 491 +++++ src/Dev/Install/client/images/logo.gif | Bin 0 -> 2097 bytes src/Dev/Install/config-form.html | 10 +- src/Dev/Install/install.php | 2 +- src/Dev/Install/install.php5 | 1762 ----------------- src/Dev/Install/install5.php | 281 +++ src/Dev/Install/php5-required.html | 8 +- src/Dev/PhpUnitShim.php | 14 - src/Dev/SapphireTest.php | 669 ++----- src/Dev/State/ExtensionTestState.php | 124 ++ src/Dev/State/FlushableTestState.php | 50 + src/Dev/State/GlobalsTestState.php | 45 + src/Dev/State/KernelTestState.php | 94 + src/Dev/State/SapphireTestState.php | 72 + src/Dev/State/TestState.php | 42 + src/Dev/TaskRunner.php | 4 +- src/Dev/Tasks/CleanupTestDatabasesTask.php | 5 +- src/Dev/Tasks/i18nTextCollectorTask.php | 3 +- src/Dev/TestKernel.php | 49 + src/Dev/TestSession.php | 10 +- src/Forms/DefaultFormFactory.php | 6 + src/Forms/Form.php | 51 +- src/Forms/GridField/GridField.php | 32 +- src/Forms/GridField/GridFieldDetailForm.php | 11 +- .../GridFieldDetailForm_ItemRequest.php | 2 +- src/Forms/GridField/GridFieldExportButton.php | 4 +- src/Forms/GridField/GridField_FormAction.php | 6 +- src/Forms/HTMLEditor/TinyMCEConfig.php | 2 +- src/Logging/DetailedErrorFormatter.php | 2 +- src/ORM/Connect/TempDatabase.php | 220 ++ src/ORM/DB.php | 58 +- src/ORM/DataList.php | 21 +- src/ORM/DataModel.php | 80 - src/ORM/DataObject.php | 54 +- src/ORM/DatabaseAdmin.php | 36 +- src/ORM/ManyManyThroughList.php | 2 +- src/Security/AuthenticationRequestFilter.php | 13 +- src/Security/Authenticator.php | 4 +- src/Security/BasicAuth.php | 24 +- src/Security/CMSSecurity.php | 12 +- src/Security/DefaultAdminService.php | 18 +- src/Security/Member.php | 50 +- .../ChangePasswordForm.php | 3 +- .../ChangePasswordHandler.php | 15 +- .../MemberAuthenticator/LoginHandler.php | 33 +- .../MemberAuthenticator.php | 43 +- .../MemberAuthenticator/MemberLoginForm.php | 14 +- .../SessionAuthenticationHandler.php | 10 +- src/Security/Security.php | 79 +- src/Security/SecurityToken.php | 6 +- src/View/Dev/RequirementsTestState.php | 39 + src/View/GenericTemplateGlobalProvider.php | 7 +- src/View/Requirements.php | 7 +- src/View/Requirements_Backend.php | 30 +- src/View/ThemeManifest.php | 75 +- src/View/ThemeResourceLoader.php | 1 - src/View/ViewableData.php | 2 +- src/conf/ConfigureFromEnv.php | 172 +- src/{conf => includes}/_manifest_exclude | 0 src/includes/autoload.php | 17 + .../Constants.php => includes/constants.php} | 29 +- src/includes/functions.php | 68 + tests/behat/serve-bootstrap.php | 1 - tests/bootstrap.php | 1 - tests/bootstrap/mysite.php | 10 - tests/bootstrap/phpunit.php | 24 - tests/php/Control/ControllerTest.php | 27 +- .../Control/ControllerTest/SubController.php | 6 +- .../Control/ControllerTest/TestController.php | 8 + tests/php/Control/DirectorTest.php | 278 +-- .../Control/DirectorTest/TestController.php | 28 +- .../DirectorTest/TestRequestFilter.php | 8 +- tests/php/Control/FakeController.php | 31 - tests/php/Control/FlushRequestFilterTest.php | 2 - .../FlushRequestFilterTest/TestFlushable.php | 1 - tests/php/Control/HTTPTest.php | 72 +- .../Control/PjaxResponseNegotiatorTest.php | 9 +- .../RequestHandlingTest/TestController.php | 12 +- tests/php/Control/SessionTest.php | 86 +- tests/php/Core/CoreTest.php | 15 +- tests/php/Core/Injector/InjectorTest.php | 10 +- .../InjectorTest/TestStaticInjections.php | 6 +- tests/php/Core/KernelTest.php | 81 + tests/php/Core/Manifest/ClassLoaderTest.php | 10 +- tests/php/Core/Manifest/ClassManifestTest.php | 9 +- .../php/Core/Manifest/ConfigManifestTest.php | 11 +- .../php/Core/Manifest/ModuleManifestTest.php | 4 +- .../Manifest/NamespacedClassManifestTest.php | 3 +- .../Core/Manifest/ThemeResourceLoaderTest.php | 3 +- .../mysite/_config.php | 5 - tests/php/Core/MemoryLimitTest.php | 169 +- tests/php/Core/ObjectTest.php | 11 +- .../Core/Startup/ErrorControlChainTest.php | 4 +- .../ParameterConfirmationTokenTest.php | 112 +- .../ParameterConfirmationTokenTest_Token.php | 4 +- ...ameterConfirmationTokenTest_ValidToken.php | 2 - tests/php/Forms/CheckboxSetFieldTest.php | 7 +- .../php/Forms/ConfirmedPasswordFieldTest.php | 23 +- tests/php/Forms/DatetimeFieldTest.php | 12 +- tests/php/Forms/DropdownFieldTest.php | 2 +- .../Forms/EmailFieldTest/TestController.php | 23 +- tests/php/Forms/FileFieldTest.php | 4 +- tests/php/Forms/FormFactoryTest.php | 3 +- .../Forms/FormFactoryTest/TestController.php | 11 + tests/php/Forms/FormFieldTest.php | 34 +- tests/php/Forms/FormRequestHandlerTest.php | 7 +- tests/php/Forms/FormSchemaTest.php | 12 + tests/php/Forms/FormTest.php | 17 +- tests/php/Forms/FormTest/TestController.php | 7 + .../GridFieldAddExistingAutocompleterTest.php | 9 +- .../TestController.php | 11 +- .../GridField/GridFieldDeleteActionTest.php | 55 +- .../GridField/GridFieldDetailFormTest.php | 15 +- .../CategoryController.php | 8 +- .../TestController.php | 7 + .../GridField/GridFieldPrintButtonTest.php | 7 +- tests/php/Forms/GridField/GridFieldTest.php | 7 +- .../GridField/GridField_URLHandlerTest.php | 2 +- .../TestComponent.php | 16 +- .../TestController.php | 8 + .../Forms/HTMLEditor/HTMLEditorConfigTest.php | 39 +- tests/php/Forms/TreeDropdownFieldTest.php | 3 + tests/php/Forms/ValidatorTest.php | 2 +- tests/php/ORM/DBTest.php | 11 +- tests/php/ORM/DataListTest.php | 2 +- tests/php/ORM/DataObjectLazyLoadingTest.php | 2 +- tests/php/ORM/DataObjectTest.php | 2 +- tests/php/ORM/HasManyListTest.php | 2 +- tests/php/ORM/HierarchyTest.php | 2 +- tests/php/ORM/ManyManyListTest.php | 2 +- tests/php/ORM/MapTest.php | 2 +- tests/php/ORM/MarkedSetTest.php | 2 +- tests/php/ORM/PaginatedListTest.php | 2 +- tests/php/ORM/PolymorphicHasManyListTest.php | 2 +- tests/php/Security/BasicAuthTest.php | 16 +- .../ControllerSecuredWithPermission.php | 1 + tests/php/Security/GroupTest.php | 25 +- .../php/Security/MemberAuthenticatorTest.php | 40 +- tests/php/Security/MemberTest.php | 4 +- .../php/Security/SecurityDefaultAdminTest.php | 6 +- tests/php/Security/SecurityTest.php | 150 +- tests/php/View/RequirementsTest.php | 2 + tests/php/View/SSViewerCacheBlockTest.php | 5 +- tests/php/View/SSViewerTest.php | 45 +- .../View/SSViewerTest/TestViewableData.php | 4 + tests/php/i18n/i18nTestManifest.php | 11 +- 202 files changed, 7048 insertions(+), 5414 deletions(-) create mode 100644 _config/tests.yml create mode 100644 docs/en/02_Developer_Guides/16_Execution_Pipeline/03_App_Object_and_Kernel.md delete mode 100644 main.php5 create mode 100644 src/Control/CLIRequestBuilder.php create mode 100644 src/Control/HTTPApplication.php create mode 100644 src/Control/HTTPMiddleware.php create mode 100644 src/Control/HTTPRequestBuilder.php create mode 100644 src/Core/Application.php delete mode 100644 src/Core/Core.php create mode 100644 src/Core/CoreKernel.php create mode 100644 src/Core/Environment.php create mode 100644 src/Core/Injector/InjectorLoader.php create mode 100644 src/Core/Kernel.php create mode 100644 src/Core/Startup/ErrorControlChainMiddleware.php create mode 100644 src/Core/TempFolder.php delete mode 100644 src/Core/TempPath.php create mode 100644 src/Dev/Install/InstallRequirements.php create mode 100644 src/Dev/Install/Installer.php create mode 100644 src/Dev/Install/client/images/logo.gif delete mode 100755 src/Dev/Install/install.php5 create mode 100755 src/Dev/Install/install5.php delete mode 100644 src/Dev/PhpUnitShim.php create mode 100644 src/Dev/State/ExtensionTestState.php create mode 100644 src/Dev/State/FlushableTestState.php create mode 100644 src/Dev/State/GlobalsTestState.php create mode 100644 src/Dev/State/KernelTestState.php create mode 100644 src/Dev/State/SapphireTestState.php create mode 100644 src/Dev/State/TestState.php create mode 100644 src/Dev/TestKernel.php create mode 100644 src/ORM/Connect/TempDatabase.php delete mode 100644 src/ORM/DataModel.php create mode 100644 src/View/Dev/RequirementsTestState.php rename src/{conf => includes}/_manifest_exclude (100%) create mode 100644 src/includes/autoload.php rename src/{Core/Constants.php => includes/constants.php} (91%) create mode 100644 src/includes/functions.php delete mode 100644 tests/bootstrap/mysite.php delete mode 100644 tests/bootstrap/phpunit.php delete mode 100644 tests/php/Control/FakeController.php create mode 100644 tests/php/Core/KernelTest.php diff --git a/_config/tests.yml b/_config/tests.yml new file mode 100644 index 000000000..439f76a2e --- /dev/null +++ b/_config/tests.yml @@ -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 diff --git a/cli-script.php b/cli-script.php index f6515247e..cb88be8e5 100755 --- a/cli-script.php +++ b/cli-script.php @@ -1,152 +1,23 @@ '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 <<handle($request); +$response->output(); diff --git a/composer.json b/composer.json index ad453b478..598c6f4ff 100644 --- a/composer.json +++ b/composer.json @@ -82,12 +82,12 @@ "SilverStripe\\Framework\\Tests\\Behaviour\\": "tests/behat/src/" }, "files": [ - "src/Core/Constants.php", - "src/Dev/PhpUnitShim.php" + "src/includes/constants.php" ] }, "include-path": [ "src/", + "src/includes/", "thirdparty/" ], "scripts": { diff --git a/docs/en/00_Getting_Started/00_Server_Requirements.md b/docs/en/00_Getting_Started/00_Server_Requirements.md index c11d302b6..85b72b64f 100644 --- a/docs/en/00_Getting_Started/00_Server_Requirements.md +++ b/docs/en/00_Getting_Started/00_Server_Requirements.md @@ -24,12 +24,13 @@ Our web-based [PHP installer](installation/) can check if you meet the requireme * `/dev/urandom` * [`mcrypt_create_iv()`](http://php.net/manual/en/function.mcrypt-create-iv.php) * 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 - - safe_mode = Off - magic_quotes_gpc = Off - memory_limit = 48M + * Dev (local development for running test framework): memory_limit 512MB + * Production: memory_limit = 64M * See [phpinfo()](http://php.net/manual/en/function.phpinfo.php) for more information about your environment * One of the following databases: diff --git a/docs/en/00_Getting_Started/01_Installation/04_Other_installation_Options/Mac_OSX_Homebrew.md b/docs/en/00_Getting_Started/01_Installation/04_Other_installation_Options/Mac_OSX_Homebrew.md index 0d6542708..c25f60cd3 100644 --- a/docs/en/00_Getting_Started/01_Installation/04_Other_installation_Options/Mac_OSX_Homebrew.md +++ b/docs/en/00_Getting_Started/01_Installation/04_Other_installation_Options/Mac_OSX_Homebrew.md @@ -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/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`). diff --git a/docs/en/00_Getting_Started/04_Directory_Structure.md b/docs/en/00_Getting_Started/04_Directory_Structure.md index 8375cd4c1..f53f85fa8 100644 --- a/docs/en/00_Getting_Started/04_Directory_Structure.md +++ b/docs/en/00_Getting_Started/04_Directory_Structure.md @@ -18,25 +18,30 @@ Directory | Description We're using `` as an example - arbitrary directory-names are allowed, as long as they don't collide with existing modules or the directories lists in "Core Structure". - | Directory | Description | - | --------- | ----------- | + | Directory | Description | + | --------- | ----------- | | `/` | This directory contains all of your code that defines your website. | | `/_config` | YAML configuration specific to your application | - | `/code` | PHP code for model and controller (subdirectories are optional) | - | `/templates` | HTML [templates](/developer_guides/templates) with *.ss-extension | + | `/src` | PHP code for model and controller (subdirectories are optional) | + | `/tests` | PHP Unit tests | + | `/templates` | HTML [templates](/developer_guides/templates) with *.ss-extension for the `$default` theme | | `/css ` | CSS files | | `/images ` | Images used in the HTML templates | - | `/javascript` | Javascript and other script files | + | `/javascript` | Javascript and other script files | | `/client` | More complex projects can alternatively contain frontend assets in a common `client` folder | + | `/themes/` | Custom nested themes (note: theme structure is described below) | Check our [JavaScript Coding Conventions](javascript_coding_conventions) for more details on folder and file naming in SilverStripe core modules. ## Themes Structure - | `themes/simple/` | Standard "simple" theme | - | ------------------ | --------------------------- | - | `themes/yourtheme/` | The themes folder can contain more than one theme - here's your own | + | Directory | Description | + | ------------------ | --------------------------- | + | `themes/simple/` | Standard "simple" theme | + | `themes//` | Custom theme base directory | + | `themes//templates` | Theme templates | + | `themes//css` | Theme CSS files | See [themes](/developer_guides/templates/themes) @@ -77,7 +82,7 @@ Example Forum 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/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/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.| diff --git a/docs/en/02_Developer_Guides/08_Performance/05_Resource_Usage.md b/docs/en/02_Developer_Guides/08_Performance/05_Resource_Usage.md index 670f9a805..8d9a3ef8c 100644 --- a/docs/en/02_Developer_Guides/08_Performance/05_Resource_Usage.md +++ b/docs/en/02_Developer_Guides/08_Performance/05_Resource_Usage.md @@ -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.
-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.
:::php - function myBigFunction() { - increase_time_limit_to(400); - - // or.. - - set_increase_time_limit_max(); - - // .. - } \ No newline at end of file + public function myBigFunction() { + Environment::increaseTimeLimitTo(400); + } diff --git a/docs/en/02_Developer_Guides/16_Execution_Pipeline/03_App_Object_and_Kernel.md b/docs/en/02_Developer_Guides/16_Execution_Pipeline/03_App_Object_and_Kernel.md new file mode 100644 index 000000000..8db4ba7b7 --- /dev/null +++ b/docs/en/02_Developer_Guides/16_Execution_Pipeline/03_App_Object_and_Kernel.md @@ -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 + 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. + + ::: + + # ... + RewriteCond %{REQUEST_URI} ^(.*)$ + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule .* mysite/main.php?url=%1 [QSA] + # ... + + + +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 + + # ... + # Non existant files passed to requesthandler + RewriteCond %{REQUEST_URI} ^(.*)$ + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule .* ../mysite/main.php?url=%1 [QSA] + + +## 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); + + + diff --git a/docs/en/02_Developer_Guides/16_Execution_Pipeline/index.md b/docs/en/02_Developer_Guides/16_Execution_Pipeline/index.md index 7e91aa99b..32f9937e7 100644 --- a/docs/en/02_Developer_Guides/16_Execution_Pipeline/index.md +++ b/docs/en/02_Developer_Guides/16_Execution_Pipeline/index.md @@ -78,21 +78,26 @@ can leave sensitive files exposed to public access (the `RewriteRule` conditions ## 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. - * 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. - The configuration is either defined in your `_config.php`, or through `.env` - * Sets up [error handlers](../debugging/error_handling) - * 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) - * Executes all procedural configuration defined through `_config.php` in all discovered modules - * Loads the Composer PHP class autoloader - * Hands control over to [api:Director] + * Sets constants based on the filesystem structure (e.g. `BASE_URL`, `BASE_PATH` and `TEMP_FOLDER`) + +All requests go through `framework/main.php`, which sets up the core [api:Kernel] and [api:HTTPApplication] +objects. See [/developer_guides/execution_pipeline/app_object_and_kernel] for details on this. +The main process follows: + + + * Include `autoload.php` + * Construct [api:HTTPRequest] object from environment. + * Construct a `Kernel` instance + * 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 adding your own manifests or a performance-optimized routing might require it. diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md index 92ce5e0f4..6fa1d7632 100644 --- a/docs/en/04_Changelogs/4.0.0.md +++ b/docs/en/04_Changelogs/4.0.0.md @@ -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 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 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. +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. #### Replace usages of Object class @@ -245,6 +258,30 @@ Extensions $has = DataObject::has_extension(File::class, Versioned::class); // alternate $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 @@ -1248,6 +1285,7 @@ After (`mysite/_config/config.yml`): #### General and Core API Additions / Changes * 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)), 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. @@ -1270,6 +1308,9 @@ After (`mysite/_config/config.yml`): * `Configurable` Provides Config API helper methods * `Injectable` Provides Injector API helper methods * `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. * `Injector` dependencies no longer automatically inherit from parent classes. * `$action` parameter to `Controller::Link()` method is standardised. @@ -1335,10 +1376,34 @@ After (`mysite/_config/config.yml`): * `findAnAdministrator` use `DefaultAdminService::findOrCreateDefaultAdmin()` instead * `Member` methods deprecated: * `checkPassword`. Use Authenticator::checkPassword() instead +* `RequestFilter` changed. $session and $dataModel variables removed from preRequest / postRequest #### 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:` 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. +* `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. * Removed `CMSBatchAction_Delete` * 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 is provided without a `Currency` specified, it will be formatted as per the current locale. * 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` * `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` #### 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::db` removed and replaced with `DataObjectSchema::fieldSpec` and `DataObjectSchema::fieldSpecs` * `DataObject::manyManyComponent` moved to `DataObjectSchema` diff --git a/main.php b/main.php index 8bcd53be6..ae38ff532 100644 --- a/main.php +++ b/main.php @@ -1,226 +1,18 @@ url=[^&?]*)(?.*[&?]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.

' - . 'See the ' - . 'Environment Management 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()); +// Default application +$kernel = new CoreKernel(BASE_PATH); +$app = new HTTPApplication($kernel); +$app->addMiddleware(new ErrorControlChainMiddleware($app)); +$response = $app->handle($request); +$response->output(); diff --git a/main.php5 b/main.php5 deleted file mode 100644 index f89e90eee..000000000 --- a/main.php5 +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/src/Control/CLIRequestBuilder.php b/src/Control/CLIRequestBuilder.php new file mode 100644 index 000000000..660ffb7e0 --- /dev/null +++ b/src/Control/CLIRequestBuilder.php @@ -0,0 +1,69 @@ + '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); + } +} diff --git a/src/Control/Controller.php b/src/Control/Controller.php index 9f2e6f9b3..0a7233703 100644 --- a/src/Control/Controller.php +++ b/src/Control/Controller.php @@ -3,9 +3,7 @@ namespace SilverStripe\Control; use SilverStripe\Core\ClassInfo; -use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\Debug; -use SilverStripe\ORM\DataModel; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\Security\BasicAuth; use SilverStripe\Security\Member; @@ -47,13 +45,6 @@ class Controller extends RequestHandler implements TemplateGlobalProvider */ 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. * @@ -61,6 +52,14 @@ class Controller extends RequestHandler implements TemplateGlobalProvider */ protected static $controller_stack = array(); + /** + * Assign templates for this controller. + * Map of action => template name + * + * @var array + */ + protected $templates = []; + /** * @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 * * @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 $this->pushCurrent(); - //Set up the internal dependencies (request, response, datamodel) - $this->setRequest($request); $this->setResponse(new HTTPResponse()); - $this->setDataModel($model); //kick off the init functionality $this->doInit(); } @@ -192,24 +189,22 @@ class Controller extends RequestHandler implements TemplateGlobalProvider * and end the method with $this->afterHandleRequest() * * @param HTTPRequest $request - * @param DataModel $model - * * @return HTTPResponse */ - public function handleRequest(HTTPRequest $request, DataModel $model) + public function handleRequest(HTTPRequest $request) { if (!$request) { user_error("Controller::handleRequest() not passed a request!", E_USER_ERROR); } //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 // handling if (!$this->getResponse()->isFinished()) { //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) $this->prepareResponse($response); @@ -520,7 +515,7 @@ class Controller extends RequestHandler implements TemplateGlobalProvider $template = $this->getViewer($this->getAction()); // 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) { $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, * session setting, or other things that rely on Controller::curr() will now write to this * controller object. + * + * Note: Ensure this controller is assigned a request with a valid session before pushing + * it to the stack. */ public function pushCurrent() { + // Ensure this controller has a valid session + $this->getRequest()->getSession(); 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'); } - /** - * 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 * for building the results of {@link Link()} methods. If either of the links have query strings, diff --git a/src/Control/Director.php b/src/Control/Director.php index 19aa27e7b..f5f2d920f 100644 --- a/src/Control/Director.php +++ b/src/Control/Director.php @@ -2,15 +2,12 @@ namespace SilverStripe\Control; -use InvalidArgumentException; use SilverStripe\CMS\Model\SiteTree; -use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Configurable; +use SilverStripe\Core\Environment; use SilverStripe\Core\Injector\Injector; +use SilverStripe\Core\Kernel; use SilverStripe\Dev\Deprecation; -use SilverStripe\Dev\SapphireTest; -use SilverStripe\ORM\ArrayLib; -use SilverStripe\ORM\DataModel; use SilverStripe\Versioned\Versioned; use SilverStripe\View\Requirements; use SilverStripe\View\Requirements_Backend; @@ -120,112 +117,40 @@ class Director implements TemplateGlobalProvider * * @uses handleRequest() rule-lookup logic is handled by this. * @uses TestController::handleRequest() This handles the page logic for a Director::direct() call. - * @param string $url - * @param DataModel $model + * @param HTTPRequest $request + * @return HTTPResponse * @throws HTTPResponse_Exception */ - public static function direct($url, DataModel $model) + public static function direct(HTTPRequest $request) { // check allowed hosts - if (getenv('SS_ALLOWED_HOSTS') && !Director::is_cli()) { - $all_allowed_hosts = explode(',', getenv('SS_ALLOWED_HOSTS')); - if (!in_array(static::host(), $all_allowed_hosts)) { - throw new HTTPResponse_Exception('Invalid Host', 400); + if (getenv('SS_ALLOWED_HOSTS') && !static::is_cli()) { + $allowedHosts = explode(',', getenv('SS_ALLOWED_HOSTS')); + if (!in_array(static::host(), $allowedHosts)) { + return new HTTPResponse('Invalid Host', 400); } } - - // Validate $_FILES array before merging it with $_POST - 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); - + // Pre-request + $output = RequestProcessor::singleton()->preRequest($request); if ($output === false) { - // @TODO Need to NOT proceed with the request in an elegant manner - throw new HTTPResponse_Exception(_t('SilverStripe\\Control\\Director.INVALID_REQUEST', 'Invalid request'), 400); + return new HTTPResponse(_t(__CLASS__.'.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. - $session->inst_save(); + // Save session data. Note that save() will start/resume the session if required. + $request->getSession()->save(); - // Return code for a redirection request - if (is_string($result) && substr($result, 0, 9) == 'redirect:') { - $url = substr($result, 9); - - 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); - if ($res !== false) { - $response->output(); - } else { - // @TODO Proper response here. - throw new HTTPResponse_Exception("Invalid response"); - } - - - //$controllerObj->getSession()->inst_save(); + // Post-request handling + $postRequest = RequestProcessor::singleton()->postRequest($request, $result); + if ($postRequest === false) { + return new HTTPResponse(_t(__CLASS__ . '.REQUEST_ABORTED', 'Request aborted'), 500); } + + // Return + return $result; } /** @@ -253,7 +178,7 @@ class Director implements TemplateGlobalProvider */ public static function test( $url, - $postVars = null, + $postVars = [], $session = array(), $httpMethod = null, $body = null, @@ -261,132 +186,157 @@ class Director implements TemplateGlobalProvider $cookies = array(), &$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. // Really, it's some inappropriate coupling and should be resolved by making less use of statics. - $oldReadingMode = null; if (class_exists(Versioned::class)) { $oldReadingMode = Versioned::get_reading_mode(); - } - $getVars = array(); - - if (!$httpMethod) { - $httpMethod = ($postVars || is_array($postVars)) ? "POST" : "GET"; + $finally[] = function () use ($oldReadingMode) { + Versioned::set_reading_mode($oldReadingMode); + }; } - if (!$session) { - $session = Session::create([]); + // Default httpMethod + $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 ? $cookies : Injector::inst()->createWithArgs(Cookie_Backend::class, array($cookies ?: [])); - - // 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(); - + $newVars['_COOKIE'] = $cookieJar->getAll(false); Cookie::config()->update('report_errors', false); - Requirements::set_backend(Requirements_Backend::create()); + Injector::inst()->registerService($cookieJar, Cookie_Backend::class); - if (strpos($url, '#') !== false) { - $url = substr($url, 0, strpos($url, '#')); - } + // Backup requirements + $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 if (parse_url($url, PHP_URL_HOST)) { $bits = parse_url($url); + // If a port is mentioned in the absolute URL, be sure to add that into the HTTP host - if (isset($bits['port'])) { - $_SERVER['HTTP_HOST'] = $bits['host'].':'.$bits['port']; - } else { - $_SERVER['HTTP_HOST'] = $bits['host']; - } + $newVars['_SERVER']['HTTP_HOST'] = isset($bits['port']) + ? $bits['host'].':'.$bits['port'] + : $bits['host']; } // Ensure URL is properly made relative. // Example: url passed is "/ss31/my-page" (prefixed with BASE_URL), this should be changed to "my-page" $url = self::makeRelative($url); - - $urlWithQuerystring = $url; if (strpos($url, '?') !== false) { 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 - $_REQUEST = ArrayLib::array_merge_recursive((array) $getVars, (array) $postVars); - $_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; + // Normalise vars + $newVars = HTTPRequestBuilder::cleanEnvironment($newVars); - $request = new HTTPRequest($httpMethod, $url, $getVars, $postVars, $body); + // Create new request + $request = HTTPRequestBuilder::createFromVariables($newVars, $body); if ($headers) { foreach ($headers as $k => $v) { $request->addHeader($k, $v); } } + // Apply new vars to environment + Environment::setVariables($newVars); + try { - // Pre-request filtering - $model = DataModel::inst(); - $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; + // Normal request handling + return call_user_func($callback, $request); } finally { - // Restore the super globals - $_REQUEST = $existingRequestVars; - $_GET = $existingGetVars; - $_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); + // Restore state in reverse order to assignment + foreach (array_reverse($finally) as $callback) { + call_user_func($callback); } - - Injector::unnest(); // Restore old CookieJar, etc - Config::unnest(); } } @@ -395,15 +345,14 @@ class Director implements TemplateGlobalProvider * * @skipUpgrade * @param HTTPRequest $request - * @param Session $session - * @param DataModel $model - * @return HTTPResponse|string + * @return HTTPResponse */ - protected static function handleRequest(HTTPRequest $request, Session $session, DataModel $model) + protected static function handleRequest(HTTPRequest $request) { $rules = Director::config()->uninherited('rules'); foreach ($rules as $pattern => $controllerOptions) { + // Normalise route rule if (is_string($controllerOptions)) { if (substr($controllerOptions, 0, 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); // controllerOptions provide some default arguments $arguments = array_merge($controllerOptions, $arguments); @@ -424,24 +375,20 @@ class Director implements TemplateGlobalProvider // Handle redirection if (isset($arguments['Redirect'])) { - return "redirect:" . Director::absoluteURL($arguments['Redirect'], true); - } else { - // Find the controller name - $controller = $arguments['Controller']; - $controllerObj = Injector::inst()->create($controller); - $controllerObj->setSession($session); + // Redirection + $response = new HTTPResponse(); + $response->redirect(static::absoluteURL($arguments['Redirect'])); + return $response; + } - try { - $result = $controllerObj->handleRequest($request, $model); - } catch (HTTPResponse_Exception $responseException) { - $result = $responseException->getResponse(); - } - if (!is_object($result) || $result instanceof HTTPResponse) { - return $result; - } + // Find the controller name + $controller = $arguments['Controller']; + $controllerObj = Injector::inst()->create($controller); - user_error("Bad result from url " . $request->getURL() . " handled by " . - get_class($controllerObj)." controller: ".get_class($result), E_USER_WARNING); + try { + return $controllerObj->handleRequest($request); + } catch (HTTPResponse_Exception $responseException) { + return $responseException->getResponse(); } } } @@ -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. * @@ -942,17 +859,15 @@ class Director implements TemplateGlobalProvider * Skip any further processing and immediately respond with a redirect to the passed URL. * * @param string $destURL + * @throws HTTPResponse_Exception */ protected static function force_redirect($destURL) { + // Redirect to installer $response = new HTTPResponse(); $response->redirect($destURL, 301); - HTTP::add_cache_headers($response); - - // TODO: Use an exception - ATM we can be called from _config.php, before Director#handleRequest's try block - $response->output(); - die; + throw new HTTPResponse_Exception($response); } /** @@ -983,19 +898,23 @@ class Director implements TemplateGlobalProvider * * @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. - * - * @return bool|string String of URL when unit tests running, boolean FALSE if patterns don't match request URI. + * @return bool true if already on SSL, false if doesn't match patterns (or cannot redirect) + * @throws HTTPResponse_Exception Throws exception with redirect, if successful */ 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'])) { return false; } - $matched = false; - if ($patterns) { + $matched = false; $relativeURL = self::makeRelative(Director::absoluteURL($_SERVER['REQUEST_URI'])); // protect portions of the site based on the pattern @@ -1005,31 +924,20 @@ class Director implements TemplateGlobalProvider break; } } - } else { - // protect the entire site - $matched = true; + if (!$matched) { + return false; + } } - 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; + // 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; } /** @@ -1073,47 +981,7 @@ class Director implements TemplateGlobalProvider */ public static function is_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; + return php_sapi_name() === "cli"; } /** @@ -1124,22 +992,9 @@ class Director implements TemplateGlobalProvider */ public static function get_environment_type() { - // Check saved session - if ($env = self::session_environment()) { - return $env; - } - - // Check set - if (self::$environment_type) { - return self::$environment_type; - } - - // Check getenv - if ($env = getenv('SS_ENVIRONMENT_TYPE')) { - return $env; - } - - return 'live'; + /** @var Kernel $kernel */ + $kernel = Injector::inst()->get(Kernel::class); + return $kernel->getEnvironment(); } /** @@ -1175,37 +1030,6 @@ class Director implements TemplateGlobalProvider 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 * as global variables in the templates. diff --git a/src/Control/Email/Email.php b/src/Control/Email/Email.php index a01bf6f0d..c879a6af0 100644 --- a/src/Control/Email/Email.php +++ b/src/Control/Email/Email.php @@ -96,6 +96,72 @@ class Email extends ViewableData 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. * At the moment only simple string substitutions, @@ -197,7 +263,7 @@ class Email extends ViewableData public function setSwiftMessage($swiftMessage) { $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); } $this->swiftMessage = $swiftMessage; @@ -238,7 +304,7 @@ class Email extends ViewableData } /** - * @return array + * @return string */ public function getSender() { diff --git a/src/Control/Email/SwiftPlugin.php b/src/Control/Email/SwiftPlugin.php index b00edc09e..a0274731f 100644 --- a/src/Control/Email/SwiftPlugin.php +++ b/src/Control/Email/SwiftPlugin.php @@ -11,36 +11,29 @@ class SwiftPlugin implements \Swift_Events_SendListener */ public function beforeSendPerformed(\Swift_Events_SendEvent $evt) { - /** @var \Swift_Message $message */ $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)) { $this->setTo($message, $sendAllTo); } + $ccAllTo = Email::getCCAllEmailsTo(); if (!empty($ccAllTo)) { - if (!is_array($ccAllTo)) { - $ccAllTo = array($ccAllTo => null); - } foreach ($ccAllTo as $address => $name) { $message->addCc($address, $name); } } + $bccAllTo = Email::getBCCAllEmailsTo(); if (!empty($bccAllTo)) { - if (!is_array($bccAllTo)) { - $bccAllTo = array($bccAllTo => null); - } foreach ($bccAllTo as $address => $name) { $message->addBcc($address, $name); } } + $sendAllFrom = Email::getSendAllEmailsFrom(); if (!empty($sendAllFrom)) { $this->setFrom($message, $sendAllFrom); } @@ -48,7 +41,7 @@ class SwiftPlugin implements \Swift_Events_SendListener /** * @param \Swift_Mime_Message $message - * @param string $to + * @param array|string $to */ protected function setTo($message, $to) { @@ -70,7 +63,7 @@ class SwiftPlugin implements \Swift_Events_SendListener /** * @param \Swift_Mime_Message $message - * @param string $from + * @param array|string $from */ protected function setFrom($message, $from) { diff --git a/src/Control/FlushRequestFilter.php b/src/Control/FlushRequestFilter.php index 594f5aaac..2f7c0a694 100644 --- a/src/Control/FlushRequestFilter.php +++ b/src/Control/FlushRequestFilter.php @@ -3,7 +3,6 @@ namespace SilverStripe\Control; use SilverStripe\Core\Flushable; -use SilverStripe\ORM\DataModel; use SilverStripe\Core\ClassInfo; /** @@ -11,37 +10,17 @@ use SilverStripe\Core\ClassInfo; */ class FlushRequestFilter implements RequestFilter { - - /** - * @inheritdoc - * - * @param HTTPRequest $request - * @param Session $session - * @param DataModel $model - * - * @return bool - */ - public function preRequest(HTTPRequest $request, Session $session, DataModel $model) + public function preRequest(HTTPRequest $request) { if (array_key_exists('flush', $request->getVars())) { foreach (ClassInfo::implementorsOf(Flushable::class) as $class) { $class::flush(); } } - return true; } - /** - * @inheritdoc - * - * @param HTTPRequest $request - * @param HTTPResponse $response - * @param DataModel $model - * - * @return bool - */ - public function postRequest(HTTPRequest $request, HTTPResponse $response, DataModel $model) + public function postRequest(HTTPRequest $request, HTTPResponse $response) { return true; } diff --git a/src/Control/HTTP.php b/src/Control/HTTP.php index 494530c04..1cff05eb2 100644 --- a/src/Control/HTTP.php +++ b/src/Control/HTTP.php @@ -70,7 +70,7 @@ class HTTP */ 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) { //no need to rewrite, if uri has a protocol (determined here by existence of reserved URI character ":") if (preg_match('/^\w+:/', $url)) { @@ -163,14 +163,17 @@ class HTTP * * @param string $varname * @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(). - * * @return string */ public static function setGetVar($varname, $varvalue, $currentURL = null, $separator = '&') { - $uri = $currentURL ? $currentURL : Director::makeRelative($_SERVER['REQUEST_URI']); + if (!isset($currentURL)) { + $request = Controller::curr()->getRequest(); + $currentURL = $request->getURL(true); + } + $uri = $currentURL; $isRelative = false; // We need absolute URLs for parse_url() diff --git a/src/Control/HTTPApplication.php b/src/Control/HTTPApplication.php new file mode 100644 index 000000000..f3610f113 --- /dev/null +++ b/src/Control/HTTPApplication.php @@ -0,0 +1,127 @@ +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(); + } + } +} diff --git a/src/Control/HTTPMiddleware.php b/src/Control/HTTPMiddleware.php new file mode 100644 index 000000000..a1be72d93 --- /dev/null +++ b/src/Control/HTTPMiddleware.php @@ -0,0 +1,19 @@ +httpMethod = strtoupper(self::detect_method($httpMethod, $postVars)); $this->setUrl($url); - $this->getVars = (array) $getVars; $this->postVars = (array) $postVars; $this->body = $body; @@ -435,21 +439,15 @@ class HTTPRequest implements ArrayAccess return $this->requestVar($offset); } - /** - * @ignore - * @param string $offset - * @param mixed $value - */ public function offsetSet($offset, $value) { + $this->getVars[$offset] = $value; } - /** - * @ignore - * @param mixed $offset - */ public function offsetUnset($offset) { + unset($this->getVars[$offset]); + unset($this->postVars[$offset]); } /** @@ -866,4 +864,25 @@ class HTTPRequest implements ArrayAccess 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; + } } diff --git a/src/Control/HTTPRequestBuilder.php b/src/Control/HTTPRequestBuilder.php new file mode 100644 index 000000000..a30657c72 --- /dev/null +++ b/src/Control/HTTPRequestBuilder.php @@ -0,0 +1,148 @@ + $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=.*)$/', $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; + } +} diff --git a/src/Control/RequestFilter.php b/src/Control/RequestFilter.php index a7fbd0b97..0d64980a8 100644 --- a/src/Control/RequestFilter.php +++ b/src/Control/RequestFilter.php @@ -2,8 +2,6 @@ namespace SilverStripe\Control; -use SilverStripe\ORM\DataModel; - /** * A request filter is an object that's executed before and after a * request occurs. By returning 'false' from the preRequest method, @@ -14,24 +12,20 @@ use SilverStripe\ORM\DataModel; */ interface RequestFilter { - /** * Filter executed before a request processes * * @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) */ - public function preRequest(HTTPRequest $request, Session $session, DataModel $model); + public function preRequest(HTTPRequest $request); /** * Filter executed AFTER a request * * @param HTTPRequest $request Request container object - * @param HTTPResponse $response Response output object - * @param DataModel $model Current DataModel - * @return boolean Whether to continue processing other filters. Null or true will continue processing (optional) + * @param HTTPResponse $response + * @return bool 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); } diff --git a/src/Control/RequestHandler.php b/src/Control/RequestHandler.php index 0a087835f..86f4460aa 100644 --- a/src/Control/RequestHandler.php +++ b/src/Control/RequestHandler.php @@ -6,7 +6,6 @@ use InvalidArgumentException; use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Config\Config; use SilverStripe\Dev\Debug; -use SilverStripe\ORM\DataModel; use SilverStripe\Security\Security; use SilverStripe\Security\PermissionFailureException; use SilverStripe\Security\Permission; @@ -123,22 +122,9 @@ class RequestHandler extends ViewableData $this->setRequest(new NullHTTPRequest()); - // This will prevent bugs if setDataModel() isn't called. - $this->model = DataModel::inst(); - parent::__construct(); } - /** - * Set the DataModel for this request. - * - * @param DataModel $model - */ - public function setDataModel($model) - { - $this->model = $model; - } - /** * Handles URL requests. * @@ -156,10 +142,9 @@ class RequestHandler extends ViewableData * customise the controller. * * @param HTTPRequest $request The object that is reponsible for distributing URL parsing - * @param DataModel $model * @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 if ($this->brokenOnConstruct) { @@ -170,7 +155,6 @@ class RequestHandler extends ViewableData } $this->setRequest($request); - $this->setDataModel($model); $match = $this->findAction($request); @@ -237,7 +221,7 @@ class RequestHandler extends ViewableData if ($result instanceof HasRequestHandler) { $result = $result->getRequestHandler(); } - $returnValue = $result->handleRequest($request, $model); + $returnValue = $result->handleRequest($request); // Array results can be used to handle if (is_array($returnValue)) { diff --git a/src/Control/RequestProcessor.php b/src/Control/RequestProcessor.php index 70e61e99c..a1e0cad1a 100644 --- a/src/Control/RequestProcessor.php +++ b/src/Control/RequestProcessor.php @@ -3,7 +3,6 @@ namespace SilverStripe\Control; use SilverStripe\Core\Injector\Injectable; -use SilverStripe\ORM\DataModel; /** * 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; } - public function preRequest(HTTPRequest $request, Session $session, DataModel $model) + public function preRequest(HTTPRequest $request) { foreach ($this->filters as $filter) { - $res = $filter->preRequest($request, $session, $model); + $res = $filter->preRequest($request); if ($res === false) { return false; } @@ -45,10 +44,10 @@ class RequestProcessor implements RequestFilter return null; } - public function postRequest(HTTPRequest $request, HTTPResponse $response, DataModel $model) + public function postRequest(HTTPRequest $request, HTTPResponse $response) { foreach ($this->filters as $filter) { - $res = $filter->postRequest($request, $response, $model); + $res = $filter->postRequest($request, $response); if ($res === false) { return false; } diff --git a/src/Control/Session.php b/src/Control/Session.php index 4b9edeb7b..bf5f3f230 100644 --- a/src/Control/Session.php +++ b/src/Control/Session.php @@ -2,10 +2,8 @@ namespace SilverStripe\Control; -use SilverStripe\Core\Config\Config; -use SilverStripe\Core\Injector\Injectable; -use SilverStripe\Core\Injector\Injector; -use SilverStripe\Dev\Deprecation; +use BadMethodCallException; +use SilverStripe\Core\Config\Configurable; /** * Handles all manipulation of the session. @@ -89,7 +87,7 @@ use SilverStripe\Dev\Deprecation; */ class Session { - use Injectable; + use Configurable; /** * Set session timeout in seconds. @@ -130,12 +128,23 @@ class Session 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(); + /** + * Get user agent for this request + * + * @return string + */ protected function userAgent() { 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. * - * @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) { if ($data instanceof Session) { - $data = $data->inst_getAll(); + $data = $data->getAll(); } $this->data = $data; + } + /** + * Init this session instance before usage + */ + public function init() + { + if (!$this->isStarted()) { + $this->start(); + } + + // Funny business detected! if (isset($this->data['HTTP_USER_AGENT'])) { - if ($this->data['HTTP_USER_AGENT'] != $this->userAgent()) { - // Funny business detected! - $this->inst_clearAll(); - $this->inst_destroy(); - $this->inst_start(); + if ($this->data['HTTP_USER_AGENT'] !== $this->userAgent()) { + $this->clearAll(); + $this->destroy(); + $this->start(); } } } /** - * Add a value to a specific key in the session array - * - * @param string $name - * @param mixed $val + * Destroy existing session and restart */ - public static function add_to_array($name, $val) + public function restart() { - return self::current_session()->inst_addToArray($name, $val); + $this->destroy(); + $this->init(); } /** - * Set a key/value pair in the session + * Determine if this session has started * - * @param string $name Key - * @param string|array $val Value + * @return bool */ - public static function set($name, $val) + public function isStarted() { - return self::current_session()->inst_set($name, $val); + return isset($this->data); } /** - * Return a specific value by session key + * Begin session * - * @param string $name Key to lookup - * @return mixed + * @param string $sid */ - public static function get($name) + public function start($sid = null) { - 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; + if ($this->isStarted()) { + throw new BadMethodCallException("Session has already started"); } - } - public function inst_start($sid = null) - { - $path = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_path'); + $path = $this->config()->get('cookie_path'); if (!$path) { $path = Director::baseURL(); } - $domain = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_domain'); - $secure = Director::is_https() && Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_secure'); - $session_path = Config::inst()->get('SilverStripe\\Control\\Session', 'session_store_path'); - $timeout = Config::inst()->get('SilverStripe\\Control\\Session', 'timeout'); + $domain = $this->config()->get('cookie_domain'); + $secure = Director::is_https() && $this->config()->get('cookie_secure'); + $session_path = $this->config()->get('session_store_path'); + $timeout = $this->config()->get('timeout'); // Director::baseURL can return absolute domain names - this extracts the relevant parts // for the session otherwise we can get broken session cookies @@ -300,6 +261,8 @@ class Session session_start(); $this->data = isset($_SESSION) ? $_SESSION : array(); + } else { + $this->data = []; } // 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 ($removeCookie) { - $path = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_path') ?: Director::baseURL(); - $domain = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_domain'); - $secure = Config::inst()->get('SilverStripe\\Control\\Session', 'cookie_secure'); - + $path = $this->config()->get('cookie_path') ?: Director::baseURL(); + $domain = $this->config()->get('cookie_domain'); + $secure = $this->config()->get('cookie_secure'); Cookie::force_expiry(session_name(), $path, $domain, $secure, true); } - session_destroy(); - - // Clean up the superglobal - session_destroy does not do it. - // http://nz1.php.net/manual/en/function.session-destroy.php - unset($_SESSION); - $this->data = array(); } + // Clean up the superglobal - session_destroy does not do it. + // http://nz1.php.net/manual/en/function.session-destroy.php + unset($_SESSION); + $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 if (strpos($name, '.') === false) { $this->data[$name] = $val; @@ -360,10 +336,21 @@ class Session $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); // 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; } - 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 if (strpos($name, '.') === false) { 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); // 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) { // don't clear a record that doesn't exist if (!isset($var[$n])) { - return; + return $this; } $var = &$var[$n]; } @@ -432,38 +439,54 @@ class Session $var = 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)) { 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; } - 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 * Only save the changes, so that anyone manipulating $_SESSION directly doesn't get burned. */ - public function inst_save() + public function save() { if ($this->changedData) { - $this->inst_finalize(); + $this->finalize(); - if (!isset($_SESSION)) { - $this->inst_start(); + if (!$this->isStarted()) { + $this->start(); } $this->recursivelyApply($this->changedData, $_SESSION); @@ -493,55 +516,11 @@ class Session /** * Return the changed data, for debugging purposes. + * * @return array */ - public function inst_changedData() + public function 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); - } } diff --git a/src/Core/Application.php b/src/Core/Application.php new file mode 100644 index 000000000..9306a9b96 --- /dev/null +++ b/src/Core/Application.php @@ -0,0 +1,29 @@ +countManifests() < 2) { + if ($loader->countManifests() <= 1) { user_error( "Unable to unnest root Config, please make sure you don't have mis-matched nest/unnest", E_USER_WARNING diff --git a/src/Core/Config/ConfigLoader.php b/src/Core/Config/ConfigLoader.php index c4a299828..811fc062f 100644 --- a/src/Core/Config/ConfigLoader.php +++ b/src/Core/Config/ConfigLoader.php @@ -2,6 +2,7 @@ namespace SilverStripe\Core\Config; +use BadMethodCallException; use SilverStripe\Config\Collections\ConfigCollectionInterface; /** @@ -25,7 +26,7 @@ class ConfigLoader */ 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() { + 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]; } @@ -78,14 +87,31 @@ class ConfigLoader } /** - * Nest the current manifest + * Nest the config loader and activates it * - * @return ConfigCollectionInterface + * @return static */ public function nest() { - $manifest = $this->getManifest()->nest(); - $this->pushManifest($manifest); - return $manifest; + // Nest config + $manifest = clone $this->getManifest(); + + // 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; } } diff --git a/src/Core/Config/CoreConfigFactory.php b/src/Core/Config/CoreConfigFactory.php index 3c1a9a217..88e1343c3 100644 --- a/src/Core/Config/CoreConfigFactory.php +++ b/src/Core/Config/CoreConfigFactory.php @@ -2,22 +2,18 @@ namespace SilverStripe\Core\Config; -use Monolog\Handler\ErrorLogHandler; -use Monolog\Handler\StreamHandler; -use Monolog\Logger; -use Psr\Log\LoggerInterface; use Psr\SimpleCache\CacheInterface; use SilverStripe\Config\Collections\CachedConfigCollection; use SilverStripe\Config\Collections\MemoryConfigCollection; use SilverStripe\Config\Transformer\PrivateStaticTransformer; use SilverStripe\Config\Transformer\YamlTransformer; -use SilverStripe\Control\Director; use SilverStripe\Core\Cache\CacheFactory; use SilverStripe\Core\Config\Middleware\ExtensionMiddleware; use SilverStripe\Core\Config\Middleware\InheritanceMiddleware; +use SilverStripe\Core\Injector\Injector; +use SilverStripe\Core\Kernel; use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ModuleLoader; -use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Finder\Finder; /** @@ -26,19 +22,18 @@ use Symfony\Component\Finder\Finder; 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) { - self::$inst = new static(); - } - return self::$inst; + $this->cacheFactory = $cacheFactory; } /** @@ -46,20 +41,19 @@ class CoreConfigFactory * This will be an immutable cached config, * which conditionally generates a nested "core" config. * - * @param bool $flush - * @param CacheFactory $cacheFactory * @return CachedConfigCollection */ - public function createRoot($flush, CacheFactory $cacheFactory) + public function createRoot() { $instance = new CachedConfigCollection(); // Create config cache - $cache = $cacheFactory->create(CacheInterface::class.'.configcache', [ - 'namespace' => 'configcache' - ]); - $instance->setCache($cache); - $instance->setFlush($flush); + if ($this->cacheFactory) { + $cache = $this->cacheFactory->create(CacheInterface::class . '.configcache', [ + 'namespace' => 'configcache' + ]); + $instance->setCache($cache); + } // Set collection creator $instance->setCollectionCreator(function () { @@ -171,8 +165,11 @@ class CoreConfigFactory } ) ->addRule('environment', function ($env) { - $current = Director::get_environment_type(); - return strtolower($current) === strtolower($env); + // Note: The below relies on direct assignment of kernel to injector instance, + // 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) { return ModuleLoader::inst()->getManifest()->moduleExists($module); diff --git a/src/Core/Convert.php b/src/Core/Convert.php index da5b136fc..4b167434a 100644 --- a/src/Core/Convert.php +++ b/src/Core/Convert.php @@ -550,4 +550,47 @@ class Convert 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]; + } } diff --git a/src/Core/Core.php b/src/Core/Core.php deleted file mode 100644 index 3cf52008e..000000000 --- a/src/Core/Core.php +++ /dev/null @@ -1,324 +0,0 @@ - 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; -} diff --git a/src/Core/CoreKernel.php b/src/Core/CoreKernel.php new file mode 100644 index 000000000..df3339b18 --- /dev/null +++ b/src/Core/CoreKernel.php @@ -0,0 +1,580 @@ +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.

' + . 'See the ' + . 'Environment Management 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; + } +} diff --git a/src/Core/Environment.php b/src/Core/Environment.php new file mode 100644 index 000000000..5f46afa12 --- /dev/null +++ b/src/Core/Environment.php @@ -0,0 +1,154 @@ + $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; + } +} diff --git a/src/Core/Extensible.php b/src/Core/Extensible.php index dc8254267..ca0508e5f 100644 --- a/src/Core/Extensible.php +++ b/src/Core/Extensible.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use SilverStripe\Control\RequestHandler; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Injector\Injector; +use SilverStripe\ORM\DataObject; use SilverStripe\View\ViewableData; /** @@ -271,8 +272,8 @@ trait Extensible } Config::modify()->set($class, 'extensions', $config); - // unset singletons to avoid side-effects - Injector::inst()->unregisterAllObjects(); + // Unset singletons + Injector::inst()->unregisterObjects($class); // unset some caches $subclasses = ClassInfo::subclassesFor($class); diff --git a/src/Core/Injector/Injector.php b/src/Core/Injector/Injector.php index 769c95a36..6b3f28abd 100644 --- a/src/Core/Injector/Injector.php +++ b/src/Core/Injector/Injector.php @@ -2,14 +2,15 @@ namespace SilverStripe\Core\Injector; +use ArrayObject; +use InvalidArgumentException; +use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; +use ReflectionMethod; +use ReflectionObject; +use ReflectionProperty; use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Config\Config; -use ReflectionProperty; -use ArrayObject; -use ReflectionObject; -use ReflectionMethod; -use Psr\Container\ContainerInterface; use SilverStripe\Dev\Deprecation; /** @@ -202,21 +203,18 @@ class Injector implements ContainerInterface { $this->injectMap = array(); $this->serviceCache = array( - 'Injector' => $this, + 'Injector' => $this, ); - $this->specs = array( - 'Injector' => array('class' => 'SilverStripe\\Core\\Injector\\Injector') - ); - + $this->specs = [ + 'Injector' => ['class' => static::class] + ]; $this->autoProperties = array(); - - $creatorClass = isset($config['creator']) ? $config['creator'] - : 'SilverStripe\\Core\\Injector\\InjectionCreator'; + : InjectionCreator::class; $locatorClass = isset($config['locator']) ? $config['locator'] - : 'SilverStripe\\Core\\Injector\\SilverStripeServiceConfigurationLocator'; + : SilverStripeServiceConfigurationLocator::class; $this->objectCreator = new $creatorClass; $this->configLocator = new $locatorClass; @@ -234,28 +232,11 @@ class Injector implements ContainerInterface protected $nestedFrom = null; /** - * If a user wants to use the injector as a static reference - * - * @param array $config * @return Injector */ - public static function inst($config = null) + public static function inst() { - if (!self::$instance) { - 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; + return InjectorLoader::inst()->getManifest(); } /** @@ -270,11 +251,10 @@ class Injector implements ContainerInterface */ public static function nest() { - $current = self::$instance; - - $new = clone $current; - $new->nestedFrom = $current; - return self::set_inst($new); + // Clone current injector and nest + $new = clone self::inst(); + InjectorLoader::inst()->pushManifest($new); + return $new; } /** @@ -285,15 +265,17 @@ class Injector implements ContainerInterface */ public static function unnest() { - if (self::inst()->nestedFrom) { - self::set_inst(self::inst()->nestedFrom); - } else { + // Unnest unless we would be left at 0 manifests + $loader = InjectorLoader::inst(); + if ($loader->countManifests() <= 1) { user_error( "Unable to unnest root Injector, please make sure you don't have mis-matched nest/unnest", 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... if (empty($class)) { - throw new \InvalidArgumentException('Missing spec class'); + throw new InvalidArgumentException('Missing spec class'); } $spec['class'] = $class; @@ -654,21 +636,21 @@ class Injector implements ContainerInterface // Format validation 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." ); } 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])) { - 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 $objectMethod = array($object, $method[0]); 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 @@ -850,17 +832,18 @@ class Injector implements ContainerInterface * @param object $service The object to register * @param string $replace The name of the object to replace (if different to the * class name of the object to register) + * @return $this */ public function registerService($service, $replace = null) { $registerAt = get_class($service); - if ($replace != null) { + if ($replace !== null) { $registerAt = $replace; } $this->specs[$registerAt] = array('class' => get_class($service)); $this->serviceCache[$registerAt] = $service; - $this->inject($service); + return $this; } /** @@ -868,18 +851,40 @@ class Injector implements ContainerInterface * by the inject * * @param string $name The name to unregister + * @return $this */ public function unregisterNamedObject($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; } /** diff --git a/src/Core/Injector/InjectorLoader.php b/src/Core/Injector/InjectorLoader.php new file mode 100644 index 000000000..f618dccd7 --- /dev/null +++ b/src/Core/Injector/InjectorLoader.php @@ -0,0 +1,117 @@ +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; + } +} diff --git a/src/Core/Injector/SilverStripeServiceConfigurationLocator.php b/src/Core/Injector/SilverStripeServiceConfigurationLocator.php index f6d7ea439..55020e2a3 100644 --- a/src/Core/Injector/SilverStripeServiceConfigurationLocator.php +++ b/src/Core/Injector/SilverStripeServiceConfigurationLocator.php @@ -51,7 +51,7 @@ class SilverStripeServiceConfigurationLocator implements ServiceConfigurationLoc return $this->configs[$name]; } - $config = Config::inst()->get('SilverStripe\\Core\\Injector\\Injector', $name); + $config = Config::inst()->get(Injector::class, $name); $this->configs[$name] = $config; return $config; } diff --git a/src/Core/Kernel.php b/src/Core/Kernel.php new file mode 100644 index 000000000..a80a23203 --- /dev/null +++ b/src/Core/Kernel.php @@ -0,0 +1,135 @@ +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. * diff --git a/src/Core/Manifest/ClassManifest.php b/src/Core/Manifest/ClassManifest.php index e64b9695a..6fd04c03b 100644 --- a/src/Core/Manifest/ClassManifest.php +++ b/src/Core/Manifest/ClassManifest.php @@ -29,12 +29,11 @@ class ClassManifest protected $base; /** - * Set if including test classes + * Used to build cache during boot * - * @see TestOnly - * @var bool + * @var CacheFactory */ - protected $tests; + protected $cacheFactory; /** * Cache to use, if caching. @@ -56,7 +55,7 @@ class ClassManifest * * @var array */ - protected $classes = array(); + protected $classes = array(); /** * List of root classes with no parent class @@ -122,27 +121,30 @@ class ClassManifest * from the cache or re-scanning for classes. * * @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. */ - public function __construct( - $base, - $includeTests = false, - $forceRegen = false, - CacheFactory $cacheFactory = null - ) { + public function __construct($base, CacheFactory $cacheFactory = null) + { $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 - if ($cacheFactory) { - $this->cache = $cacheFactory->create( + if ($this->cacheFactory) { + $this->cache = $this->cacheFactory->create( CacheInterface::class.'.classmanifest', [ 'namespace' => 'classmanifest' . ($includeTests ? '_tests' : '') ] ); } - $this->cacheKey = 'manifest'; if (!$forceRegen && $this->cache && ($data = $this->cache->get($this->cacheKey))) { $this->classes = $data['classes']; @@ -151,7 +153,7 @@ class ClassManifest $this->implementors = $data['implementors']; $this->traits = $data['traits']; } else { - $this->regenerate(); + $this->regenerate($includeTests); } } @@ -346,8 +348,10 @@ class ClassManifest /** * Completely regenerates the manifest file. + * + * @param bool $includeTests */ - public function regenerate() + public function regenerate($includeTests) { $resets = array( 'classes', 'roots', 'children', 'descendants', 'interfaces', @@ -363,8 +367,10 @@ class ClassManifest $finder->setOptions(array( 'name_regex' => '/^[^_].*\\.php$/', 'ignore_files' => array('index.php', 'main.php', 'cli-script.php'), - 'ignore_tests' => !$this->tests, - 'file_callback' => array($this, 'handleFile'), + 'ignore_tests' => !$includeTests, + 'file_callback' => function ($basename, $pathname) use ($includeTests) { + $this->handleFile($basename, $pathname, $includeTests); + }, )); $finder->find($this->base); @@ -384,7 +390,7 @@ class ClassManifest } } - public function handleFile($basename, $pathname) + public function handleFile($basename, $pathname, $includeTests) { $classes = null; $interfaces = null; @@ -397,6 +403,7 @@ class ClassManifest $key = preg_replace('/[^a-zA-Z0-9_]/', '_', $basename) . '_' . md5_file($pathname); // Attempt to load from cache + $changed = false; if ($this->cache && ($data = $this->cache->get($key)) && $this->validateItemCache($data) @@ -405,6 +412,7 @@ class ClassManifest $interfaces = $data['interfaces']; $traits = $data['traits']; } else { + $changed = true; // Build from php file parser $fileContents = ClassContentRemover::remove_class_content($pathname); try { @@ -418,23 +426,16 @@ class ClassManifest $classes = $this->getVisitor()->getClasses(); $interfaces = $this->getVisitor()->getInterfaces(); $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 foreach ($classes as $className => $classInfo) { - $extends = isset($classInfo['extends']) ? $classInfo['extends'] : null; - $implements = isset($classInfo['interfaces']) ? $classInfo['interfaces'] : null; - + $extends = !empty($classInfo['extends']) + ? array_map('strtolower', $classInfo['extends']) + : []; + $implements = !empty($classInfo['interfaces']) + ? array_map('strtolower', $classInfo['interfaces']) + : []; $lowercaseName = strtolower($className); if (array_key_exists($lowercaseName, $this->classes)) { 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; if ($extends) { foreach ($extends as $ancestor) { - $ancestor = strtolower($ancestor); - if (!isset($this->children[$ancestor])) { $this->children[$ancestor] = array($className); } else { @@ -463,8 +472,6 @@ class ClassManifest if ($implements) { foreach ($implements as $interface) { - $interface = strtolower($interface); - if (!isset($this->implementors[$interface])) { $this->implementors[$interface] = array($className); } else { @@ -480,6 +487,16 @@ class ClassManifest foreach ($traits as $traitName => $traitInfo) { $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); + } } /** diff --git a/src/Core/Manifest/ClassManifestVisitor.php b/src/Core/Manifest/ClassManifestVisitor.php index d97307e1c..8e9c15e75 100644 --- a/src/Core/Manifest/ClassManifestVisitor.php +++ b/src/Core/Manifest/ClassManifestVisitor.php @@ -30,7 +30,7 @@ class ClassManifestVisitor extends NodeVisitorAbstract public function enterNode(Node $node) { if ($node instanceof Node\Stmt\Class_) { - $extends = ''; + $extends = []; $interfaces = []; if ($node->extends) { diff --git a/src/Core/Manifest/ModuleLoader.php b/src/Core/Manifest/ModuleLoader.php index 0c4152dee..20b294a74 100644 --- a/src/Core/Manifest/ModuleLoader.php +++ b/src/Core/Manifest/ModuleLoader.php @@ -22,7 +22,7 @@ class ModuleLoader */ 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); } + + /** + * 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); + } + } } diff --git a/src/Core/Manifest/ModuleManifest.php b/src/Core/Manifest/ModuleManifest.php index eb9b8356e..04abc183e 100644 --- a/src/Core/Manifest/ModuleManifest.php +++ b/src/Core/Manifest/ModuleManifest.php @@ -26,11 +26,11 @@ class ModuleManifest 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 @@ -87,19 +87,24 @@ class ModuleManifest * from the cache or re-scanning for classes. * * @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 */ - public function __construct($base, $includeTests = false, $forceRegen = false, CacheFactory $cacheFactory = null) + public function __construct($base, CacheFactory $cacheFactory = null) { $this->base = $base; - $this->cacheKey = sha1($base).'_modules'; - $this->includeTests = $includeTests; + $this->cacheKey = sha1($base) . '_modules'; + $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 - if ($cacheFactory) { - $this->cache = $cacheFactory->create( + if ($this->cacheFactory) { + $this->cache = $this->cacheFactory->create( CacheInterface::class.'.modulemanifest', [ 'namespace' => 'modulemanifest' . ($includeTests ? '_tests' : '') ] ); diff --git a/src/Core/Startup/ErrorControlChainMiddleware.php b/src/Core/Startup/ErrorControlChainMiddleware.php new file mode 100644 index 000000000..d5a10ef2a --- /dev/null +++ b/src/Core/Startup/ErrorControlChainMiddleware.php @@ -0,0 +1,119 @@ +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; + } +} diff --git a/src/Core/Startup/ParameterConfirmationToken.php b/src/Core/Startup/ParameterConfirmationToken.php index 862ed4739..45b363097 100644 --- a/src/Core/Startup/ParameterConfirmationToken.php +++ b/src/Core/Startup/ParameterConfirmationToken.php @@ -2,7 +2,10 @@ 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; /** @@ -25,6 +28,11 @@ class ParameterConfirmationToken */ protected $parameterName = null; + /** + * @var HTTPRequest + */ + protected $request = null; + /** * The parameter given * @@ -88,17 +96,19 @@ class ParameterConfirmationToken * Create a new ParameterConfirmationToken * * @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 $this->parameterName = $parameterName; + $this->request = $request; // 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 - $token = isset($_GET[$parameterName.'token']) ? $_GET[$parameterName.'token'] : null; + $token = $request->getVar($parameterName.'token'); if ($this->checkToken($token)) { $this->token = $token; } @@ -151,7 +161,7 @@ class ParameterConfirmationToken */ 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; - - protected function currentAbsoluteURL() + /** + * Get redirect url, excluding querystring + * + * @return string + */ + protected function currentURL() { - global $url; - - // Are we http or https? Replicates Director::is_https() without its dependencies/ - $proto = 'http'; - // 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)); + return Controller::join_links( + BASE_URL, + '/', + $this->request->getURL(false) + ); } /** * Forces a reload of the request with the token included - * This method will terminate the script with `die` + * + * @return HTTPResponse */ 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 = <<location.href='$locationJS'; + +You are being redirected. If you are not redirected soon, click here to continue the flush +HTML; - // What's our GET params (ensuring they include the original parameter + a new token) - $params = array_merge($_GET, $this->params()); - unset($params['url']); - - if ($params) { - $location .= '?'.http_build_query($params); - } - - // And redirect - if (headers_sent()) { - echo " - - -You are being redirected. If you are not redirected soon, click here to continue the flush -"; - } else { - header('location: '.$location, true, 302); - } - die; + // Build response + $result = new HTTPResponse($body); + $result->redirect($location); + return $result; } /** @@ -249,13 +223,14 @@ You are being redirected. If you are not redirected soon, cl * return the non-validated token with the highest priority * * @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 */ - public static function prepare_tokens($keys) + public static function prepare_tokens($keys, HTTPRequest $request) { $target = null; foreach ($keys as $key) { - $token = new ParameterConfirmationToken($key); + $token = new ParameterConfirmationToken($key, $request); // Validate this token if ($token->reloadRequired()) { $token->suppress(); diff --git a/src/Core/TempFolder.php b/src/Core/TempFolder.php new file mode 100644 index 000000000..821f88dde --- /dev/null +++ b/src/Core/TempFolder.php @@ -0,0 +1,117 @@ +deleteExistingRecords) { diff --git a/src/Dev/CSSContentParser.php b/src/Dev/CSSContentParser.php index 56f201617..ed63515d0 100644 --- a/src/Dev/CSSContentParser.php +++ b/src/Dev/CSSContentParser.php @@ -69,7 +69,7 @@ class CSSContentParser * See {@link getByXpath()} for a more direct selector syntax. * * @param String $selector - * @return SimpleXMLElement + * @return SimpleXMLElement[] */ public function getBySelector($selector) { @@ -81,7 +81,7 @@ class CSSContentParser * Allows querying the content through XPATH selectors. * * @param String $xpath SimpleXML compatible XPATH statement - * @return SimpleXMLElement|false + * @return SimpleXMLElement[] */ public function getByXpath($xpath) { diff --git a/src/Dev/DevBuildController.php b/src/Dev/DevBuildController.php index b661d7da1..9f0cd669d 100644 --- a/src/Dev/DevBuildController.php +++ b/src/Dev/DevBuildController.php @@ -21,7 +21,7 @@ class DevBuildController extends Controller { if (Director::is_cli()) { $da = DatabaseAdmin::create(); - return $da->handleRequest($request, $this->model); + return $da->handleRequest($request); } else { $renderer = DebugView::create(); echo $renderer->renderHeader(); @@ -29,7 +29,7 @@ class DevBuildController extends Controller echo "
"; $da = DatabaseAdmin::create(); - $response = $da->handleRequest($request, $this->model); + $response = $da->handleRequest($request); echo "
"; echo $renderer->renderFooter(); diff --git a/src/Dev/DevelopmentAdmin.php b/src/Dev/DevelopmentAdmin.php index c946f8494..c71c58262 100644 --- a/src/Dev/DevelopmentAdmin.php +++ b/src/Dev/DevelopmentAdmin.php @@ -42,6 +42,16 @@ class DevelopmentAdmin extends Controller '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() { parent::init(); @@ -52,10 +62,11 @@ class DevelopmentAdmin extends Controller // 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. + $allowAllCLI = static::config()->get('allow_all_cli'); $canAccess = ( $requestedDevBuild || Director::isDev() - || Director::is_cli() + || (Director::is_cli() && $allowAllCLI) // Its important that we don't run this check if dev/build was requested || Permission::check("ADMIN") ); diff --git a/src/Dev/FixtureBlueprint.php b/src/Dev/FixtureBlueprint.php index a511f86fd..bb753a8c0 100644 --- a/src/Dev/FixtureBlueprint.php +++ b/src/Dev/FixtureBlueprint.php @@ -2,13 +2,13 @@ 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 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. @@ -94,7 +94,7 @@ class FixtureBlueprint try { $class = $this->class; $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 // This is just in case field setters triggered by the population code in the next block diff --git a/src/Dev/FunctionalTest.php b/src/Dev/FunctionalTest.php index 53c20bd6f..1ee146936 100644 --- a/src/Dev/FunctionalTest.php +++ b/src/Dev/FunctionalTest.php @@ -2,6 +2,7 @@ namespace SilverStripe\Dev; +use SilverStripe\Control\Controller; use SilverStripe\Control\Session; use SilverStripe\Control\HTTPResponse; use SilverStripe\Core\Config\Config; @@ -33,7 +34,7 @@ use SimpleXMLElement; * } * */ -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. @@ -84,12 +85,13 @@ class FunctionalTest extends SapphireTest protected function setUp() { + parent::setUp(); + // Skip calling FunctionalTest directly. if (static::class == __CLASS__) { $this->markTestSkipped(sprintf('Skipping %s ', static::class)); } - parent::setUp(); $this->mainSession = new TestSession(); // Disable theme, if necessary @@ -114,9 +116,8 @@ class FunctionalTest extends SapphireTest protected function tearDown() { SecurityToken::enable(); - - parent::tearDown(); unset($this->mainSession); + parent::tearDown(); } /** @@ -408,11 +409,11 @@ class FunctionalTest extends SapphireTest public function useDraftSite($enabled = true) { if ($enabled) { - $this->session()->inst_set('readingMode', 'Stage.Stage'); - $this->session()->inst_set('unsecuredDraftSite', true); + $this->session()->set('readingMode', 'Stage.Stage'); + $this->session()->set('unsecuredDraftSite', true); } else { - $this->session()->inst_set('readingMode', 'Stage.Live'); - $this->session()->inst_set('unsecuredDraftSite', false); + $this->session()->set('readingMode', 'Stage.Live'); + $this->session()->set('unsecuredDraftSite', false); } } diff --git a/src/Dev/Install/InstallRequirements.php b/src/Dev/Install/InstallRequirements.php new file mode 100644 index 000000000..889506338 --- /dev/null +++ b/src/Dev/Install/InstallRequirements.php @@ -0,0 +1,1131 @@ +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 & + // which will results in links like ?param=value&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 "

The following problems are preventing me from installing SilverStripe CMS:

\n\n"; + foreach ($this->errors as $error) { + echo "
  • " . htmlentities(implode(", ", $error), ENT_COMPAT, 'UTF-8') . "
  • \n"; + } + } + } + + public function showTable($section = null) + { + if ($section) { + $tests = $this->tests[$section]; + $id = strtolower(str_replace(' ', '_', $section)); + echo ""; + foreach ($tests as $test => $result) { + echo ""; + } + echo "
    $test" + . nl2br(htmlentities($result[1], ENT_COMPAT, 'UTF-8')) . "
    "; + } 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 .= "$test" + . nl2br(htmlentities($result[1], ENT_COMPAT, 'UTF-8')) . ""; + } + $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 "
    $section Show All Requirements $text
    "; + echo ""; + echo $output; + echo "
    "; + } + } + } + + 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); + } +} diff --git a/src/Dev/Install/Installer.php b/src/Dev/Install/Installer.php new file mode 100644 index 000000000..f0fe2ca82 --- /dev/null +++ b/src/Dev/Install/Installer.php @@ -0,0 +1,491 @@ +getBaseDir(); + } + + protected function installHeader() + { + ?> + + + + Installing SilverStripe... + + + + +
    +
    +
    + + +

    SilverStripe

    +
    +
    +
    + + +
    + +
    +
    +

    Installing SilverStripe...

    + +

    I am now running through the installation steps (this should take about 30 seconds)

    + +

    If you receive a fatal error, refresh this page to continue the installation

    +
      + 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 "

      Bad config submitted

      ";
      +            print_r($config);
      +            echo "
      "; + 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", "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", <<statusMessage("Setting up 'mysite/_config/config.yml'"); + $this->writeToFile("mysite/_config/config.yml", <<checkModuleExists('cms')) { + $this->writeToFile("mysite/code/RootURLController.php", <<Your site is now set up. Start adding controllers to mysite to get started."; + } + +} +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 <<SilverStripe successfully installed; I am now redirecting you to your SilverStripe site... + + +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 = << + Order deny,allow + Deny from all + Allow from 127.0.0.1 + + +# Deny access to IIS configuration + + Order deny,allow + Deny from all + + +# Deny access to YAML configuration files which might include sensitive information + + Order allow,deny + Deny from all + + +# Route errors to static pages automatically generated by SilverStripe +ErrorDocument 404 /assets/error-404.html +ErrorDocument 500 /assets/error-500.html + + + + # Turn off index.php handling requests to the homepage fixes issue in apache >=2.4 + + DirectoryIndex disabled + + + 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] + +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; + + $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 <<Testing... + + +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 "
    • $msg
    • \n"; + flush(); + } +} diff --git a/src/Dev/Install/client/images/logo.gif b/src/Dev/Install/client/images/logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..77642f24ae53eb6ba2d4bc25e3714df1257d8bd5 GIT binary patch literal 2097 zcmV-12+sFMNk%w1VW9w|0J8=F09ulIk&C>@#hb0D-s9pBWSRf}|3HJSf|!!O*5u9B z)9&%`th%=_d8crq!jHGlA^8LW000I6EC2ui0HFY+000F4(8x)vy*TU5yZ>M)j%0Ze zi>a<`>uyi+ymW2f)^Y$mf&aiDC{f_?9KxhBsWUoL%c!(PbUJ6#toE~TSgnVxcx*{D zzSaVHE%u_nP_{ftIKRJ%xgO}gg&ezQVVwX00%zwWwEDD z2Au>60tz$$?^*+eH3&R#&@bA72N0Vj@CP6ljV?w1JQZ+|(G`J^;~v^bKrq$;L$q2# z^XNicx0kLqriA&T;mmo6WcDgogCECrB@yX_R)A+slt&F#61qy>N~lKxNNq%bsZ$q5 zvwq1J=z%h;H>hUa8b~c6wl$WD#gD2f;DvUQ--EVh=VkPjCs;^rGYMT#iWhMR%#DmU>+e z9}7Gu&=gjq<=~z09J!vRYk);a;bpiRgjGuaYGgGEBqLEj$IMQVA2@p+|?k_~yU?Se3#PJi~=h-5xQXrkrz5oT$Q$ zdO)Zn4;Hvl;|Mttvd02Ro<^VnI=J`9Er8TF*oH1xi47nE4%q_`Oo|7H0A3pLWDV7I zbcmKmNy!46V;$0EXJ^)wCJ#WZagvUJ-9TP%j_lc)nqObYzQhA zqlMmS1*LizdJm!`IC=}DANu2CX6j%Xh^BXOIsz}_^qG^TT0M|w32NVCBUpC z;C9nF5&?I|0S?HDL#@*BstW-KHLI-u3chxO-a=CRd5x>o(gCY4pk~{GwGI*LZ40R0 z^z68eHmMAN=-Pm+Cjhk@#JiHPD=!W7E;8!9D;;Gjzb1rv&%OZHkgt6O|G*+N4Tzht z2?u{c)WbihE0V($mw;a{6=rO(R~`oeYsVqikfI;*wdY`5+9+i=G%_uO>XUGM@52%xv#3IMYJ-xDlAz~Enhl=t2W6rO_u z3jBb#-m?)fKme3iUikn%5P(4csi ze{MkSlxMNP=CWU|g6}pkz`y{j6A-{WyZ3H60x2l4yac!hkj3c;2oMkfdAshM^&p&{ zIt`c)Ai4nv)aAV84D5Y+=}=Ptdh}B)Q2A`Nw=B5g9Sop=_ydq%m*|yyUEmVu9EiYA z3dRcn`!-NNISh{hp%Vb}FmSruiQs^R8XVIS_`M7?P!P%zTm=?DJOPMKfEK7=09rGE z7U~arpWB}cJ7|F6wUB)TU;ys|fPm^zKmaXlg#;`;M8+28VZHG#bU^-Ti+q+IzlV7bnHE_sy$Mgl2UMdbxx08pS_^x$|opt!Mr z24LeAQ}Dst@i2^|%LpJX2S*T`kB}&&KpA0JISBv~h0(*C8`;>z%3V>2mQ3UXl1RBF zwqtooWTZl(m%i+YF%Gz60ON+Jz!0b~lNMN{9~%iJLQ-Id5WJ!)SxLDRyfT1}T%IOH z8A~EgqmQ^lVeA6%I{EPsB5PzC36(d-3CI$IDujYFDTh5-)^dIZP+<`bfPx7`uZNoK zqcwYJ0c3Wvo$s6_4FOoQ(7927*R-7h4#~`5im`^4tUv`@Q9%S`z@CKscdQLZqhpkF=kL9<47riC@DGeOEbrfFc9Xl!drF$h`9 z2{4ix5UU618Ca(d7PF(IX%~s;&tYP;hpHr;7AyBvzeX=}vwdE0I|&R0EKs!^N+TmR zdD*o#6^#MjIbTanFi7QX)VHz&sYkuJT|#nFt;i*(>!x8v+6L9UP;xC)Xh5aEIIK~8n@d4+4U1VG^3ReG;V1uiw?Gc{ zaFXAXWoYpl-vyVqciJ;*2Y*S(Y5cNr3x;U#FlT`ek1oU?AOH|Ys=s6IO(8kl-0Hq) z&KA}MZ^Y}QDk*R{?)BG%Z#D7=Z79C?31FBq<{~47w?x}QNrpG%G2F~*H_FPk%OBt} bm>KxxF>^r7X2t-T)6C{JyZOyXivR#Sa!Af4 literal 0 HcmV?d00001 diff --git a/src/Dev/Install/config-form.html b/src/Dev/Install/config-form.html index 2b920a752..df1dc48ea 100644 --- a/src/Dev/Install/config-form.html +++ b/src/Dev/Install/config-form.html @@ -6,7 +6,7 @@ SilverStripe CMS / Framework Installation - + @@ -29,13 +29,13 @@
      - +

      You aren't currently able to install the software. Please see below for details.
      If you are having problems meeting the requirements, see the server requirements.

      - -

      Your php.ini file is located at

      + +

      Your php.ini file is located at

      @@ -252,7 +252,7 @@

    Confirm Install Step 5 of 5

    - +

    You aren't currently able to install the software. Please see above for details.
    If you are having problems meeting the requirements, see the server requirements page. diff --git a/src/Dev/Install/install.php b/src/Dev/Install/install.php index 6f1ab4b8d..69304eab5 100644 --- a/src/Dev/Install/install.php +++ b/src/Dev/Install/install.php @@ -24,4 +24,4 @@ if (version_compare(phpversion(), '5.5.0', '<')) { die(); } -include(__DIR__ . '/install.php5'); +include(__DIR__ . '/install5.php'); diff --git a/src/Dev/Install/install.php5 b/src/Dev/Install/install.php5 deleted file mode 100755 index 3baad35ed..000000000 --- a/src/Dev/Install/install.php5 +++ /dev/null @@ -1,1762 +0,0 @@ - '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 & - // which will results in links like ?param=value&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 "

    The following problems are preventing me from installing SilverStripe CMS:

    \n\n"; - foreach($this->errors as $error) { - echo "
  • " . htmlentities(implode(", ", $error), ENT_COMPAT, 'UTF-8') . "
  • \n"; - } - } - } - - public function showTable($section = null) { - if($section) { - $tests = $this->tests[$section]; - $id = strtolower(str_replace(' ', '_', $section)); - echo ""; - foreach($tests as $test => $result) { - echo ""; - } - echo "
    $test" - . nl2br(htmlentities($result[1], ENT_COMPAT, 'UTF-8')) . "
    "; - - } 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 .= "$test" - . nl2br(htmlentities($result[1], ENT_COMPAT, 'UTF-8')) . ""; - } - $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 "
    $section Show All Requirements $text
    "; - echo ""; - echo $output; - echo "
    "; - } - } - } - - 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) { - ?> - - - - Installing SilverStripe... - - - - -
    -
    -
    - - -

    SilverStripe

    -
    -
    -
    - - -
    - -
    -
    -

    Installing SilverStripe...

    - -

    I am now running through the installation steps (this should take about 30 seconds)

    - -

    If you receive a fatal error, refresh this page to continue the installation

    -