Commit 8fadfdfb authored by catch's avatar catch
Browse files

Issue #1961938 by chx, alexpott, YesCT, xjm, attheshow, dawehner: Test the interactive installer.

parent 6049d251
...@@ -136,7 +136,9 @@ function _drupal_decode_exception($exception) { ...@@ -136,7 +136,9 @@ function _drupal_decode_exception($exception) {
* An error message. * An error message.
*/ */
function _drupal_render_exception_safe($exception) { function _drupal_render_exception_safe($exception) {
return check_plain(strtr('%type: !message in %function (line %line of %file).', _drupal_decode_exception($exception))); $decode = _drupal_decode_exception($exception);
unset($decode['backtrace']);
return check_plain(strtr('%type: !message in %function (line %line of %file).', $decode));
} }
/** /**
......
...@@ -266,13 +266,6 @@ function install_begin_request(&$install_state) { ...@@ -266,13 +266,6 @@ function install_begin_request(&$install_state) {
if (!$install_state['interactive']) { if (!$install_state['interactive']) {
drupal_override_server_variables($install_state['server']); drupal_override_server_variables($install_state['server']);
} }
// The user agent header is used to pass a database prefix in the request when
// running tests. However, for security reasons, it is imperative that no
// installation be permitted using such a prefix.
elseif (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], "simpletest") !== FALSE) {
header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
exit;
}
// Initialize conf_path(). // Initialize conf_path().
// This primes the site path to be used during installation. By not requiring // This primes the site path to be used during installation. By not requiring
...@@ -282,6 +275,17 @@ function install_begin_request(&$install_state) { ...@@ -282,6 +275,17 @@ function install_begin_request(&$install_state) {
drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION); drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION);
// If the hash salt leaks, it becomes possible to forge a valid testing user
// agent, install a new copy of Drupal, and take over the original site. To
// avoid this yet allow for automated testing of the installer, make sure
// there is also a special test-specific settings.php overriding conf_path().
// _drupal_load_test_overrides() sets the simpletest_conf_path in-memory
// setting in this case.
if ($install_state['interactive'] && drupal_valid_test_ua() && !settings()->get('simpletest_conf_path')) {
header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
exit;
}
// A request object from the HTTPFoundation to tell us about the request. // A request object from the HTTPFoundation to tell us about the request.
$request = Request::createFromGlobals(); $request = Request::createFromGlobals();
...@@ -1079,6 +1083,12 @@ function install_settings_form($form, &$form_state, &$install_state) { ...@@ -1079,6 +1083,12 @@ function install_settings_form($form, &$form_state, &$install_state) {
function install_settings_form_validate($form, &$form_state) { function install_settings_form_validate($form, &$form_state) {
$driver = $form_state['values']['driver']; $driver = $form_state['values']['driver'];
$database = $form_state['values'][$driver]; $database = $form_state['values'][$driver];
// When testing the interactive installer, copy the database password and
// the test prefix.
if ($test_prefix = drupal_valid_test_ua()) {
$database['prefix'] = $test_prefix;
$database['password'] = $GLOBALS['databases']['default']['default']['password'];
}
$drivers = drupal_get_database_types(); $drivers = drupal_get_database_types();
$reflection = new \ReflectionClass($drivers[$driver]); $reflection = new \ReflectionClass($drivers[$driver]);
$install_namespace = $reflection->getNamespaceName(); $install_namespace = $reflection->getNamespaceName();
...@@ -1086,9 +1096,11 @@ function install_settings_form_validate($form, &$form_state) { ...@@ -1086,9 +1096,11 @@ function install_settings_form_validate($form, &$form_state) {
$database['namespace'] = substr($install_namespace, 0, strrpos($install_namespace, '\\')); $database['namespace'] = substr($install_namespace, 0, strrpos($install_namespace, '\\'));
$database['driver'] = $driver; $database['driver'] = $driver;
// TODO: remove when PIFR will be updated to use 'db_prefix' instead of // @todo PIFR uses 'db_prefix' instead of 'prefix'. Remove this when it gets
// 'prefix' in the database settings form. // fixed.
$database['prefix'] = $database['db_prefix']; if (!$test_prefix) {
$database['prefix'] = $database['db_prefix'];
}
unset($database['db_prefix']); unset($database['db_prefix']);
$form_state['storage']['database'] = $database; $form_state['storage']['database'] = $database;
...@@ -1148,14 +1160,35 @@ function install_settings_form_submit($form, &$form_state) { ...@@ -1148,14 +1160,35 @@ function install_settings_form_submit($form, &$form_state) {
global $install_state; global $install_state;
// Update global settings array and save. // Update global settings array and save.
$settings['databases'] = (object) array( $settings = array();
'value' => array('default' => array('default' => $form_state['storage']['database'])), $database = $form_state['storage']['database'];
'required' => TRUE, // Ideally, there is no difference between the code executed by the
); // automated test browser and an ordinary browser. However, the database
$settings['drupal_hash_salt'] = (object) array( // settings need a different format and also need to skip the password
'value' => Crypt::randomStringHashed(55), // when testing. The hash salt also needs to be skipped because the original
'required' => TRUE, // salt is used to verify the validity of the automated test browser.
); // Because of these, there's a little difference in the code following but
// it is small and self-contained.
if ($test_prefix = drupal_valid_test_ua()) {
foreach ($form_state['storage']['database'] as $k => $v) {
if ($k != 'password') {
$settings['databases']['default']['default'][$k] = (object) array(
'value' => $v,
'required' => TRUE,
);
}
}
}
else {
$settings['databases']['default']['default'] = (object) array(
'value' => $database,
'required' => TRUE,
);
$settings['drupal_hash_salt'] = (object) array(
'value' => Crypt::randomStringHashed(55),
'required' => TRUE,
);
}
drupal_rewrite_settings($settings); drupal_rewrite_settings($settings);
......
...@@ -28,7 +28,7 @@ protected function profiles($directory) { ...@@ -28,7 +28,7 @@ protected function profiles($directory) {
// For SimpleTest to be able to test modules packaged together with a // For SimpleTest to be able to test modules packaged together with a
// distribution we need to include the profile of the parent site (in // distribution we need to include the profile of the parent site (in
// which test runs are triggered). // which test runs are triggered).
if (drupal_valid_test_ua()) { if (drupal_valid_test_ua() && !drupal_installation_attempted()) {
$testing_profile = config('simpletest.settings')->get('parent_profile'); $testing_profile = config('simpletest.settings')->get('parent_profile');
if ($testing_profile && $testing_profile != $profile) { if ($testing_profile && $testing_profile != $profile) {
$searchdir[] = drupal_get_path('profile', $testing_profile) . '/' . $directory; $searchdir[] = drupal_get_path('profile', $testing_profile) . '/' . $directory;
......
...@@ -64,12 +64,9 @@ function testInternalBrowser() { ...@@ -64,12 +64,9 @@ function testInternalBrowser() {
))); )));
$this->assertNoTitle('Foo'); $this->assertNoTitle('Foo');
// Make sure that we are locked out of the installer when prefixing
// using the user-agent header. This is an important security check.
global $base_url; global $base_url;
$this->drupalGet(url($base_url . '/core/install.php', array('external' => TRUE, 'absolute' => TRUE)));
$this->drupalGet($base_url . '/core/install.php', array('external' => TRUE)); $this->assertResponse(403, 'Cannot access install.php.');
$this->assertResponse(403, 'Cannot access install.php with a "simpletest" user-agent header.');
$user = $this->drupalCreateUser(); $user = $this->drupalCreateUser();
$this->drupalLogin($user); $this->drupalLogin($user);
......
...@@ -1385,9 +1385,9 @@ protected function drupalPost($path, $edit, $submit, array $options = array(), a ...@@ -1385,9 +1385,9 @@ protected function drupalPost($path, $edit, $submit, array $options = array(), a
$this->fail(t('Failed to set field @name to @value', array('@name' => $name, '@value' => $value))); $this->fail(t('Failed to set field @name to @value', array('@name' => $name, '@value' => $value)));
} }
if (!$ajax && isset($submit)) { if (!$ajax && isset($submit)) {
$this->assertTrue($submit_matches, t('Found the @submit button', array('@submit' => $submit))); $this->assertTrue($submit_matches, format_string('Found the @submit button', array('@submit' => $submit)));
} }
$this->fail(t('Found the requested form fields at @path', array('@path' => $path))); $this->fail(format_string('Found the requested form fields at @path', array('@path' => $path)));
} }
} }
......
<?php
/**
* @file
* Contains \Drupal\system\Tests\InstallerTest.
*/
namespace Drupal\system\Tests;
use Drupal\Component\Utility\NestedArray;
use Drupal\simpletest\WebTestBase;
/**
* Allows testing of the interactive installer.
*/
class InstallerTest extends WebTestBase {
public static function getInfo() {
return array(
'name' => 'Installer tests',
'description' => 'Tests the interactive installer.',
'group' => 'Installer',
);
}
protected function setUp() {
global $conf;
// When running tests through the SimpleTest UI (vs. on the command line),
// SimpleTest's batch conflicts with the installer's batch. Batch API does
// not support the concept of nested batches (in which the nested is not
// progressive), so we need to temporarily pretend there was no batch.
// Back up the currently running SimpleTest batch.
$this->originalBatch = batch_get();
// Create the database prefix for this test.
$this->prepareDatabasePrefix();
// Prepare the environment for running tests.
$this->prepareEnvironment();
if (!$this->setupEnvironment) {
return FALSE;
}
// Reset all statics and variables to perform tests in a clean environment.
$conf = array();
drupal_static_reset();
// Change the database prefix.
// All static variables need to be reset before the database prefix is
// changed, since \Drupal\Core\Utility\CacheArray implementations attempt to
// write back to persistent caches when they are destructed.
$this->changeDatabasePrefix();
if (!$this->setupDatabasePrefix) {
return FALSE;
}
$variable_groups = array(
'system.file' => array(
'path.private' => $this->private_files_directory,
'path.temporary' => $this->temp_files_directory,
),
'locale.settings' => array(
'translation.path' => $this->translation_files_directory,
),
);
foreach ($variable_groups as $config_base => $variables) {
foreach ($variables as $name => $value) {
NestedArray::setValue($GLOBALS['conf'], array_merge(array($config_base), explode('.', $name)), $value);
}
}
$settings['conf_path'] = (object) array(
'value' => $this->public_files_directory,
'required' => TRUE,
);
$settings['config_directories'] = (object) array(
'value' => array(),
'required' => TRUE,
);
$this->writeSettings($settings);
$this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=en&profile=minimal');
$this->drupalPost(NULL, array(), 'Save and continue');
// Reload config directories.
include $this->public_files_directory . '/settings.php';
$prefix = substr($this->public_files_directory, strlen(conf_path() . '/files/'));
foreach ($config_directories as $type => $data) {
$GLOBALS['config_directories'][$type]['path'] = $prefix . '/files/' . $data['path'];
}
$this->rebuildContainer();
foreach ($variable_groups as $config_base => $variables) {
$config = config($config_base);
foreach ($variables as $name => $value) {
$config->set($name, $value);
}
$config->save();
}
// Use the test mail class instead of the default mail handler class.
config('system.mail')->set('interface.default', 'Drupal\Core\Mail\VariableLog')->save();
drupal_set_time_limit($this->timeLimit);
// When running from run-tests.sh we don't get an empty current path which
// would indicate we're on the home page.
$path = current_path();
if (empty($path)) {
_current_path('run-tests');
}
$this->setup = TRUE;
}
/**
* {@inheritdoc}
*
* During setup(), drupalPost calls refreshVariables() which tries to read
* variables which are not yet there because the child Drupal is not yet
* installed.
*/
protected function refreshVariables() {
if (!empty($this->setup)) {
parent::refreshVariables();
}
}
/**
* {@inheritdoc}
*
* This override is necessary because the parent drupalGet() calls t(), which
* is not available early during installation.
*/
protected function drupalGet($path, array $options = array(), array $headers = array()) {
// We are re-using a CURL connection here. If that connection still has
// certain options set, it might change the GET into a POST. Make sure we
// clear out previous options.
$out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => $this->getAbsoluteUrl($path), CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers));
$this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up.
// Replace original page output with new output from redirected page(s).
if ($new = $this->checkForMetaRefresh()) {
$out = $new;
}
$this->verbose('GET request to: ' . $path .
'<hr />Ending URL: ' . $this->getUrl() .
'<hr />' . $out);
return $out;
}
/**
* Ensures that the user page is available after every test installation.
*/
public function testInstaller() {
$this->drupalGet('user');
$this->assertResponse(200);
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment