ENHANCEMENT Improve upgrading experience. (#8025)

* ENHANCEMENT Improve upgrading experience.
Show errors and back button if errors encountered during install
BUG Fix hard-coded 'mysite' folder
Fixes #8024

* Patch behat tests to work with new  buttons
This commit is contained in:
Damian Mooyman 2018-05-21 18:08:34 +08:00 committed by Aaron Carlino
parent b0e46ea5c0
commit 865ebb3398
6 changed files with 366 additions and 209 deletions

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Dev\Install;
use BadMethodCallException;
use SilverStripe\Core\Environment;
/**
@ -11,6 +12,8 @@ use SilverStripe\Core\Environment;
*/
class InstallConfig
{
use InstallEnvironmentAware;
/**
* List of preferred DB classes in order
*
@ -21,6 +24,11 @@ class InstallConfig
'MySQLDatabase',
];
public function __construct($basePath = null)
{
$this->initBaseDir($basePath);
}
/**
* Get database config from the current environment
*
@ -119,7 +127,7 @@ class InstallConfig
*/
protected function getConfigPath()
{
return BASE_PATH . '/mysite/_config.php';
return $this->getBaseDir() . $this->getProjectDir() . DIRECTORY_SEPARATOR . '_config.php';
}
/**
@ -127,7 +135,7 @@ class InstallConfig
*/
protected function getEnvPath()
{
return BASE_PATH . '/.env';
return $this->getBaseDir() . '.env';
}
/**

View File

@ -0,0 +1,231 @@
<?php
namespace SilverStripe\Dev\Install;
use BadMethodCallException;
use SilverStripe\Core\Path;
/**
* For classes which are aware of install, project, and environment state.
*
* These should be basic getters / setters that infer from current state.
*/
trait InstallEnvironmentAware
{
/**
* Base path
* @var
*/
protected $baseDir;
/**
* Init base path, or guess if able
*
* @param string|null $basePath
*/
protected function initBaseDir($basePath)
{
if ($basePath) {
$this->setBaseDir($basePath);
} elseif (defined('BASE_PATH')) {
$this->setBaseDir(BASE_PATH);
} else {
throw new BadMethodCallException("No BASE_PATH defined");
}
}
/**
* @param string $base
* @return $this
*/
protected function setBaseDir($base)
{
$this->baseDir = $base;
return $this;
}
/**
* Get base path for this installation
*
* @return string
*/
public function getBaseDir()
{
return Path::normalise($this->baseDir) . DIRECTORY_SEPARATOR;
}
/**
* Get path to public directory
*
* @return string
*/
public function getPublicDir()
{
$base = $this->getBaseDir();
$public = Path::join($base, 'public') . DIRECTORY_SEPARATOR;
if (file_exists($public)) {
return $public;
}
return $base;
}
/**
* Check that a module exists
*
* @param string $dirname
* @return bool
*/
public function checkModuleExists($dirname)
{
// Mysite is base-only and doesn't need _config.php to be counted
if (in_array($dirname, ['mysite', 'app'])) {
return file_exists($this->getBaseDir() . $dirname);
}
$paths = [
"vendor/silverstripe/{$dirname}/",
"{$dirname}/",
];
foreach ($paths as $path) {
$checks = ['_config', '_config.php'];
foreach ($checks as $check) {
if (file_exists($this->getBaseDir() . $path . $check)) {
return true;
}
}
}
return false;
}
/**
* Get project dir name.
*
* @return string 'app', or 'mysite' (deprecated)
*/
protected function getProjectDir()
{
$base = $this->getBaseDir();
if (is_dir($base . 'mysite')) {
/** @deprecated 4.2..5.0 */
return 'mysite';
}
// Default
return 'app';
}
/**
* Get src dir name for project
*
* @return string
*/
protected function getProjectSrcDir()
{
$projectDir = $this->getProjectDir();
if ($projectDir === 'mysite') {
/** @deprecated 4.2..5.0 */
return $projectDir . DIRECTORY_SEPARATOR . 'code';
}
// Default
return $projectDir . DIRECTORY_SEPARATOR . 'src';
}
/**
* Check if the web server is IIS and version greater than the given version.
*
* @param int $fromVersion
* @return bool
*/
public function isIIS($fromVersion = 7)
{
$webserver = $this->findWebserver();
if (preg_match('#.*IIS/(?<version>[.\\d]+)$#', $webserver, $matches)) {
return version_compare($matches['version'], $fromVersion, '>=');
}
return false;
}
/**
* @return bool
*/
public function isApache()
{
return strpos($this->findWebserver(), 'Apache') !== false;
}
/**
* Find the webserver software running on the PHP host.
*
* @return string|false 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));
}
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;
}
}
/**
* Determines if the web server has any rewriting capability.
*
* @return bool
*/
public function hasRewritingCapability()
{
return ($this->testApacheRewriteExists() || $this->testIISRewriteModuleExists());
}
/**
* Get "nice" database name without "Database" suffix
*
* @param string $databaseClass
* @return string
*/
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);
}
}

