Skip to content

Fix GRAV_CONFIG env override gate to also check $_SERVER/$_ENV - #4286

Open
wakqasahmed wants to merge 2 commits into
getgrav:developfrom
wakqasahmed:fix/env-fallback-getenv-only-4279-v2
Open

Fix GRAV_CONFIG env override gate to also check $_SERVER/$_ENV#4286
wakqasahmed wants to merge 2 commits into
getgrav:developfrom
wakqasahmed:fix/env-fallback-getenv-only-4279-v2

Conversation

@wakqasahmed

Copy link
Copy Markdown
Contributor

The GRAV_CONFIG override gate in InitializeProcessor::initializeConfig() checks getenv($prefix), but the body one line below already reads $_ENV + $_SERVER. On Apache with SetEnv or nginx with fastcgi_param, the variable lands in $_SERVER but never in the process environment getenv() reads, so the gate is false and the whole GRAV_CONFIG__* override feature does nothing — same failure mode as #4260/#4275.

Setup.php has the identical pattern for GRAV_ENVIRONMENT, GRAV_SETUP_PATH, GRAV_ENVIRONMENT_PATH and GRAV_ENVIRONMENTS_PATH.

Env.php already solves this correctly ($_SERVER[$key] ?? $_ENV[$key] ?? (getenv($key) ?: null)) and has its own test coverage proving the precedence. This PR applies the same fallback at all five call sites instead of inventing a new pattern.

Added a test that sets the override only in $_SERVER and drives the real initializeConfig() — confirmed it fails against the old code and passes against the fix. Ran the full InitializeProcessorTest and EnvTest suites after, both green (59 tests).

