-
Notifications
You must be signed in to change notification settings - Fork 824
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
BUG Replace phpdotenv with thread-safe replacement #7484
BUG Replace phpdotenv with thread-safe replacement #7484
Conversation
ed6976f
to
301a09b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
I'm not really behind this fix as the whole point in using an external library is so that we aren't maintaining this kind of code ourselves. from my understanding Have we investigated using |
The external library is not intended for production, so using it in the first place only satisfies the environment when in development albeit is not in IMHO the dotenv dependency should be dropped entirely for methodology like this. Even if I am being biased here because this resolves my issue of wanting to use the .env file like laravel/symfony allow flawlessly |
Our external library is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A minor request, it's not blocking for this to be merged
src/Core/Environment.php
Outdated
$variables = parse_ini_string($contents) ?: []; | ||
foreach ($variables as $name => $value) { | ||
// Don't overload vars | ||
if (static::getenv($name) === false) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could we have overloading vars as a second param for this method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that's a good idea thank you. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed, few stylistic comments. Hand-rolling our own dotenv implementation surprised me a bit – we're trying to reduce wheel reinvention. Sure it's not lengthy code, but it's the kind of regex soup that can end up having bugs.
src/Core/Environment.php
Outdated
* | ||
* @param string $string Setting to assign in KEY=VALUE or KEY="VALUE" syntax | ||
*/ | ||
public static function putenv($string) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this to be our API rather than setEnv($k, $v)
? Who would you recommend calls putenv?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having looked at the other code that calls it, it seems making the setEnv API public and removing putenv would be more appropriate to developers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've ensure that putenv() promises to be a drop-in replacement for the core putenv
. Mirroring the API means that users don't have to do any string-manipulation in their usercode. As a developer I love the peace of mind of an api promising to work the same way; Less for me to think about and re-test. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see much value in that. People will still have to change their code, and if they're going to change their code we may as well give them the API they're more likely to want, which is setEnv.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you look at all our putenv calls we're doing string concatenation in them now, so providing setEnv()
isn't going to force people to do string manipulation.
If we had 5 years of code built around putenv there might be a stronger argument for it, but we don't.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, fair enough.
Shall I make putEnv
and setEnv
both public?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like a reasonable compromise.
src/Core/Environment.php
Outdated
* @param string $name | ||
* @return mixed Value of the environment variable, or null if not set | ||
*/ | ||
public static function getenv($name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make this camel case, so getEnv()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This api mirrors the old global getenv
, so I've opted to maintain the semantics as close as possible, including case name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can make it getEnv
src/Core/Environment.php
Outdated
* @param string $path Path to the file | ||
* @return array List of values parsed as an associative array | ||
*/ | ||
public static function putenvFromFile($path) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why have we reimplemented dotenv?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty much!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't it be better to retain a dotenv dependency (either vlucas or symfony) and make reference to its loader within Environment
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll have a look into symfony.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Symfony seems to do exactly the same thing; symfony dotenv simply uses core-php getenv
. Given our implementation is small and works for us, I suggest to leave it as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
src/includes/constants.php
Outdated
@@ -58,39 +57,39 @@ | |||
} | |||
|
|||
// Allow a first class env var to be set that disables .env file loading | |||
if (!getenv('SS_IGNORE_DOT_ENV')) { | |||
if (!Environment::getenv('SS_IGNORE_DOT_ENV')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably still be getenv, otherwise it's a bit of a circular dependency, and this var can't be set by .env
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This behaviour is required in case user-specific bootstrap files set environment vars.
Note that Environment::getenv
doesn't actually require .env
files to be loaded; It also acts as an accessor to the underlying system environment. This just maintains consistency between all getenv accessors.
The risk of using old getenv
is that if a user does custom loading of env vars in user-code or custom index.php
, then that call is going to be vulnerable to the same race condition we are trying to address.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough
301a09b
to
8e6bb43
Compare
Ok, all feedback has been implemented and also on all other repos. |
@@ -32,13 +32,18 @@ You can set "real" environment variables using Apache. Please | |||
|
|||
## How to access the environment variables | |||
|
|||
Accessing the environment varaibles is easy and can be done using the `getenv` method or in the `$_ENV` and `$_SERVER` | |||
super-globals: | |||
Accessing the environment varaibles should be done via the `Environment::getenv()` method |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
referencing old API, maybe double check all docs :)
docs/en/04_Changelogs/4.0.0.md
Outdated
@@ -486,6 +486,8 @@ SS_BASE_URL="//localhost/" | |||
The global values `$database` and `$databaseConfig` have been deprecated, as has `ConfigureFromEnv.php` | |||
which is no longer necessary. | |||
|
|||
To access environment variables you can use the `SilverStripe\Core\Environment::getenv()` method. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same in changelogs
@@ -58,39 +57,39 @@ | |||
} | |||
|
|||
// Allow a first class env var to be set that disables .env file loading | |||
if (!getenv('SS_IGNORE_DOT_ENV')) { | |||
if (!Environment::getEnv('SS_IGNORE_DOT_ENV')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just revisiting what @sminnee said - this should only ever be set by a global bonefido environment variable that won't change from execution to execution. As the comment says "Allow a first class env var to be set that disables .env file loading" but this allows second-class / psuedo environment variables to manipulate/unset this value which is the opposite of what's intended here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but this allows second-class / psuedo environment variables to manipulate/unset this value which is the opposite of what's intended here.
getenv
allows exactly the same if you use putenv
, so what's the introduced risk that didn't exist before?
src/Core/Environment.php
Outdated
// If PHP is running as an Apache module and an existing | ||
// Apache environment variable exists, overwrite it | ||
if (function_exists('apache_getenv') && function_exists('apache_setenv') && apache_getenv($name)) { | ||
apache_setenv($name, $value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand the preference for handrolling our own dotenv work when this is practically identical to using Dotenv\Loader::setEnvironmentVariable
and Dotenv\Loader::getEnvironmentVariable
and means we don't have to maintain this code :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what the setEnvironmentVariable
is in the library: https://github.com/vlucas/phpdotenv/blob/828d19e597052ddeee65890bb2b1a0912d79fea8/src/Loader.php#L353-L375
It's super close to what we have just we also have an extra layer of the local static $env
too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just we also have an extra layer of the local static $env too.
That is the critical point of this PR. That's not simply a cache, that's the key feature that solves the process isolation issue.
@@ -0,0 +1,13 @@ | |||
# Test file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are no tests for nested variables, eg:
TEST="value"
TEST_AGAIN="$TEST"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't write any code to support it either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are nested variables a thing? :o
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please don't let this be a thing... at least not here :P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My thoughts exactly. We're not implementing a POSIX shell.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a feature of the dotenv lib so if we're replacing it like-for-like it needs to be here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've restored this. ;)
I don't see how lifting the code from the library and putting it in our codebase makes this any more suitable for production. If we feel that this PR solves the problem, then using the built in methods (instead of php's
Laravel uses the same library for their |
We aren't lifting it, we have made a completely unique implementation. We store the vars in a static $env to protected it from cross-process pollution (which apparently $_ENV and both $_SERVER were subject to in testing).
I'm not happy, as even laravel / symfony suffer from the risks documented at https://gist.github.com/progmars/1e545d96dd48676a2aa7 |
Maybe someone who has an environment which can reproduce this issue (e.g. @zanderwar ) could take over development of this fix and see if they can minimise the fix to one that still solves the issue, but maybe without removal of the existing backend dotenv library? |
Maybe we should request feedback from one of the laravel devs; Maybe they can provide some insight that I haven't seen. |
Well, I've tracked down the appropriate laravel discussion. laravel/framework#8191 (comment)
The way this is addressed in laravel is that the result of the combined config is cached, and thus there is no race condition when setting / loading In the case of SilverStripe, what we could do is use We already use the back-tick operator to interpolate env / constants into config. What we could do is actually cache these within the config manifest (rather than lazy-evaluating on each request). For example, this is how the default cache factory sets the temp path (cache.yml) ---
Name: corecache
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Core\Cache\CacheFactory:
class: 'SilverStripe\Core\Cache\DefaultCacheFactory'
constructor:
args:
directory: '`TEMP_PATH`' This could be solved in a number of ways: Option 1
Option 2Alternatively, we could not interpolate these variables, but instead create a separate config transformer which populates config directly from the environment into a holder class (e.g. SilverStripe\Core\Environment). Thus |
@dhensby what do you think about these other options? |
We're close to an RC1 now is not the time to be radically re-thinking APIs. Are we all 👍 on I love the idea of relying on the config's backtick system as much as possible, but that's a styleguide question, we'd still need the public API. I think that making more use of the backtick system is something that we can look at incrementing towards in 4.x. I, like Dan, questioned why we rewrote the dotenv package instead of just including it as a dependency, but I don't think that an argument over that should have anything to do with the public API we expose. And, because of that, we could change this in 4.1 if we want. I don't think that we should expose Re-reading, I can see that Damian has found specific issues with the 3rd party implementations. I would say that we bless our current public API, go with Damian's implementation for now, and if we want, we can look at:
|
@sminnee pro-tip; If we switch to Environment::getEnv() now, we can still switch the back-end cache at some later point (e.g. 4.1). |
$_ENV['SS_DATABASE_CLASS']; | ||
$_SERVER['SS_DATABASE_CLASS']; | ||
use SilverStripe\Core\Environment; | ||
Environment::getenv('SS_DATABASE_CLASS'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getEnv
|
||
```php | ||
use SilverStripe\Core\Environment; | ||
Environment::putenv('API_KEY="AABBCCDDEEFF012345"'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setEnv('API_KEY', 'AABBCCDDEEFF012345')
} | ||
use SilverStripe\Core\Environment; | ||
$env = BASE_PATH . '/mysite/.env'; | ||
Environment::putenvFromFile($env); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
putEnvFromFile
I'll fix up the docs now. :) |
@@ -100,8 +100,7 @@ public function testModuleRules() | |||
|
|||
public function testEnvVarSetRules() | |||
{ | |||
$loader = new Loader(null); | |||
$loader->setEnvironmentVariable('ENVVARSET_FOO', 1); | |||
Environment::putEnv('ENVVARSET_FOO=1'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setEnv('ENVVARSET_FOO', 1)
I'll defer to your better experience in this case. :) |
So in summary:
Right? Perhaps we ditch the |
This is my view. We can use our own API which wraps the It also seems that this fix doesn't actually solve the problem anyway as we still rely on |
I may update so that Environment::setEnv() simply sets the $env static, and we don't modify the environment at all. |
008bb3a
to
883dda6
Compare
One more iteration, with these changes:
|
4dcfc5b
to
be85c99
Compare
Final version looks good to me; let's merge this, assuming that it works for @zanderwar and @NightJar |
|
@NightJar does the current solution fix the bugs that you experienced? Also: in your comment above you didn't say whether or not that edge-case worked. |
I think supporting escaped quotes can be a later release fix, if necessary. |
Shall we escew dotenv / parse_ini_string() for say, one of the below: Unlike the prior library, these support the ability for us to parse and extract, but not populate I'm not really interested in complicating my regexp any more. |
be85c99
to
a5d8fcb
Compare
Switched out env parser for m1/env. @dhensby see, no internal-env-parser implementation anymore. :) And feature parity. |
Attempting to test. |
This patch; good news. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Working for me 100%
Merged, and merged 4.0 -> 4 as well on cms and framework |
Yay thank you. :) |
Yay |
The current version of phpdotenv is robust and thread-safe. The following example will work in a multithreaded environment. This avoids using the adapter that would have called <?php
use Dotenv\Environment\Adapter\EnvConstAdapter;
use Dotenv\Environment\Adapter\ServerConstAdapter;
use Dotenv\Environment\DotenvFactory;
use Dotenv\Dotenv;
$factory = new DotenvFactory([new EnvConstAdapter(), new ServerConstAdapter()]);
Dotenv::create($path, null, $factory)->load(); |
That's excellent, thank you @GrahamCampbell. I think the approach we've gone with is the best for us, however, but we appreciate your getting back to us. |
Fixes #7478
Not strictly breaking, but any code which uses
getenv
php method will remain vulnerable until upgrading to the newEnvironment::getenv
method.In order to be as unobtrusive as possible the
getenv
andputenv
methods onEnvironment
class are designed as drop-in replacements for the global php methods.Thanks to @NightJar for beginning this solution (see NightJar@e614ede).
Other PRs for various modules incoming to replace
getenv
usages.Other prs to merge after this:
References to this issue (notably laravel based)