View File

@ -21,6 +21,8 @@ use SplFileInfo;
*/
class InstallRequirements
{
use InstallEnvironmentAware;
/**
* List of errors
*
@ -48,46 +50,9 @@ class InstallRequirements
*/
protected $originalIni = [];
/**
* Base path
* @var
*/
protected $baseDir;
public function __construct($basePath = null)
{
if ($basePath) {
$this->baseDir = $basePath;
} elseif (defined('BASE_PATH')) {
$this->baseDir = BASE_PATH;
} else {
throw new BadMethodCallException("No BASE_PATH defined");
}
}
/**
* Get base path for this installation
*
* @return string
*/
public function getBaseDir()
{
return Path::normalise($this->baseDir) . DIRECTORY_SEPARATOR;
}
/**
* Get path to public directory
*
* @return string
*/
public function getPublicDir()
{
$base = $this->getBaseDir();
$public = Path::join($base, 'public') . DIRECTORY_SEPARATOR;
if (file_exists($public)) {
return $public;
}
return $base;
$this->initBaseDir($basePath);
}
/**
@ -203,50 +168,11 @@ class InstallRequirements
}
}
/**
* Check if the web server is IIS and version greater than the given version.
*
* @param int $fromVersion
* @return bool
*/
public function isIIS($fromVersion = 7)
{
$webserver = $this->findWebserver();
if (preg_match('#.*IIS/(?<version>[.\\d]+)$#', $webserver, $matches)) {
return version_compare($matches['version'], $fromVersion, '>=');
}
return false;
}
/**
* @return bool
*/
public function isApache()
{
return strpos($this->findWebserver(), 'Apache') !== false;
}
/**
* Find the webserver software running on the PHP host.
*
* @return string|false 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
*
* @param array $originalIni
* @return array
*/
public function check($originalIni)
{
@ -256,6 +182,10 @@ class InstallRequirements
$isIIS = $this->isIIS();
$webserver = $this->findWebserver();
// Get project dirs to inspect
$projectDir = $this->getProjectDir();
$projectSrcDir = $this->getProjectSrcDir();
$this->requirePHPVersion('5.5.0', '5.5.0', array(
"PHP Configuration",
"PHP5 installed",
@ -271,11 +201,11 @@ class InstallRequirements
$this->getBaseDir()
));
$this->requireModule('mysite', array(
$this->requireModule($projectDir, [
"File permissions",
"mysite/ directory exists?",
"{$projectDir}/ directory exists?",
''
));
]);
$this->requireModule('vendor/silverstripe/framework', array(
"File permissions",
"vendor/silverstripe/framework/ directory exists?",
@ -283,7 +213,15 @@ class InstallRequirements
));
$this->requireWriteable($this->getPublicDir() . 'index.php', array("File permissions", "Is the index.php file writeable?", null), true);
$this->requireWriteable(
$this->getPublicDir() . 'index.php',
[
"File permissions",
"Is the index.php file writeable?",
null,
],
true
);
$this->requireWriteable('.env', ["File permissions", "Is the .env file writeable?", null], false, false);
@ -294,29 +232,37 @@ class InstallRequirements
"SilverStripe requires Apache version 2 or greater",
$webserver
));
$this->requireWriteable($this->getPublicDir() . '.htaccess', array("File permissions", "Is the .htaccess file writeable?", null), true);
$this->requireWriteable(
$this->getPublicDir() . '.htaccess',
array("File permissions", "Is the .htaccess file writeable?", null),
true
);
} elseif ($isIIS) {
$this->requireWriteable($this->getPublicDir() . 'web.config', array("File permissions", "Is the web.config file writeable?", null), true);
$this->requireWriteable(
$this->getPublicDir() . 'web.config',
array("File permissions", "Is the web.config file writeable?", null),
true
);
}
$this->requireWriteable('mysite/_config.php', array(
$this->requireWriteable("{$projectDir}/_config.php", [
"File permissions",
"Is the mysite/_config.php file writeable?",
null
));
"Is the {$projectDir}/_config.php file writeable?",
null,
]);
$this->requireWriteable('mysite/_config/theme.yml', array(
$this->requireWriteable("{$projectDir}/_config/theme.yml", [
"File permissions",
"Is the mysite/_config/theme.yml file writeable?",
null
));
"Is the {$projectDir}/_config/theme.yml file writeable?",
null,
]);
if (!$this->checkModuleExists('cms')) {
$this->requireWriteable('mysite/code/RootURLController.php', array(
$this->requireWriteable("{$projectSrcDir}/RootURLController.php", [
"File permissions",
"Is the mysite/code/RootURLController.php file writeable?",
null
));
"Is the {$projectSrcDir}/RootURLController.php file writeable?",
null,
]);
}
// Check public folder exists
@ -332,7 +278,11 @@ class InstallRequirements
);
// Ensure root assets dir is writable
$this->requireWriteable(ASSETS_PATH, array("File permissions", "Is the assets/ directory writeable?", null), true);
$this->requireWriteable(
ASSETS_PATH,
array("File permissions", "Is the assets/ directory writeable?", null),
true
);
// Ensure all assets files are writable
$innerIterator = new RecursiveDirectoryIterator(ASSETS_PATH, RecursiveDirectoryIterator::SKIP_DOTS);
@ -824,35 +774,6 @@ class InstallRequirements
return true;
}
/**
* Check that a module exists
*
* @param string $dirname
* @return bool
*/
public function checkModuleExists($dirname)
{
// Mysite is base-only and doesn't need _config.php to be counted
if ($dirname === 'mysite') {
return file_exists($this->getBaseDir() . $dirname);
}
$paths = [
"vendor/silverstripe/{$dirname}/",
"{$dirname}/",
];
foreach ($paths as $path) {
$checks = ['_config', '_config.php'];
foreach ($checks as $check) {
if (file_exists($this->getBaseDir() . $path . $check)) {
return true;
}
}
}
return false;
}
/**
* The same as {@link requireFile()} but does additional checks
* to ensure the module directory is intact.
@ -867,7 +788,7 @@ class InstallRequirements
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') {
} elseif (!file_exists($path . '/_config.php') && !in_array($dirname, ['mysite', 'app'])) {
$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);
@ -985,29 +906,6 @@ class InstallRequirements
}
}
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);
@ -1019,15 +917,6 @@ class InstallRequirements
}
}
/**
* 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);
@ -1039,22 +928,6 @@ class InstallRequirements
}
}
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);

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Dev\Install;
use BadMethodCallException;
use Exception;
use SilverStripe\Control\Cookie;
use SilverStripe\Control\HTTPApplication;
@ -11,6 +12,7 @@ use SilverStripe\Core\Convert;
use SilverStripe\Core\CoreKernel;
use SilverStripe\Core\EnvironmentLoader;
use SilverStripe\Core\Kernel;
use SilverStripe\Core\Path;
use SilverStripe\Core\Startup\ParameterConfirmationToken;
use SilverStripe\ORM\DatabaseAdmin;
use SilverStripe\Security\DefaultAdminService;
@ -19,13 +21,37 @@ use SilverStripe\Security\Security;
/**
* This installer doesn't use any of the fancy SilverStripe stuff in case it's unsupported.
*/
class Installer extends InstallRequirements
class Installer
{
use InstallEnvironmentAware;
/**
* Errors during install
*
* @var array
*/
protected $errors = [];
/**
* value='' attribute placeholder for password fields
*/
const PASSWORD_PLACEHOLDER = '********';
public function __construct($basePath = null)
{
$this->initBaseDir($basePath);
}
/**
* Installer error
*
* @param string $message
*/
protected function error($message = null)
{
$this->errors[] = $message;
}
protected function installHeader()
{
$clientPath = PUBLIC_DIR
@ -43,8 +69,6 @@ class Installer extends InstallRequirements
<div class="install-header">
<div class="inner">
<div class="brand">
<span class="logo"></span>
<h1>SilverStripe</h1>
</div>
</div>
@ -68,9 +92,10 @@ class Installer extends InstallRequirements
{
// Render header
$this->installHeader();
$isIIS = $this->isIIS();
$isApache = $this->isApache();
$projectDir = $this->getProjectDir();
$projectSrcDir = $this->getProjectSrcDir();
flush();
@ -80,10 +105,12 @@ class Installer extends InstallRequirements
}
// Cleanup _config.php
if (file_exists('mysite/_config.php')) {
$basePath = $this->getBaseDir();
$appConfigPath = $basePath . "{$projectDir}/_config.php";
if (file_exists($appConfigPath)) {
// 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');
$fh = fopen($appConfigPath, 'wb');
fclose($fh);
}
@ -95,17 +122,18 @@ class Installer extends InstallRequirements
// Write other stuff
if (!$this->checkModuleExists('cms')) {
$this->writeToFile("mysite/code/RootURLController.php", <<<PHP
$rootURLControllerPath = $basePath . "{$projectSrcDir}/RootURLController.php";
$this->writeToFile($rootURLControllerPath, <<<PHP
<?php
use SilverStripe\\Control\\Controller;
class RootURLController extends Controller {
public function index() {
echo "<html>Your site is now set up. Start adding controllers to mysite to get started.</html>";
class RootURLController extends Controller
{
public function index()
{
echo "<html>Your site is now set up. Start adding controllers to app/src to get started.</html>";
}
}
PHP
);
@ -124,7 +152,7 @@ PHP
$request = HTTPRequestBuilder::createFromEnvironment();
// Install kernel (fix to dev)
$kernel = new CoreKernel(BASE_PATH);
$kernel = new CoreKernel(Path::normalise($basePath));
$kernel->setEnvironment(Kernel::DEV);
$app = new HTTPApplication($kernel);
@ -197,6 +225,15 @@ PHP
</noscript>
HTML;
}
} else {
// Output all errors
$this->statusMessage('Encountered ' . count($this->errors) . ' errors during install:');
echo "<ul>";
foreach ($this->errors as $error) {
$this->statusMessage($error);
}
echo "</ul>";
$this->statusMessage('Please <a href="install.php">Click here</a> to return to the installer.');
}
return $this->errors;
@ -310,8 +347,9 @@ PHP;
*/
protected function writeConfigPHP($config)
{
$configPath = $this->getProjectDir() . DIRECTORY_SEPARATOR . "_config.php";
if ($config['usingEnv']) {
$this->writeToFile("mysite/_config.php", "<?php\n ");
$this->writeToFile($configPath, "<?php\n ");
return;
}
@ -325,7 +363,7 @@ PHP;
);
}
$databaseConfigContent = implode(",\n", $lines);
$this->writeToFile("mysite/_config.php", <<<PHP
$this->writeToFile($configPath, <<<PHP
<?php
use SilverStripe\\ORM\\DB;
@ -347,6 +385,7 @@ PHP
{
// Escape user input for safe insertion into PHP file
$locale = $this->ymlString($config['locale']);
$projectDir = $this->getProjectDir();
// Set either specified, or no theme
if ($config['theme'] && $config['theme'] !== 'tutorial') {
@ -364,7 +403,7 @@ YML;
}
// Write theme.yml
$this->writeToFile("mysite/_config/theme.yml", <<<YML
$this->writeToFile("{$projectDir}/_config/theme.yml", <<<YML
---
Name: mytheme
---
@ -399,17 +438,23 @@ YML
*/
public function writeToFile($filename, $content, $absolute = false)
{
$path = $absolute
? $filename
: $this->getBaseDir() . $filename;
$this->statusMessage("Setting up $path");
// Get absolute / relative paths by either combining or removing base from path
list($absolutePath, $relativePath) = $absolute
? [
$filename,
substr($filename, strlen($this->getBaseDir()))]
: [
$this->getBaseDir() . $filename,
$filename
];
$this->statusMessage("Setting up $relativePath");
if ((@$fh = fopen($path, 'wb')) && fwrite($fh, $content) && fclose($fh)) {
if ((@$fh = fopen($absolutePath, 'wb')) && fwrite($fh, $content) && fclose($fh)) {
// Set permissions to writable
@chmod($path, 0775);
@chmod($absolutePath, 0775);
return true;
}
$this->error("Couldn't write to file $path");
$this->error("Couldn't write to file $relativePath");
return false;
}

View File

@ -58,7 +58,7 @@ Feature: Manage users
When I click the "Users" CMS tab
And I click "staffmember@example.org" in the "#Root_Users" element
And I select "ADMIN group" from "Groups"
And I press the "Save" button
And I press the "Apply changes" button
Then I should see a "Saved Member" message
When I go to "admin/security"

View File

@ -29,7 +29,7 @@ Feature: Manage Security Permissions for Groups
Then the "Access to 'Security' section" checkbox should be checked
# Save so the driver can reset without having to deal with the popup alert.
Then I press the "Save" button
Then I press the "Apply changes" button
Scenario: I can see sub-permissions being properly set and restored when using "Full administrative rights"
When I check "Access to 'Security' section"
@ -46,11 +46,11 @@ Feature: Manage Security Permissions for Groups
And the "Access to 'Security' section" field should be enabled
# Save so the driver can reset without having to deal with the popup alert.
Then I press the "Save" button
Then I press the "Apply changes" button
Scenario: I can see sub-permissions being handled properly between reloads when using "Full administrative rights"
When I check "Full administrative rights"
And I press the "Save" button
And I press the "Apply changes" button
And I click the "Permissions" CMS tab
Then the "Full administrative rights" checkbox should be checked
And the "Access to 'Security' section" checkbox should be checked
@ -60,7 +60,7 @@ Feature: Manage Security Permissions for Groups
Then the "Access to 'Security' section" checkbox should not be checked
And the "Access to 'Security' section" field should be enabled
When I press the "Save" button
When I press the "Apply changes" button
And I click the "Permissions" CMS tab
Then the "Full administrative rights" checkbox should not be checked
And the "Access to 'Security' section" checkbox should not be checked
@ -68,7 +68,7 @@ Feature: Manage Security Permissions for Groups
Scenario: I can see sub-permissions being handled properly between reloads when using "Access to all CMS sections"
When I check "Access to all CMS sections"
And I press the "Save" button
And I press the "Apply changes" button
And I click the "Permissions" CMS tab
Then the "Access to all CMS sections" checkbox should be checked
And the "Access to 'Security' section" checkbox should be checked
@ -78,7 +78,7 @@ Feature: Manage Security Permissions for Groups
Then the "Access to 'Security' section" checkbox should not be checked
And the "Access to 'Security' section" field should be enabled
When I press the "Save" button
When I press the "Apply changes" button
And I click the "Permissions" CMS tab
Then the "Access to all CMS sections" checkbox should not be checked
And the "Access to 'Security' section" checkbox should not be checked