(Replaces #4285, which had the wrong commit author — closing that one.)

Fixes #4279

…etenv()

Some SAPIs (Apache SetEnv, nginx fastcgi_param) only populate $_SERVER,
never the process environment getenv() reads. The gate at
InitializeProcessor::initializeConfig() checked getenv() alone even
though the body one line below already reads $_ENV + $_SERVER, so the
whole GRAV_CONFIG__* override feature silently did nothing under those
setups. Setup.php had the same getenv()-only pattern for
GRAV_ENVIRONMENT, GRAV_SETUP_PATH, GRAV_ENVIRONMENT_PATH and
GRAV_ENVIRONMENTS_PATH.

Applies the same $_SERVER ?? $_ENV ?? getenv() fallback Env.php already
uses (and already has test coverage for) at all five call sites. Added
a regression test that fails on the old code and passes on the fix.

Fixes getgrav#4279
@rhukster

rhukster commented Sep 5, 2026

Copy link
Copy Markdown
Member

Thanks for picking this one up, and for taking the trouble to redo it cleanly after the author mix-up on #4285. That's appreciated.

The diagnosis is right, and the test is the best part of the PR. I checked it the way I check these: I reverted just the one line in InitializeProcessor and left your test in place, and it fails exactly as you said it would. A test that actually catches the bug it was written for is rarer than it should be, so thank you for that.

There's one thing I need changed before this can go in, and it's a subtle one that bit the last PR in this same family too.

$_SERVER['X'] ?? $_ENV['X'] ?? getenv('X') is an "is it set" check, not an "is it useful" check. So if the web server sets the variable but sets it to nothing (an nginx fastcgi_param pointing at a variable that turned out empty, an empty SetEnv, which is a normal thing to end up with in a multisite config) then the empty value satisfies the chain and the getenv fallback never runs. That means the change would break sites where the old code was working fine, which is the opposite of what we both want.

For GRAV_CONFIG it quietly switches the overrides back off. For GRAV_ENVIRONMENT the site loses its environment. For GRAV_ENVIRONMENT_PATH the environment:// stream ends up pointing at the Grav root, so per-environment config reads and writes land in the webroot. And for GRAV_SETUP_PATH it's worse than quiet: Setup.php sees a value it can't turn into a real file and calls exit(1), so every page on the site goes blank with a one-line message and nothing in the log.

The fix is to fall through on empty as well as missing, which is the same correction I made to the Uri::ip() change a couple of weeks ago. Here's the exact code.

One small private helper in Setup.php:

    /**
     * Read a bootstrap variable from wherever the SAPI put it.
     *
     * $_SERVER is the only source PHP guarantees for server-set variables, but
     * a present-but-empty entry (an unset nginx variable used in a
     * `fastcgi_param`, an empty `SetEnv`) must not shadow a working getenv(),
     * or the fix breaks the hosts where the old code worked. Mirrors Env.php
     * and Uri::ip(). (#4279)
     *
     * @param string $name
     * @return string|null
     */
    private static function envVar(string $name): ?string
    {
        foreach ([$_SERVER[$name] ?? null, $_ENV[$name] ?? null, getenv($name)] as $value) {
            if (is_string($value) && $value !== '') {
                return $value;
            }
        }

        return null;
    }

with the four call sites becoming:

            (defined('GRAV_ENVIRONMENT') ? GRAV_ENVIRONMENT : static::envVar('GRAV_ENVIRONMENT'));
        $setupFile = defined('GRAV_SETUP_PATH') ? GRAV_SETUP_PATH : static::envVar('GRAV_SETUP_PATH');
        $envPath = defined('GRAV_ENVIRONMENT_PATH') ? GRAV_ENVIRONMENT_PATH : static::envVar('GRAV_ENVIRONMENT_PATH');
            $envPath = defined('GRAV_ENVIRONMENTS_PATH') ? GRAV_ENVIRONMENTS_PATH : static::envVar('GRAV_ENVIRONMENTS_PATH');

And for the GRAV_CONFIG gate I'd rather it just test the same $_ENV + $_SERVER array the loop below it already builds. That makes the gate and its body agree by construction and sidesteps the empty case entirely:

        // Override configuration using the environment. The gate has to read
        // exactly what the loop below reads: a SAPI that populates only
        // $_SERVER (Apache SetEnv, nginx fastcgi_param) would otherwise skip
        // the whole feature with nothing logged, and an empty value must fall
        // through rather than shadow a working getenv(). (#4279)
        $prefix = 'GRAV_CONFIG';
        $vars = $_ENV + $_SERVER;
        if (!empty($vars[$prefix]) || getenv($prefix)) {

Two smaller things while you're in there. Could the test save and restore any pre-existing $_SERVER entry rather than unsetting it? We had to fix that same pattern in the Uri tests. And could you add one more case covering an empty $_SERVER value falling back to getenv? That's precisely the case that's broken right now, so it deserves to be pinned.

No changelog needed from you, I'll write that on merge.

Nice work. Get those in and I'll take it.

An env var read via \$_SERVER['X'] ?? \$_ENV['X'] ?? getenv('X') falls through
on missing, not on empty. A SAPI that sets the variable but sets it to
nothing (an unset nginx fastcgi_param, an empty Apache SetEnv) satisfies
the ?? chain with '' and the getenv() fallback never runs - which breaks
GRAV_CONFIG overrides, GRAV_ENVIRONMENT, GRAV_ENVIRONMENT_PATH,
GRAV_SETUP_PATH and GRAV_ENVIRONMENTS_PATH on exactly the hosts the
previous fix for getgrav#4279 was supposed to help, and for GRAV_SETUP_PATH it's
worse than quiet: an unresolvable value makes Setup.php exit(1).

Added a private Setup::envVar() helper that tries \$_SERVER, then \$_ENV,
then getenv(), skipping any that come back empty, and switched all five
call sites to it. The GRAV_CONFIG gate in InitializeProcessor now checks
the same \$_ENV + \$_SERVER array its body already reads instead of a
separate getenv()-first check, so the gate and the body can't disagree.

Test changes: the existing getgrav#4279 test now saves and restores whatever
was already in \$_SERVER instead of unconditionally unsetting it, and a
new test pins the empty-\$_SERVER-falls-back-to-getenv case with
putenv().
@wakqasahmed

Copy link
Copy Markdown
Contributor Author

Good catch, and thanks for the exact pointer - pushed a commit with your envVar() helper on all five call sites, the $_ENV + $_SERVER gate in InitializeProcessor, and the two test fixes: the existing test now stashes/restores whatever was already in $_SERVER instead of unsetting it, and I added testConfigOverrideFallsBackToGetenvWhenServerValueIsEmpty (uses putenv() to pin the empty-$_SERVER-falls-back-to-getenv() case). Full suite's green locally (51/51).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

getenv()-only reads in the bootstrap: the GRAV_CONFIG gate and the GRAV_ENVIRONMENT family

2 participants