diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 2294d47..ec12097 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -153,6 +153,7 @@ function config_get_config_directory($type) { if (!empty($config_directories[$type])) { return $config_directories[$type]; } + // @todo https://www.drupal.org/node/2696103 Throw a more specific exception. throw new \Exception("The configuration directory type '$type' does not exist"); } diff --git a/core/includes/file.inc b/core/includes/file.inc index 3cd9439..e6490e9 100644 --- a/core/includes/file.inc +++ b/core/includes/file.inc @@ -315,7 +315,22 @@ function file_ensure_htaccess() { file_save_htaccess('private://', TRUE); } file_save_htaccess('temporary://', TRUE); - file_save_htaccess(config_get_config_directory(CONFIG_SYNC_DIRECTORY), TRUE); + + // If a staging directory exists then it should contain a .htaccess file. + // @todo https://www.drupal.org/node/2696103 catch a more specific exception + // and simplify this code. + try { + $staging = config_get_config_directory(CONFIG_SYNC_DIRECTORY); + } + catch (\Exception $e) { + $staging = FALSE; + } + if ($staging) { + // Note that we log an error here if we can't write the .htaccess file. This + // can occur if the staging directory is read-only. If it is then it is the + // user's responsibility to create the .htaccess file. + file_save_htaccess($staging, TRUE); + } } /** diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 5052611..248e340 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -361,7 +361,13 @@ function install_begin_request($class_loader, &$install_state) { \Drupal::setContainer($container); // Determine whether base system services are ready to operate. - $install_state['config_verified'] = install_ensure_config_directory(CONFIG_SYNC_DIRECTORY); + try { + $sync_directory = config_get_config_directory(CONFIG_SYNC_DIRECTORY); + $install_state['config_verified'] = file_exists($sync_directory); + } + catch (Exception $e) { + $install_state['config_verified'] = FALSE; + } $install_state['database_verified'] = install_verify_database_settings($site_path); $install_state['settings_verified'] = $install_state['config_verified'] && $install_state['database_verified']; diff --git a/core/includes/install.inc b/core/includes/install.inc index 88ca1e2a..94f64bf 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -486,16 +486,12 @@ function drupal_install_config_directories() { // Add a randomized config directory name to settings.php, unless it was // manually defined in the existing already. - $settings = []; - $config_directories_hash = Crypt::randomBytesBase64(55); if (empty($config_directories[CONFIG_SYNC_DIRECTORY])) { + $config_directories[CONFIG_SYNC_DIRECTORY] = \Drupal::service('site.path') . '/files/config_' . Crypt::randomBytesBase64(55) . '/sync'; $settings['config_directories'][CONFIG_SYNC_DIRECTORY] = (object) [ - 'value' => \Drupal::service('site.path') . '/files/config_' . $config_directories_hash . '/sync', + 'value' => $config_directories[CONFIG_SYNC_DIRECTORY], 'required' => TRUE, ]; - } - - if (!empty($settings)) { // Rewrite settings.php, which also sets the value as global variable. drupal_rewrite_settings($settings); } @@ -506,19 +502,21 @@ function drupal_install_config_directories() { // public files directory, which has already been verified to be writable // itself. But if it somehow fails anyway, the installation cannot proceed. // Bail out using a similar error message as in system_requirements(). - if (!install_ensure_config_directory(CONFIG_SYNC_DIRECTORY)) { - throw new Exception(t('The directory %directory could not be created or could not be made writable. To proceed with the installation, either create the directory and modify its permissions manually or ensure that the installer has the permissions to create it automatically. For more information, see the online handbook.', array( + if (!file_prepare_directory($config_directories[CONFIG_SYNC_DIRECTORY], FILE_CREATE_DIRECTORY) + && !file_exists($config_directories[CONFIG_SYNC_DIRECTORY])) { + throw new Exception(t('The directory %directory could not be created. To proceed with the installation, either create the directory or ensure that the installer has the permissions to create it automatically. For more information, see the online handbook.', array( '%directory' => config_get_config_directory(CONFIG_SYNC_DIRECTORY), ':handbook_url' => 'https://www.drupal.org/server-permissions', ))); } - - // Put a README.txt into the sync config directory. This is required so that - // they can later be added to git. Since this directory is auto-created, we - // have to write out the README rather than just adding it to the drupal core - // repo. - $text = 'This directory contains configuration to be imported into your Drupal site. To make this configuration active, visit admin/config/development/configuration/sync.' . ' For information about deploying configuration between servers, see https://www.drupal.org/documentation/administer/config'; - file_put_contents(config_get_config_directory(CONFIG_SYNC_DIRECTORY) . '/README.txt', $text); + elseif (is_writable($config_directories[CONFIG_SYNC_DIRECTORY])) { + // Put a README.txt into the sync config directory. This is required so that + // they can later be added to git. Since this directory is auto-created, we + // have to write out the README rather than just adding it to the drupal core + // repo. + $text = 'This directory contains configuration to be imported into your Drupal site. To make this configuration active, visit admin/config/development/configuration/sync.' . ' For information about deploying configuration between servers, see https://www.drupal.org/documentation/administer/config'; + file_put_contents(config_get_config_directory(CONFIG_SYNC_DIRECTORY) . '/README.txt', $text); + } } /** @@ -529,6 +527,9 @@ function drupal_install_config_directories() { * * @return bool * TRUE if the config directory exists and is writable. + * + * @deprecated in Drupal 8.1.x, will be removed before Drupal 9.0.x. Use + * config_get_config_directory() and file_prepare_directory() instead. */ function install_ensure_config_directory($type) { // The config directory must be defined in settings.php. diff --git a/core/modules/config/config.install b/core/modules/config/config.install new file mode 100644 index 0000000..c971ae6 --- /dev/null +++ b/core/modules/config/config.install @@ -0,0 +1,32 @@ + t('Configuration directory: %type', ['%type' => CONFIG_SYNC_DIRECTORY]), + 'description' => t('The directory %directory is not writable.', ['%directory' => $directory]), + 'severity' => REQUIREMENT_WARNING, + ]; + } + return $requirements; +} diff --git a/core/modules/config/src/Form/ConfigImportForm.php b/core/modules/config/src/Form/ConfigImportForm.php index f95b7a5..1d651d8 100644 --- a/core/modules/config/src/Form/ConfigImportForm.php +++ b/core/modules/config/src/Form/ConfigImportForm.php @@ -50,6 +50,11 @@ public function getFormId() { * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { + $directory = config_get_config_directory(CONFIG_SYNC_DIRECTORY); + $directory_is_writable = is_writable($directory); + if (!$directory_is_writable) { + drupal_set_message($this->t('The directory %directory is not writable.', ['%directory' => $directory]), 'error'); + } $form['import_tarball'] = array( '#type' => 'file', '#title' => $this->t('Configuration archive'), @@ -59,6 +64,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['submit'] = array( '#type' => 'submit', '#value' => $this->t('Upload'), + '#disabled' => !$directory_is_writable, ); return $form; } diff --git a/core/modules/config/src/Tests/ConfigImportUploadTest.php b/core/modules/config/src/Tests/ConfigImportUploadTest.php index 445ca9f..9836d77 100644 --- a/core/modules/config/src/Tests/ConfigImportUploadTest.php +++ b/core/modules/config/src/Tests/ConfigImportUploadTest.php @@ -45,6 +45,16 @@ function testImport() { $edit = array('files[import_tarball]' => drupal_realpath($text_file->uri)); $this->drupalPostForm('admin/config/development/configuration/full/import', $edit, t('Upload')); $this->assertText(t('Could not extract the contents of the tar file')); + + // Make the sync directory read-only. + $directory = config_get_config_directory(CONFIG_SYNC_DIRECTORY); + \Drupal::service('file_system')->chmod($directory, 0555); + $this->drupalGet('admin/config/development/configuration/full/import'); + $this->assertRaw(t('The directory %directory is not writable.', ['%directory' => $directory])); + // Ensure submit button for \Drupal\config\Form\ConfigImportForm is + // disabled. + $submit_is_disabled = $this->cssSelect('form.config-import-form input[type="submit"]:disabled'); + $this->assertTrue(count($submit_is_disabled) === 1, 'The submit button is disabled.'); } } diff --git a/core/modules/config/src/Tests/ConfigInstallWebTest.php b/core/modules/config/src/Tests/ConfigInstallWebTest.php index 4b18a4c..fc6cee4 100644 --- a/core/modules/config/src/Tests/ConfigInstallWebTest.php +++ b/core/modules/config/src/Tests/ConfigInstallWebTest.php @@ -26,7 +26,7 @@ class ConfigInstallWebTest extends WebTestBase { protected function setUp() { parent::setUp(); - $this->adminUser = $this->drupalCreateUser(array('administer modules', 'administer themes')); + $this->adminUser = $this->drupalCreateUser(array('administer modules', 'administer themes', 'administer site configuration')); // Ensure the global variable being asserted by this test does not exist; // a previous test executed in this request/process might have set it. @@ -188,4 +188,22 @@ public function testUnmetDependenciesInstall() { $this->assertTrue(entity_load('config_test', 'other_module_test_with_dependency'), 'The config_test.dynamic.other_module_test_with_dependency configuration has been created during install.'); } + /** + * Tests config_requirements(). + */ + public function testConfigModuleRequirements() { + $this->drupalLogin($this->adminUser); + $this->drupalPostForm('admin/modules', array('modules[Core][config][enable]' => TRUE), t('Install')); + + $directory = config_get_config_directory(CONFIG_SYNC_DIRECTORY); + file_unmanaged_delete_recursive($directory); + $this->drupalGet('/admin/reports/status'); + $this->assertRaw(t('The directory %directory does not exist.', array('%directory' => $directory))); + + file_prepare_directory($directory, FILE_CREATE_DIRECTORY); + \Drupal::service('file_system')->chmod($directory, 0555); + $this->drupalGet('/admin/reports/status'); + $this->assertRaw(t('The directory %directory is not writable.', ['%directory' => $directory])); + } + } diff --git a/core/modules/system/src/Tests/Installer/InstallerExistingConfigDirectoryTest.php b/core/modules/system/src/Tests/Installer/InstallerExistingConfigDirectoryTest.php index 38174f5..ae6b606 100644 --- a/core/modules/system/src/Tests/Installer/InstallerExistingConfigDirectoryTest.php +++ b/core/modules/system/src/Tests/Installer/InstallerExistingConfigDirectoryTest.php @@ -12,11 +12,20 @@ class InstallerExistingConfigDirectoryTest extends InstallerTestBase { /** + * The expected file perms of the folder. + * + * @var int + */ + protected $expectedFilePerms; + + /** * {@inheritdoc} */ protected function setUp() { + mkdir($this->siteDirectory . '/config_read_only', 0444); + $this->expectedFilePerms = fileperms($this->siteDirectory . '/config_read_only'); $this->settings['config_directories'][CONFIG_SYNC_DIRECTORY] = (object) array( - 'value' => $this->siteDirectory . '/config', + 'value' => $this->siteDirectory . '/config_read_only', 'required' => TRUE, ); parent::setUp(); @@ -28,6 +37,8 @@ protected function setUp() { public function testInstaller() { $this->assertUrl('user/1'); $this->assertResponse(200); + $this->assertEqual($this->expectedFilePerms, fileperms($this->siteDirectory . '/config_read_only')); + $this->assertEqual([], glob($this->siteDirectory . '/config_read_only/*'), 'The sync directory is empty after install because it is read-only.'); } } diff --git a/core/modules/system/src/Tests/System/StatusTest.php b/core/modules/system/src/Tests/System/StatusTest.php index 525b079..c170316 100644 --- a/core/modules/system/src/Tests/System/StatusTest.php +++ b/core/modules/system/src/Tests/System/StatusTest.php @@ -24,6 +24,16 @@ class StatusTest extends WebTestBase { protected function setUp() { parent::setUp(); + // Unset the sync directory in settings.php to trigger $config_directories + // error. + $settings['config_directories'] = array( + CONFIG_SYNC_DIRECTORY => (object) array( + 'value' => '', + 'required' => TRUE, + ), + ); + $this->writeSettings($settings); + $admin_user = $this->drupalCreateUser(array( 'administer site configuration', )); @@ -60,6 +70,9 @@ public function testStatusPage() { // If a module is fully installed no pending updates exists. $this->assertNoText(t('Out of date')); + // The global $config_directories is not properly formed. + $this->assertRaw(t('Your %file file must define the $config_directories variable as an array containing the names of directories in which configuration files can be found. It must contain a %sync_key key.', array('%file' => $this->siteDirectory . '/settings.php', '%sync_key' => CONFIG_SYNC_DIRECTORY))); + // Set the schema version of update_test_postupdate to a lower version, so // update_test_postupdate_update_8001() needs to be executed. drupal_set_installed_schema_version('update_test_postupdate', 8000); diff --git a/core/modules/system/system.install b/core/modules/system/system.install index cdaeba6..b8f5f5f 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -549,15 +549,22 @@ function system_requirements($phase) { // defined, the installer will create a valid config directory later, but // during runtime we must always display an error. if (!empty($GLOBALS['config_directories'])) { - foreach ($GLOBALS['config_directories'] as $type => $directory) { - $directories[] = config_get_config_directory($type); + foreach (array_keys(array_filter($GLOBALS['config_directories'])) as $type) { + $directory = config_get_config_directory($type); + if (!is_dir($directory)) { + $requirements['config directory ' . $type] = array( + 'title' => t('Configuration directory: %type', ['%type' => $type]), + 'description' => t('The directory %directory does not exist.', array('%directory' => $directory)), + 'severity' => REQUIREMENT_ERROR, + ); + } } } - elseif ($phase != 'install') { + if ($phase != 'install' && (empty($GLOBALS['config_directories']) || empty($GLOBALS['config_directories'][CONFIG_SYNC_DIRECTORY]) )) { $requirements['config directories'] = array( 'title' => t('Configuration directories'), 'value' => t('Not present'), - 'description' => t('Your %file file must define the $config_directories variable as an array containing the name of a directories in which configuration files can be written.', array('%file' => $site_path . '/settings.php')), + 'description' => t('Your %file file must define the $config_directories variable as an array containing the names of directories in which configuration files can be found. It must contain a %sync_key key.', array('%file' => $site_path . '/settings.php', '%sync_key' => CONFIG_SYNC_DIRECTORY)), 'severity' => REQUIREMENT_ERROR, ); }