In #2620304: htaccess functions should be a service we attempted to move \Drupal\Component\PhpStorage\FileStorage::htaccessLines() out into \Drupal\Core\File\HtaccessWriter::htaccessLines()

However FileStorage is a component, and therefore can't call code outside of the component. It also calls self::htaccessLines() in FileStorage::ensureDirectory().

We should try to untangle this and move htaccessLines() to a separate component.

From @alexpott

@kim.pepper I think we should continue with calling \Drupal\Component\PhpStorage\FileStorage::htaccessLines() then and file a follow-up to try to resolve this. The other answer is that we should try to make the HtAccessWriter a component but that is going to be nigh on impossible. For me this points to the fact that PhpStorage doesn't really feel like a component either.

Maybe we can find some other similar stuff and make a security component that has this ¯\_(ツ)_/¯

CommentFileSizeAuthor
#37 3013210-35-to-37.interdiff.txt914 bytesgreg.1.anderson
#37 3013210-37.patch40.74 KBgreg.1.anderson
#35 3013210-35.patch40.74 KBgreg.1.anderson
#35 3013210-33-to-35-interdiff.txt959 bytesgreg.1.anderson
#33 3013210-33.patch40.78 KBgreg.1.anderson
#33 3013210-32-to-33-interdiff.txt1.5 KBgreg.1.anderson
#32 3013210-32.patch40.78 KBgreg.1.anderson
#32 3013210-28-to-32-interdiff.txt2.17 KBgreg.1.anderson
#28 3013210-28.patch40.22 KBgreg.1.anderson
#27 3013210-27.patch3.95 KBgreg.1.anderson
#27 3013210-23-to-27-interdiff.txt3.95 KBgreg.1.anderson
#23 3013210-23-interdiff.txt1.02 KBkim.pepper
#23 3013210-23.patch39.51 KBkim.pepper
#19 3013210-19-interdiff.txt816 byteskim.pepper
#19 3013210-19.patch38.5 KBkim.pepper
#15 3013210-15-interdiff.txt7.28 KBkim.pepper
#15 3013210-15.patch38.53 KBkim.pepper
#14 3013210-14-interdiff.txt2.98 KBkim.pepper
#14 3013210-14.patch35.91 KBkim.pepper
#12 3013210-12-interdiff.txt6.01 KBkim.pepper
#12 3013210-12.patch35.66 KBkim.pepper
#10 3013210-10-interdiff.txt2.69 KBkim.pepper
#10 3013210-10.patch34.75 KBkim.pepper
#8 3013210-8-interdiff.txt1.11 KBkim.pepper
#8 3013210-8.patch34.46 KBkim.pepper
#5 3013210-5.patch33.35 KBkim.pepper
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

kim.pepper created an issue. See original summary.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Mile23’s picture

Related: #3057094: Add Composer vendor/ hardening plugin to core

In that issue, we're using Drupal\Component\PhpStorage\FileStorage::htaccessLines() in the context of a Composer plugin. This replaces the same usage in the current Composer script: https://git.drupalcode.org/project/drupal/blob/8.8.x/core/lib/Drupal/Cor...

This is great because the vendor hardening plugin can just require drupal/core-php-storage and automatically get the same .htaccess file it had in the script. It's a DRY win.

So basically the issue title seems wrong to me: The dependencies are not currently tangled and don't need untangling. :-)

+1 on moving the htaccess writer to be a component. That way the Composer plugin can use it with minimal dependency hell, and core's file system can wrap it up in services context all it wants.

Alternately, the htaccess template method could be in, for instance, drupal/core-utility or somewhere else that's more granular.

Mile23’s picture

This issue is now a blocker for #3057094-101: Add Composer vendor/ hardening plugin to core and thus the Composer initiative.

+1 in favor of making an htaccess/web.config generating component that can be used in a Composer plugin context.

This might require some rearchitecting in parent issue #2620304: htaccess functions should be a service

kim.pepper’s picture

Status: Active » Needs review
Related issues: +#2620304: htaccess functions should be a service
FileSize
33.35 KB

I've created a FileSecurity component and moved FileStorage::htaccessLines() there and deprecated it.

I replaced the usages in core/lib/Drupal/Core/Composer/Composer.php

