diff --git a/core/core.services.yml b/core/core.services.yml index e3bf98833ba..66a46c66036 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -424,9 +424,6 @@ services: logger.channel.form: parent: logger.channel_base arguments: ['form'] - logger.channel.security: - parent: logger.channel_base - arguments: ['security'] logger.log_message_parser: class: Drupal\Core\Logger\LogMessageParser @@ -1653,9 +1650,6 @@ services: - { name: twig.loader, priority: -100 } element_info: alias: plugin.manager.element_info - file.htaccess_writer: - class: Drupal\Core\File\HtaccessWriter - arguments: ['@file_system', '@logger.channel.security'] file.mime_type.guesser: class: Drupal\Core\File\MimeType\MimeTypeGuesser arguments: ['@stream_wrapper_manager'] diff --git a/core/includes/file.inc b/core/includes/file.inc index 9c67bb15189..77a743eab04 100644 --- a/core/includes/file.inc +++ b/core/includes/file.inc @@ -6,12 +6,13 @@ */ use Drupal\Component\FileSystem\FileSystem as ComponentFileSystem; +use Drupal\Component\Utility\UrlHelper; use Drupal\Component\PhpStorage\FileStorage; use Drupal\Component\Utility\Bytes; -use Drupal\Component\Utility\UrlHelper; use Drupal\Core\File\FileSystem; use Drupal\Core\Site\Settings; use Drupal\Core\StreamWrapper\PublicStream; +use Drupal\Core\StreamWrapper\PrivateStream; /** * Default mode for new directories. See drupal_chmod(). @@ -314,15 +315,30 @@ function file_prepare_directory(&$directory, $options = FILE_MODIFY_PERMISSIONS) /** * Creates a .htaccess file in each Drupal files directory if it is missing. - * - * @deprecated in Drupal 8.7.x, will be removed before Drupal 9.0.0. Use - * \Drupal\Core\File\HtaccessWriterInterface::ensure() instead. - * - * @see https://www.drupal.org/node/2940126 */ function file_ensure_htaccess() { - @trigger_error("file_ensure_htaccess() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\File\HtaccessWriter::ensure() instead. See https://www.drupal.org/node/2940126", E_USER_DEPRECATED); - \Drupal::service('file.htaccess_writer')->ensure(); + file_save_htaccess('public://', FALSE); + $private_path = PrivateStream::basePath(); + if (!empty($private_path)) { + file_save_htaccess('private://', TRUE); + } + file_save_htaccess('temporary://', 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); + } } /** @@ -336,18 +352,31 @@ function file_ensure_htaccess() { * @param bool $force_overwrite * (Optional) Set to TRUE to attempt to overwrite the existing .htaccess file * if one is already present. Defaults to FALSE. - * - * @return bool - * TRUE when file exists or created successfully, FALSE otherwise. - * - * @deprecated in Drupal 8.7.x, will be removed before Drupal 9.0.0. Use - * \Drupal\Core\File\HtaccessWriterInterface::save() instead. - * - * @see https://www.drupal.org/node/2940126 */ function file_save_htaccess($directory, $private = TRUE, $force_overwrite = FALSE) { - @trigger_error('file_save_htaccess() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\File\HtaccessWriter::save() instead. See https://www.drupal.org/node/2940126', E_USER_DEPRECATED); - return \Drupal::service('file.htaccess_writer')->save($directory, $private, $force_overwrite); + if (\Drupal::service('file_system')->uriScheme($directory)) { + $htaccess_path = file_stream_wrapper_uri_normalize($directory . '/.htaccess'); + } + else { + $directory = rtrim($directory, '/\\'); + $htaccess_path = $directory . '/.htaccess'; + } + + if (file_exists($htaccess_path) && !$force_overwrite) { + // Short circuit if the .htaccess file already exists. + return TRUE; + } + $htaccess_lines = FileStorage::htaccessLines($private); + + // Write the .htaccess file. + if (file_exists($directory) && is_writable($directory) && file_put_contents($htaccess_path, $htaccess_lines)) { + return drupal_chmod($htaccess_path, 0444); + } + else { + $variables = ['%directory' => $directory, '@htaccess' => $htaccess_lines]; + \Drupal::logger('security')->error("Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines:
@htaccess
", $variables); + return FALSE; + } } /** @@ -531,7 +560,7 @@ function file_unmanaged_prepare($source, &$destination = NULL, $replace = FILE_E return FALSE; } // Make sure the .htaccess files are present. - \Drupal::service('file.htaccess_writer')->ensure(); + file_ensure_htaccess(); return TRUE; } diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 9ee8b5b9f82..6c8a0a7e4ec 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -1120,11 +1120,14 @@ function install_base_system(&$install_state) { ->save(); } - // Ensure that all of Drupal's standard directories (e.g., the public files - // directory and config directory) have appropriate .htaccess files. These - // directories will have already been created by this point in the installer, - // since Drupal creates them during the install_verify_requirements() task. - \Drupal::service("file.htaccess_writer")->ensure(); + // Call file_ensure_htaccess() to ensure that all of Drupal's standard + // directories (e.g., the public files directory and config directory) have + // appropriate .htaccess files. These directories will have already been + // created by this point in the installer, since Drupal creates them during + // the install_verify_requirements() task. Note that we cannot call + // file_ensure_access() any earlier than this, since it relies on + // system.module in order to work. + file_ensure_htaccess(); // Prime the drupal_get_filename() static cache with the user module's // exact location. diff --git a/core/lib/Drupal/Core/Config/FileStorage.php b/core/lib/Drupal/Core/Config/FileStorage.php index fe145403357..60c5fa11d65 100644 --- a/core/lib/Drupal/Core/Config/FileStorage.php +++ b/core/lib/Drupal/Core/Config/FileStorage.php @@ -79,7 +79,7 @@ class FileStorage implements StorageInterface { $success = file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); // Only create .htaccess file in root directory. if ($dir == $this->directory) { - $success = $success && $this->createHtaccess($this->directory); + $success = $success && file_save_htaccess($this->directory, TRUE, TRUE); } if (!$success) { throw new StorageException('Failed to create config directory ' . $dir); @@ -87,20 +87,6 @@ class FileStorage implements StorageInterface { return $this; } - /** - * Creates a .htaccess file in the given directory. - * - * @param string $directory - * The directory. - * - * @return bool - * TRUE if the .htaccess file was saved or already exists, FALSE otherwise. - */ - private function createHtaccess($directory) { - // @todo Properly inject services https://www.drupal.org/node/2940135 - return \Drupal::service('file.htaccess_writer')->save($directory, TRUE, TRUE); - } - /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/File/HtaccessWriter.php b/core/lib/Drupal/Core/File/HtaccessWriter.php deleted file mode 100644 index 89731a9fef0..00000000000 --- a/core/lib/Drupal/Core/File/HtaccessWriter.php +++ /dev/null @@ -1,143 +0,0 @@ -fileSystem = $file_system; - $this->logger = $logger; - } - - /** - * {@inheritdoc} - */ - public function ensure() { - try { - foreach ($this->getHtaccessFiles() as $htaccessFile => $info) { - $this->save($info['directory'], $info['private']); - } - - // 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. - $this->save($staging, TRUE); - } - } - catch (\Exception $e) { - $this->logger->error($e->getMessage()); - } - } - - /** - * {@inheritdoc} - */ - public function save($directory, $private = TRUE, $forceOverwrite = FALSE) { - if ($this->fileSystem->uriScheme($directory)) { - $htaccessPath = $this->normalizeUri($directory . '/.htaccess'); - } - else { - $directory = rtrim($directory, '/\\'); - $htaccessPath = $directory . '/.htaccess'; - } - - if (file_exists($htaccessPath) && !$forceOverwrite) { - // Short circuit if the .htaccess file already exists. - return TRUE; - } - $htaccessLines = FileStorage::htaccessLines($private); - - // Write the .htaccess file. - if (file_exists($directory) && is_writable($directory) && file_put_contents($htaccessPath, $htaccessLines)) { - return $this->fileSystem->chmod($htaccessPath, 0444); - } - else { - $this->logger->error("Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines:
@htaccess
", [ - '%directory' => $directory, - '@htaccess' => $htaccessLines, - ]); - return FALSE; - } - } - - /** - * {@inheritdoc} - */ - public function getHtaccessFiles() { - $htaccessFiles = []; - $htaccessFiles['public://.htaccess'] = [ - 'title' => new TranslatableMarkup('Public files directory'), - 'directory' => $this->fileSystem->realpath('public://'), - 'private' => FALSE, - ]; - if (PrivateStream::basePath()) { - $htaccessFiles['private://.htaccess'] = [ - 'title' => new TranslatableMarkup('Private files directory'), - 'directory' => $this->fileSystem->realpath('private://'), - 'private' => TRUE, - ]; - } - $htaccessFiles['temporary://.htaccess'] = [ - 'title' => new TranslatableMarkup('Temporary files directory'), - 'directory' => $this->fileSystem->realpath('temporary://'), - 'private' => TRUE, - ]; - return $htaccessFiles; - } - - /** - * Wraps file_stream_wrapper_uri_normalize(). - * - * @param string $uri - * A URI to normalize. - * - * @return string - * The normalized URI. - * - * @see file_stream_wrapper_uri_normalize() - */ - private function normalizeUri($uri) { - return file_stream_wrapper_uri_normalize($uri); - } - -} diff --git a/core/lib/Drupal/Core/File/HtaccessWriterInterface.php b/core/lib/Drupal/Core/File/HtaccessWriterInterface.php deleted file mode 100644 index a6385cf98be..00000000000 --- a/core/lib/Drupal/Core/File/HtaccessWriterInterface.php +++ /dev/null @@ -1,44 +0,0 @@ -save(\Drupal::root() . '/' . $site_directory, FALSE)) { + elseif (!file_save_htaccess(\Drupal::root() . '/' . $site_directory, FALSE)) { $requirements['simpletest_site_directory'] = [ 'title' => t('Simpletest site directory'), 'value' => t('Not protected'), diff --git a/core/modules/system/system.install b/core/modules/system/system.install index e54a3190fa7..e54797bb45f 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -490,10 +490,23 @@ function system_requirements($phase) { if ($phase == 'runtime') { // Try to write the .htaccess files first, to prevent false alarms in case // (for example) the /tmp directory was wiped. - /** @var \Drupal\Core\File\HtaccessWriterInterface $htaccessWriter */ - $htaccessWriter = \Drupal::service("file.htaccess_writer"); - $htaccessWriter->ensure(); - foreach ($htaccessWriter->getHtaccessFiles() as $htaccess_file => $info) { + file_ensure_htaccess(); + $file_system = \Drupal::service('file_system'); + $htaccess_files['public://.htaccess'] = [ + 'title' => t('Public files directory'), + 'directory' => $file_system->realpath('public://'), + ]; + if (PrivateStream::basePath()) { + $htaccess_files['private://.htaccess'] = [ + 'title' => t('Private files directory'), + 'directory' => $file_system->realpath('private://'), + ]; + } + $htaccess_files['temporary://.htaccess'] = [ + 'title' => t('Temporary files directory'), + 'directory' => $file_system->realpath('temporary://'), + ]; + foreach ($htaccess_files as $htaccess_file => $info) { // Check for the string which was added to the recommended .htaccess file // in the latest security update. if (!file_exists($htaccess_file) || !($contents = @file_get_contents($htaccess_file)) || strpos($contents, 'Drupal_Security_Do_Not_Remove_See_SA_2013_003') === FALSE) { diff --git a/core/modules/system/system.module b/core/modules/system/system.module index b1fa5631b73..13456c762dc 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -929,15 +929,13 @@ function system_check_directory($form_element, FormStateInterface $form_state) { $logger->error('The directory %directory exists but is not writable and could not be made writable.', ['%directory' => $directory]); } elseif (is_dir($directory)) { - /** @var \Drupal\Core\File\HtaccessWriterInterface $htaccess */ - $htaccess = \Drupal::service('file.htaccess_writer'); if ($form_element['#name'] == 'file_public_path') { // Create public .htaccess file. - $htaccess->save($directory, FALSE); + file_save_htaccess($directory, FALSE); } else { // Create private .htaccess file. - $htaccess->save($directory); + file_save_htaccess($directory); } } diff --git a/core/modules/system/tests/src/Functional/File/FileSaveHtaccessLoggingTest.php b/core/modules/system/tests/src/Functional/File/FileSaveHtaccessLoggingTest.php index 96cab90f8c4..003373de718 100644 --- a/core/modules/system/tests/src/Functional/File/FileSaveHtaccessLoggingTest.php +++ b/core/modules/system/tests/src/Functional/File/FileSaveHtaccessLoggingTest.php @@ -24,16 +24,14 @@ class FileSaveHtaccessLoggingTest extends BrowserTestBase { // Verify that file_save_htaccess() returns FALSE if .htaccess cannot be // written and writes a correctly formatted message to the error log. Set // $private to TRUE so all possible .htaccess lines are written. - /** @var \Drupal\Core\File\HtaccessWriterInterface $htaccess */ - $htaccess = \Drupal::service('file.htaccess_writer'); - $this->assertFalse($htaccess->save($private, TRUE)); + $this->assertFalse(file_save_htaccess($private, TRUE)); $this->drupalLogin($this->rootUser); $this->drupalGet('admin/reports/dblog'); $this->clickLink("Security warning: Couldn't write .htaccess file. Pleaseā€¦"); $lines = FileStorage::htaccessLines(TRUE); foreach (array_filter(explode("\n", $lines)) as $line) { - $this->assertSession()->assertEscaped($line); + $this->assertEscaped($line); } } diff --git a/core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php b/core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php index 2b15d082c7e..467d9262e16 100644 --- a/core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php +++ b/core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php @@ -88,7 +88,7 @@ class DirectoryTest extends FileTestBase { // Remove .htaccess file to then test that it gets re-created. @drupal_unlink(file_default_scheme() . '://.htaccess'); $this->assertFalse(is_file(file_default_scheme() . '://.htaccess'), 'Successfully removed the .htaccess file in the files directory.', 'File'); - $this->container->get("file.htaccess_writer")->ensure(); + file_ensure_htaccess(); $this->assertTrue(is_file(file_default_scheme() . '://.htaccess'), 'Successfully re-created the .htaccess file in the files directory.', 'File'); // Verify contents of .htaccess file. $file = file_get_contents(file_default_scheme() . '://.htaccess'); diff --git a/core/tests/Drupal/KernelTests/Core/File/HtaccessDeprecationTest.php b/core/tests/Drupal/KernelTests/Core/File/HtaccessDeprecationTest.php deleted file mode 100644 index fd0422cd998..00000000000 --- a/core/tests/Drupal/KernelTests/Core/File/HtaccessDeprecationTest.php +++ /dev/null @@ -1,28 +0,0 @@ -public = Settings::get('file_public_path') . '/test/public'; - $this->htaccess = $this->container->get('file.htaccess_writer'); - } - - /** - * @covers ::save + * Tests file_save_htaccess(). */ public function testHtaccessSave() { // Prepare test directories. + $public = Settings::get('file_public_path') . '/test/public'; $private = Settings::get('file_public_path') . '/test/private'; $stream = 'public://test/stream'; + // Verify that file_save_htaccess() returns FALSE if .htaccess cannot be + // written. + // Note: We cannot test the condition of a directory lacking write + // permissions, since at least on Windows file_save_htaccess() succeeds + // even when changing directory permissions to 0000. + $this->assertFalse(file_save_htaccess($public, FALSE)); + // Create public .htaccess file. - mkdir($this->public, 0777, TRUE); - $this->assertTrue($this->htaccess->save($this->public, FALSE)); - $content = file_get_contents($this->public . '/.htaccess'); + mkdir($public, 0777, TRUE); + $this->assertTrue(file_save_htaccess($public, FALSE)); + $content = file_get_contents($public . '/.htaccess'); $this->assertTrue(strpos($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006") !== FALSE); $this->assertFalse(strpos($content, "Require all denied") !== FALSE); $this->assertFalse(strpos($content, "Deny from all") !== FALSE); $this->assertTrue(strpos($content, "Options -Indexes -ExecCGI -Includes -MultiViews") !== FALSE); $this->assertTrue(strpos($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003") !== FALSE); - $this->assertFilePermissions($this->public . '/.htaccess', 0444); + $this->assertFilePermissions($public . '/.htaccess', 0444); - $this->assertTrue($this->htaccess->save($this->public, FALSE)); + $this->assertTrue(file_save_htaccess($public, FALSE)); // Create private .htaccess file. mkdir($private, 0777, TRUE); - $this->assertTrue($this->htaccess->save($private)); + $this->assertTrue(file_save_htaccess($private)); $content = file_get_contents($private . '/.htaccess'); $this->assertTrue(strpos($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006") !== FALSE); $this->assertTrue(strpos($content, "Require all denied") !== FALSE); @@ -74,11 +53,11 @@ class HtaccessTest extends KernelTestBase { $this->assertTrue(strpos($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003") !== FALSE); $this->assertFilePermissions($private . '/.htaccess', 0444); - $this->assertTrue($this->htaccess->save($private)); + $this->assertTrue(file_save_htaccess($private)); // Create an .htaccess file using a stream URI. mkdir($stream, 0777, TRUE); - $this->assertTrue($this->htaccess->save($stream)); + $this->assertTrue(file_save_htaccess($stream)); $content = file_get_contents($stream . '/.htaccess'); $this->assertTrue(strpos($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006") !== FALSE); $this->assertTrue(strpos($content, "Require all denied") !== FALSE); @@ -87,7 +66,7 @@ class HtaccessTest extends KernelTestBase { $this->assertTrue(strpos($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003") !== FALSE); $this->assertFilePermissions($stream . '/.htaccess', 0444); - $this->assertTrue($this->htaccess->save($stream)); + $this->assertTrue(file_save_htaccess($stream)); } /** @@ -97,10 +76,13 @@ class HtaccessTest extends KernelTestBase { * The URI of the file to check. * @param int $expected * The expected file permissions; e.g., 0444. + * + * @return bool + * Whether the actual file permissions match the expected. */ protected function assertFilePermissions($uri, $expected) { $actual = fileperms($uri) & 0777; - $this->assertSame($actual, $expected, new FormattableMarkup('@uri file permissions @actual are identical to @expected.', [ + return $this->assertIdentical($actual, $expected, new FormattableMarkup('@uri file permissions @actual are identical to @expected.', [ '@uri' => $uri, '@actual' => 0 . decoct($actual), '@expected' => 0 . decoct($expected),