vendor/symfony/dotenv/Dotenv.php line 48

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Dotenv;
  11. use Symfony\Component\Dotenv\Exception\FormatException;
  12. use Symfony\Component\Dotenv\Exception\FormatExceptionContext;
  13. use Symfony\Component\Dotenv\Exception\PathException;
  14. use Symfony\Component\Process\Exception\ExceptionInterface as ProcessException;
  15. use Symfony\Component\Process\Process;
  16. /**
  17.  * Manages .env files.
  18.  *
  19.  * @author Fabien Potencier <fabien@symfony.com>
  20.  * @author Kévin Dunglas <dunglas@gmail.com>
  21.  */
  22. final class Dotenv
  23. {
  24.     public const VARNAME_REGEX '(?i:[A-Z][A-Z0-9_]*+)';
  25.     public const STATE_VARNAME 0;
  26.     public const STATE_VALUE 1;
  27.     private $path;
  28.     private $cursor;
  29.     private $lineno;
  30.     private $data;
  31.     private $end;
  32.     private $values;
  33.     private $usePutenv;
  34.     /**
  35.      * @var bool If `putenv()` should be used to define environment variables or not.
  36.      *           Beware that `putenv()` is not thread safe and this setting will default
  37.      *           to `false` in Symfony 5.0.
  38.      */
  39.     public function __construct(bool $usePutenv true)
  40.     {
  41.         if (!\func_num_args()) {
  42.             @trigger_error(sprintf('The default value of "$usePutenv" argument of "%s" will be changed from "true" to "false" in Symfony 5.0. You should define its value explicitly.'__METHOD__), \E_USER_DEPRECATED);
  43.         }
  44.         $this->usePutenv $usePutenv;
  45.     }
  46.     /**
  47.      * Loads one or several .env files.
  48.      *
  49.      * @param string   $path          A file to load
  50.      * @param string[] ...$extraPaths A list of additional files to load
  51.      *
  52.      * @throws FormatException when a file has a syntax error
  53.      * @throws PathException   when a file does not exist or is not readable
  54.      */
  55.     public function load(string $pathstring ...$extraPaths): void
  56.     {
  57.         $this->doLoad(false, \func_get_args());
  58.     }
  59.     /**
  60.      * Loads a .env file and the corresponding .env.local, .env.$env and .env.$env.local files if they exist.
  61.      *
  62.      * .env.local is always ignored in test env because tests should produce the same results for everyone.
  63.      * .env.dist is loaded when it exists and .env is not found.
  64.      *
  65.      * @param string $path       A file to load
  66.      * @param string $varName    The name of the env vars that defines the app env
  67.      * @param string $defaultEnv The app env to use when none is defined
  68.      * @param array  $testEnvs   A list of app envs for which .env.local should be ignored
  69.      *
  70.      * @throws FormatException when a file has a syntax error
  71.      * @throws PathException   when a file does not exist or is not readable
  72.      */
  73.     public function loadEnv(string $pathstring $varName 'APP_ENV'string $defaultEnv 'dev', array $testEnvs = ['test']): void
  74.     {
  75.         if (file_exists($path) || !file_exists($p "$path.dist")) {
  76.             $this->load($path);
  77.         } else {
  78.             $this->load($p);
  79.         }
  80.         if (null === $env $_SERVER[$varName] ?? $_ENV[$varName] ?? null) {
  81.             $this->populate([$varName => $env $defaultEnv]);
  82.         }
  83.         if (!\in_array($env$testEnvstrue) && file_exists($p "$path.local")) {
  84.             $this->load($p);
  85.             $env $_SERVER[$varName] ?? $_ENV[$varName] ?? $env;
  86.         }
  87.         if ('local' === $env) {
  88.             return;
  89.         }
  90.         if (file_exists($p "$path.$env")) {
  91.             $this->load($p);
  92.         }
  93.         if (file_exists($p "$path.$env.local")) {
  94.             $this->load($p);
  95.         }
  96.     }
  97.     /**
  98.      * Loads one or several .env files and enables override existing vars.
  99.      *
  100.      * @param string   $path          A file to load
  101.      * @param string[] ...$extraPaths A list of additional files to load
  102.      *
  103.      * @throws FormatException when a file has a syntax error
  104.      * @throws PathException   when a file does not exist or is not readable
  105.      */
  106.     public function overload(string $pathstring ...$extraPaths): void
  107.     {
  108.         $this->doLoad(true, \func_get_args());
  109.     }
  110.     /**
  111.      * Sets values as environment variables (via putenv, $_ENV, and $_SERVER).
  112.      *
  113.      * @param array $values               An array of env variables
  114.      * @param bool  $overrideExistingVars true when existing environment variables must be overridden
  115.      */
  116.     public function populate(array $valuesbool $overrideExistingVars false): void
  117.     {
  118.         $updateLoadedVars false;
  119.         $loadedVars array_flip(explode(','$_SERVER['SYMFONY_DOTENV_VARS'] ?? $_ENV['SYMFONY_DOTENV_VARS'] ?? ''));
  120.         foreach ($values as $name => $value) {
  121.             $notHttpName !== strpos($name'HTTP_');
  122.             if (isset($_SERVER[$name]) && $notHttpName && !isset($_ENV[$name])) {
  123.                 $_ENV[$name] = $_SERVER[$name];
  124.             }
  125.             // don't check existence with getenv() because of thread safety issues
  126.             if (!isset($loadedVars[$name]) && !$overrideExistingVars && isset($_ENV[$name])) {
  127.                 continue;
  128.             }
  129.             if ($this->usePutenv) {
  130.                 putenv("$name=$value");
  131.             }
  132.             $_ENV[$name] = $value;
  133.             if ($notHttpName) {
  134.                 $_SERVER[$name] = $value;
  135.             }
  136.             if (!isset($loadedVars[$name])) {
  137.                 $loadedVars[$name] = $updateLoadedVars true;
  138.             }
  139.         }
  140.         if ($updateLoadedVars) {
  141.             unset($loadedVars['']);
  142.             $loadedVars implode(','array_keys($loadedVars));
  143.             $_ENV['SYMFONY_DOTENV_VARS'] = $_SERVER['SYMFONY_DOTENV_VARS'] = $loadedVars;
  144.             if ($this->usePutenv) {
  145.                 putenv('SYMFONY_DOTENV_VARS='.$loadedVars);
  146.             }
  147.         }
  148.     }
  149.     /**
  150.      * Parses the contents of an .env file.
  151.      *
  152.      * @param string $data The data to be parsed
  153.      * @param string $path The original file name where data where stored (used for more meaningful error messages)
  154.      *
  155.      * @return array An array of env variables
  156.      *
  157.      * @throws FormatException when a file has a syntax error
  158.      */
  159.     public function parse(string $datastring $path '.env'): array
  160.     {
  161.         $this->path $path;
  162.         $this->data str_replace(["\r\n""\r"], "\n"$data);
  163.         $this->lineno 1;
  164.         $this->cursor 0;
  165.         $this->end = \strlen($this->data);
  166.         $state self::STATE_VARNAME;
  167.         $this->values = [];
  168.         $name '';
  169.         $this->skipEmptyLines();
  170.         while ($this->cursor $this->end) {
  171.             switch ($state) {
  172.                 case self::STATE_VARNAME:
  173.                     $name $this->lexVarname();
  174.                     $state self::STATE_VALUE;
  175.                     break;
  176.                 case self::STATE_VALUE:
  177.                     $this->values[$name] = $this->lexValue();
  178.                     $state self::STATE_VARNAME;
  179.                     break;
  180.             }
  181.         }
  182.         if (self::STATE_VALUE === $state) {
  183.             $this->values[$name] = '';
  184.         }
  185.         try {
  186.             return $this->values;
  187.         } finally {
  188.             $this->values = [];
  189.             $this->data null;
  190.             $this->path null;
  191.         }
  192.     }
  193.     private function lexVarname(): string
  194.     {
  195.         // var name + optional export
  196.         if (!preg_match('/(export[ \t]++)?('.self::VARNAME_REGEX.')/A'$this->data$matches0$this->cursor)) {
  197.             throw $this->createFormatException('Invalid character in variable name');
  198.         }
  199.         $this->moveCursor($matches[0]);
  200.         if ($this->cursor === $this->end || "\n" === $this->data[$this->cursor] || '#' === $this->data[$this->cursor]) {
  201.             if ($matches[1]) {
  202.                 throw $this->createFormatException('Unable to unset an environment variable');
  203.             }
  204.             throw $this->createFormatException('Missing = in the environment variable declaration');
  205.         }
  206.         if (' ' === $this->data[$this->cursor] || "\t" === $this->data[$this->cursor]) {
  207.             throw $this->createFormatException('Whitespace characters are not supported after the variable name');
  208.         }
  209.         if ('=' !== $this->data[$this->cursor]) {
  210.             throw $this->createFormatException('Missing = in the environment variable declaration');
  211.         }
  212.         ++$this->cursor;
  213.         return $matches[2];
  214.     }
  215.     private function lexValue(): string
  216.     {
  217.         if (preg_match('/[ \t]*+(?:#.*)?$/Am'$this->data$matches0$this->cursor)) {
  218.             $this->moveCursor($matches[0]);
  219.             $this->skipEmptyLines();
  220.             return '';
  221.         }
  222.         if (' ' === $this->data[$this->cursor] || "\t" === $this->data[$this->cursor]) {
  223.             throw $this->createFormatException('Whitespace are not supported before the value');
  224.         }
  225.         $loadedVars array_flip(explode(','$_SERVER['SYMFONY_DOTENV_VARS'] ?? ($_ENV['SYMFONY_DOTENV_VARS'] ?? '')));
  226.         unset($loadedVars['']);
  227.         $v '';
  228.         do {
  229.             if ("'" === $this->data[$this->cursor]) {
  230.                 $len 0;
  231.                 do {
  232.                     if ($this->cursor + ++$len === $this->end) {
  233.                         $this->cursor += $len;
  234.                         throw $this->createFormatException('Missing quote to end the value');
  235.                     }
  236.                 } while ("'" !== $this->data[$this->cursor $len]);
  237.                 $v .= substr($this->data$this->cursor$len 1);
  238.                 $this->cursor += $len;
  239.             } elseif ('"' === $this->data[$this->cursor]) {
  240.                 $value '';
  241.                 if (++$this->cursor === $this->end) {
  242.                     throw $this->createFormatException('Missing quote to end the value');
  243.                 }
  244.                 while ('"' !== $this->data[$this->cursor] || ('\\' === $this->data[$this->cursor 1] && '\\' !== $this->data[$this->cursor 2])) {
  245.                     $value .= $this->data[$this->cursor];
  246.                     ++$this->cursor;
  247.                     if ($this->cursor === $this->end) {
  248.                         throw $this->createFormatException('Missing quote to end the value');
  249.                     }
  250.                 }
  251.                 ++$this->cursor;
  252.                 $value str_replace(['\\"''\r''\n'], ['"'"\r""\n"], $value);
  253.                 $resolvedValue $value;
  254.                 $resolvedValue $this->resolveVariables($resolvedValue$loadedVars);
  255.                 $resolvedValue $this->resolveCommands($resolvedValue$loadedVars);
  256.                 $resolvedValue str_replace('\\\\''\\'$resolvedValue);
  257.                 $v .= $resolvedValue;
  258.             } else {
  259.                 $value '';
  260.                 $prevChr $this->data[$this->cursor 1];
  261.                 while ($this->cursor $this->end && !\in_array($this->data[$this->cursor], ["\n"'"'"'"], true) && !((' ' === $prevChr || "\t" === $prevChr) && '#' === $this->data[$this->cursor])) {
  262.                     if ('\\' === $this->data[$this->cursor] && isset($this->data[$this->cursor 1]) && ('"' === $this->data[$this->cursor 1] || "'" === $this->data[$this->cursor 1])) {
  263.                         ++$this->cursor;
  264.                     }
  265.                     $value .= $prevChr $this->data[$this->cursor];
  266.                     if ('$' === $this->data[$this->cursor] && isset($this->data[$this->cursor 1]) && '(' === $this->data[$this->cursor 1]) {
  267.                         ++$this->cursor;
  268.                         $value .= '('.$this->lexNestedExpression().')';
  269.                     }
  270.                     ++$this->cursor;
  271.                 }
  272.                 $value rtrim($value);
  273.                 $resolvedValue $value;
  274.                 $resolvedValue $this->resolveVariables($resolvedValue$loadedVars);
  275.                 $resolvedValue $this->resolveCommands($resolvedValue$loadedVars);
  276.                 $resolvedValue str_replace('\\\\''\\'$resolvedValue);
  277.                 if ($resolvedValue === $value && preg_match('/\s+/'$value)) {
  278.                     throw $this->createFormatException('A value containing spaces must be surrounded by quotes');
  279.                 }
  280.                 $v .= $resolvedValue;
  281.                 if ($this->cursor $this->end && '#' === $this->data[$this->cursor]) {
  282.                     break;
  283.                 }
  284.             }
  285.         } while ($this->cursor $this->end && "\n" !== $this->data[$this->cursor]);
  286.         $this->skipEmptyLines();
  287.         return $v;
  288.     }
  289.     private function lexNestedExpression(): string
  290.     {
  291.         ++$this->cursor;
  292.         $value '';
  293.         while ("\n" !== $this->data[$this->cursor] && ')' !== $this->data[$this->cursor]) {
  294.             $value .= $this->data[$this->cursor];
  295.             if ('(' === $this->data[$this->cursor]) {
  296.                 $value .= $this->lexNestedExpression().')';
  297.             }
  298.             ++$this->cursor;
  299.             if ($this->cursor === $this->end) {
  300.                 throw $this->createFormatException('Missing closing parenthesis.');
  301.             }
  302.         }
  303.         if ("\n" === $this->data[$this->cursor]) {
  304.             throw $this->createFormatException('Missing closing parenthesis.');
  305.         }
  306.         return $value;
  307.     }
  308.     private function skipEmptyLines()
  309.     {
  310.         if (preg_match('/(?:\s*+(?:#[^\n]*+)?+)++/A'$this->data$match0$this->cursor)) {
  311.             $this->moveCursor($match[0]);
  312.         }
  313.     }
  314.     private function resolveCommands(string $value, array $loadedVars): string
  315.     {
  316.         if (false === strpos($value'$')) {
  317.             return $value;
  318.         }
  319.         $regex '/
  320.             (\\\\)?               # escaped with a backslash?
  321.             \$
  322.             (?<cmd>
  323.                 \(                # require opening parenthesis
  324.                 ([^()]|\g<cmd>)+  # allow any number of non-parens, or balanced parens (by nesting the <cmd> expression recursively)
  325.                 \)                # require closing paren
  326.             )
  327.         /x';
  328.         return preg_replace_callback($regex, function ($matches) use ($loadedVars) {
  329.             if ('\\' === $matches[1]) {
  330.                 return substr($matches[0], 1);
  331.             }
  332.             if ('\\' === \DIRECTORY_SEPARATOR) {
  333.                 throw new \LogicException('Resolving commands is not supported on Windows.');
  334.             }
  335.             if (!class_exists(Process::class)) {
  336.                 throw new \LogicException('Resolving commands requires the Symfony Process component.');
  337.             }
  338.             $process method_exists(Process::class, 'fromShellCommandline') ? Process::fromShellCommandline('echo '.$matches[0]) : new Process('echo '.$matches[0]);
  339.             if (!method_exists(Process::class, 'fromShellCommandline') && method_exists(Process::class, 'inheritEnvironmentVariables')) {
  340.                 // Symfony 3.4 does not inherit env vars by default:
  341.                 $process->inheritEnvironmentVariables();
  342.             }
  343.             $env = [];
  344.             foreach ($this->values as $name => $value) {
  345.                 if (isset($loadedVars[$name]) || (!isset($_ENV[$name]) && !(isset($_SERVER[$name]) && !== strpos($name'HTTP_')))) {
  346.                     $env[$name] = $value;
  347.                 }
  348.             }
  349.             $process->setEnv($env);
  350.             try {
  351.                 $process->mustRun();
  352.             } catch (ProcessException $e) {
  353.                 throw $this->createFormatException(sprintf('Issue expanding a command (%s)'$process->getErrorOutput()));
  354.             }
  355.             return preg_replace('/[\r\n]+$/'''$process->getOutput());
  356.         }, $value);
  357.     }
  358.     private function resolveVariables(string $value, array $loadedVars): string
  359.     {
  360.         if (false === strpos($value'$')) {
  361.             return $value;
  362.         }
  363.         $regex '/
  364.             (?<!\\\\)
  365.             (?P<backslashes>\\\\*)             # escaped with a backslash?
  366.             \$
  367.             (?!\()                             # no opening parenthesis
  368.             (?P<opening_brace>\{)?             # optional brace
  369.             (?P<name>'.self::VARNAME_REGEX.')? # var name
  370.             (?P<default_value>:[-=][^\}]++)?   # optional default value
  371.             (?P<closing_brace>\})?             # optional closing brace
  372.         /x';
  373.         $value preg_replace_callback($regex, function ($matches) use ($loadedVars) {
  374.             // odd number of backslashes means the $ character is escaped
  375.             if (=== \strlen($matches['backslashes']) % 2) {
  376.                 return substr($matches[0], 1);
  377.             }
  378.             // unescaped $ not followed by variable name
  379.             if (!isset($matches['name'])) {
  380.                 return $matches[0];
  381.             }
  382.             if ('{' === $matches['opening_brace'] && !isset($matches['closing_brace'])) {
  383.                 throw $this->createFormatException('Unclosed braces on variable expansion');
  384.             }
  385.             $name $matches['name'];
  386.             if (isset($loadedVars[$name]) && isset($this->values[$name])) {
  387.                 $value $this->values[$name];
  388.             } elseif (isset($_ENV[$name])) {
  389.                 $value $_ENV[$name];
  390.             } elseif (isset($_SERVER[$name]) && !== strpos($name'HTTP_')) {
  391.                 $value $_SERVER[$name];
  392.             } elseif (isset($this->values[$name])) {
  393.                 $value $this->values[$name];
  394.             } else {
  395.                 $value = (string) getenv($name);
  396.             }
  397.             if ('' === $value && isset($matches['default_value']) && '' !== $matches['default_value']) {
  398.                 $unsupportedChars strpbrk($matches['default_value'], '\'"{$');
  399.                 if (false !== $unsupportedChars) {
  400.                     throw $this->createFormatException(sprintf('Unsupported character "%s" found in the default value of variable "$%s".'$unsupportedChars[0], $name));
  401.                 }
  402.                 $value substr($matches['default_value'], 2);
  403.                 if ('=' === $matches['default_value'][1]) {
  404.                     $this->values[$name] = $value;
  405.                 }
  406.             }
  407.             if (!$matches['opening_brace'] && isset($matches['closing_brace'])) {
  408.                 $value .= '}';
  409.             }
  410.             return $matches['backslashes'].$value;
  411.         }, $value);
  412.         return $value;
  413.     }
  414.     private function moveCursor(string $text)
  415.     {
  416.         $this->cursor += \strlen($text);
  417.         $this->lineno += substr_count($text"\n");
  418.     }
  419.     private function createFormatException(string $message): FormatException
  420.     {
  421.         return new FormatException($message, new FormatExceptionContext($this->data$this->path$this->lineno$this->cursor));
  422.     }
  423.     private function doLoad(bool $overrideExistingVars, array $paths): void
  424.     {
  425.         foreach ($paths as $path) {
  426.             if (!is_readable($path) || is_dir($path)) {
  427.                 throw new PathException($path);
  428.             }
  429.             $this->populate($this->parse(file_get_contents($path), $path), $overrideExistingVars);
  430.         }
  431.     }
  432. }