Let's see how this goes!

kim.pepper’s picture

https://www.drupal.org/node/2418133 will need updating if this goes in.

Status: Needs review » Needs work

The last submitted patch, 5: 3013210-5.patch, failed testing. View results

kim.pepper’s picture

Status: Needs work » Needs review
FileSize
34.46 KB
1.11 KB

Fix test fails.

Mile23’s picture

Title: Untangle PhpStorage component use of htaccessLines() » Add drupal/core-filesecurity component for writing htaccess files
Status: Needs review » Needs work

Nice, thanks.

+++ b/core/lib/Drupal/Component/FileSecurity/FileSecurity.php
@@ -0,0 +1,110 @@
+  /**
+   * Ensures an .htaccess file exists for the given directory.
+   *
+   * @param string $directory
+   *   The directory.
+   * @param bool $private
+   *   (optional) Set to FALSE to ensure an .htaccess file for an open and
+   *   public directory. Default is TRUE.
+   */
+  public static function ensureHtaccess($directory, $private = TRUE) {
...
+  /**
+   * Ensures a web.config file exists for the given directory.
+   *
+   * @param string $directory
+   *   The directory.
+   */
+  public static function ensureWebConfig($directory) {

We should return a value so that callers can know whether it happened or not.

kim.pepper’s picture

Status: Needs work » Needs review
FileSize
34.75 KB
2.69 KB

#9: Now returns a booleean.

Mile23’s picture

Thanks.

+++ b/core/tests/Drupal/Tests/Component/FileSecurity/FileSecurityTest.php
@@ -0,0 +1,51 @@
+class FileSecurityTest extends TestCase {

Right, so now that we have a return value that can be FALSE, we need a test case where that happens.

Also needs the same tests for ensureWebConfig().

I'd do it but then I can't review. :-)

kim.pepper’s picture

Thanks @Mile23. I've added some checks if the directory exists and is writeable, and refactored out the common code. Added tests for the negative return values as suggested.

I've also renamed ensureHtaccess -> writeHtaccess as 'ensure' means something different in the existing file_ensure_htaccess(). Basically it knows about the standard directories that need to be written to. I didn't want to confuse the concepts.

kim.pepper’s picture

FWIW the existing file_save_htaccess() has a few other features which make it hard to reuse the code in here, such as:

  1. Supports stream wrappers for the directory. We are just using a string for the directory
  2. Optionally force overwriting the file. We just return if the file already exists.
  3. Setting the filemode. We don't do anything.

Not sure if you think we should/must support some of those features in this component?

kim.pepper’s picture

Added file mode, as I think that's a hard requirement. It also allows us to replace duplicate code in \Drupal\Component\PhpStorage\FileStorage::ensureDirectory().

kim.pepper’s picture

I also think the force overwrite feature is a hard requirement so adding it here.

This means we can replace a good chunk of what's currently in file_save_htaccess() with FileSecurity::writeHtaccess()! 🎉

    return \Drupal::service('file_system')->chmod($htaccess_path, 0444);

This is just doing @chmod($htaccess_path, 0444) each time, so I think we are safe to replace it with the components implementation.

larowlan’s picture

+++ b/core/lib/Drupal/Component/FileSecurity/FileSecurity.php
@@ -0,0 +1,143 @@
+    if (file_exists($directory) && is_writable($directory)) {
+      if (file_put_contents($file_path, $contents . "\n")) {

could this be one if instead of two?

The last submitted patch, 14: 3013210-14.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 15: 3013210-15.patch, failed testing. View results

kim.pepper’s picture

Status: Needs work » Needs review
FileSize
38.5 KB
816 bytes

Fixes tests and addresses #16: moved to a single line.

Mile23’s picture

Status: Needs review » Reviewed & tested by the community

LGTM assuming the tests pass.

larowlan’s picture

larowlan’s picture

Status: Reviewed & tested by the community » Needs review
+++ b/core/lib/Drupal/Component/PhpStorage/FileStorage.php
@@ -64,46 +66,15 @@ public function save($name, $code) {
+    @trigger_error("htaccessLines() is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Component\FileSecurity\FileSecurity::htaccessLines() instead. See https://www.drupal.org/node/3075098", E_USER_DEPRECATED);

shouldn't we be adding a deprecation test for this?

kim.pepper’s picture

Adds a deprecation test for FileStorage::htaccessLines()

kim.pepper’s picture

Status: Needs review » Reviewed & tested by the community

Feedback addressed so back to RTBC!

joachim’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Component/FileSecurity/FileSecurity.php
    @@ -0,0 +1,141 @@
    +   * @param bool $private
    +   *   (optional) Set to FALSE to ensure an .htaccess file for an open and
    +   *   public directory. Default is TRUE.
    

    I've read this three times and I don't understand the connection between the name of the parameter and the description.

    Does it mean 'only act for private directories'?

    Oh, wait reading the code further down, it's to do with what is written in the file itself.

    The use of 'ensure' is what makes it unclear here, because it makes it sound like for some values of the parameter, it's not written. 'write' would be better.

    While we're at it, I don't think a boolean value here is very clear. What about having class constants self::PUBLIC_ACCESS and self::PRIVATE_ACCESS?

  2. +++ b/core/lib/Drupal/Component/FileSecurity/FileSecurity.php
    @@ -0,0 +1,141 @@
    +    if ($private) {
    

    This should be an if/else rather than a clobber.

  3. +++ b/core/lib/Drupal/Component/FileSecurity/FileSecurity.php
    @@ -0,0 +1,141 @@
    +   * @see file_create_htaccess()
    

    This doesn't exist in 8.x as far as I can tell. It looks like it maybe hasn't existed since D6! Let's not bring obsolete documentation into the new class. Besides, you don't need a @see to the method that writes the files, since now it's in the same class.

  4. +++ b/core/lib/Drupal/Component/FileSecurity/FileSecurity.php
    @@ -0,0 +1,141 @@
    +    // Don't overwrite if the file exists.
    

    This comment is not true, since we can force. Should append 'unless told to force.'

joachim’s picture

And a couple of things for follow-ups:

+++ b/core/lib/Drupal/Component/FileSecurity/FileSecurity.php
@@ -0,0 +1,141 @@
+    if (file_exists($directory) && is_writable($directory) && file_put_contents($file_path, $contents)) {
+      return @chmod($file_path, 0444);

This looks weird. If the file_put_contents() succeeds, but the chmod() fails, we return FALSE, even though the file has been written.

Not one to be fixed here, since this issue is about moving the existing code and keeping the same behaviour.

Also, it seems to me like most of the methods on this new class could be private. You're only going to call writeHtAccess() and writeWebConfig(), right? We need the others public for now for the deprecated methods to call, but we could deprecate them as public.

greg.1.anderson’s picture

Status: Needs work » Needs review
FileSize
3.95 KB
3.95 KB

I added the new drupal/core-filesecurity component to the replace section of the drupal/core composer.json file, like the other Components.

I've read this three times and I don't understand the connection between the name of the parameter and the description.

I updated the variable name and code comment for increased clarity.

What about having class constants self::PUBLIC_ACCESS and self::PRIVATE_ACCESS?

I didn't put this in, because $deny_public_access = self::DENY_PUBLIC_ACCESS looked redundant to me. I am not opposed to doing this if it is the consensus.

This should be an if/else rather than a clobber.

This is not a clobber; $lines appears in the second set of lines (prepend). If this were a clobber, I would prefer early-exit (return <<<EOF) over an else. We could move the conditional up and make $lines always append to improve clarity a bit. I didn't do this because it's more lines, and I think the existing code is clear enough, but I wouldn't be opposed to do it that way if it was the consensus.

@see file_create_htaccess()

Good catch. This method is now called file_save_htaccess, which still calls htaccessLines in order to present the error message.

This comment is not true, since we can force. Should append 'unless told to force.'

Updated as suggested.

This looks weird. If the file_put_contents() succeeds, but the chmod() fails, we return FALSE, even though the file has been written.

Being able to write the file but not chmod it is an edge case. Perhaps this could be covered in a follow-up issue; however, if it is fixed, then the resolution should be to add a specific error message if the chmod fails. In the current behavior, if the chmod fails after the file is written, then you get an error message; the error message is wrong, but it does at least draw your attention to the file. It would be undesirable to simply return TRUE and silently fail if the chmod did not succeed.

Also, it seems to me like most of the methods on this new class could be private.

The contents of the htaccessLines and webConfigLines are made available so that they may be included in error messages should the automatic creation of these files fail. These methods therefore should remain public.

greg.1.anderson’s picture

FileSize
40.22 KB

Sigh, accidentally posted the interdiff twice. Here's the patch that should have been #27.

joachim’s picture

> This is not a clobber; $lines appears in the second set of lines (prepend).

Ooops, my mistake. I don't think it's very clear, but I can't think of a way of making it clearer.

> The contents of the htaccessLines and webConfigLines are made available so that they may be included in error messages should the automatic creation of these files fail. These methods therefore should remain public.

I missed the code that calls them. Thanks for the explanation.

> I didn't put this in, because $deny_public_access = self::DENY_PUBLIC_ACCESS looked redundant to me. I am not opposed to doing this if it is the consensus.

I was thinking for the improved clarity in code that calls this:

FileSecurity::writeHtaccess($vendor_dir, TRUE);

// or:

FileSecurity::writeHtaccess($vendor_dir, FileSecurity::DENY_PUBLIC_ACCESS);

The only place outside of tests that uses the 2nd parameter is this in file_save_htaccess():

+  if (FileSecurity::writeHtaccess($directory, $private, $force_overwrite)) {

and it's passing on a $private that it gets as its OWN parameter. But then that too could use constants FileSecurity::DENY_PUBLIC_ACCESS / FileSecurity::ALLOW_PUBLIC_ACCESS.

andypost’s picture

mod_php5

Maybe it's time to clean it up?

Btw not sure constant is needed, I'd better split methods to writeAllow() and writeDeny() - this is very fragile way to bet on bool for this difference in behaviour

Also if this so much bound to specific apache + php versions this this class could detect used env and be more smart to detect IIS and not write apache for it.

I think it good time to review current webservers and their EOL

greg.1.anderson’s picture

Maybe it's time to clean [mod_php5] up?

Like deprecations of code, I would imagine this should be removed in Drupal 9. If that's not the case, I'd rather discuss timing as a separate issue anyway.

Btw not sure constant is needed, I'd better split methods to writeAllow() and writeDeny()

Yeah, I think that might work. Of course, the existing (deprecated) methods will need to maintain the boolean. I'll look at it a bit and see if it's an improvement.

greg.1.anderson’s picture

I split out the htaccess snippets into their own functions to make the logic easier to read in htaccessLines. I did not try to get rid of the $deny_public_access boolean, as I did not want to propagate the method bifurcation up the call stack. It didn't seem to make sense to have two writeHtaccess methods, etc., and I think with this minor refactor, the existing code is now clear enough.

I used 3v4l.org to make sure that return <<<EOT is still okay in old PHP versions.

greg.1.anderson’s picture

Here's another patch that renames core-filesecurity to core-file-security, as discussed with @mixologic in slack. Forgot that for a minute.

kim.pepper’s picture

+++ b/core/includes/file.inc
@@ -362,28 +362,22 @@ function file_save_htaccess($directory, $private = TRUE, $force_overwrite = FALS
+  $variables = [
+    '%directory' => $directory,
+    '@htaccess' => FileSecurity::htaccessLines($private),
+  ];
+  \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: <pre><code>@htaccess

", $variables);

Maybe we can inline $variables here?

greg.1.anderson’s picture

Sure. The original code had these elements broken out, perhaps because the logger line is so long. However, inlining variables is the normal style for Drupal logging, so let's go with consistency.

kim.pepper’s picture

I worked on this too much to RTBC but it LGTM.

greg.1.anderson’s picture

Fixing code style errors from the test bot.

Mile23’s picture

Status: Needs review » Reviewed & tested by the community

Diggit. LGTM.

andypost’s picture

greg.1.anderson’s picture

Issue tags: +Composer initiative

Add 'composer initiative' tag.

larowlan’s picture

larowlan’s picture

Status: Reviewed & tested by the community » Fixed

Committed 00b3ed6 and pushed to 8.8.x. Thanks!

Published change record

  • larowlan committed 00b3ed6 on 8.8.x
    Issue #3013210 by kim.pepper, greg.1.anderson, Mile23, joachim: Add...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.