Completed
Push — master ( 3bddf0...f66149 )
by
unknown
114:11 queued 100:28
created

YamlFileLoader::isEnvPlaceholder()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 2
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
namespace TYPO3\CMS\Core\Configuration\Loader;
3
4
/*
5
 * This file is part of the TYPO3 CMS project.
6
 *
7
 * It is free software; you can redistribute it and/or modify it under
8
 * the terms of the GNU General Public License, either version 2
9
 * of the License, or any later version.
10
 *
11
 * For the full copyright and license information, please read the
12
 * LICENSE.txt file that was distributed with this source code.
13
 *
14
 * The TYPO3 project - inspiring people to share!
15
 */
16
17
use Symfony\Component\Yaml\Yaml;
18
use TYPO3\CMS\Core\Configuration\Processor\PlaceholderProcessorList;
19
use TYPO3\CMS\Core\Utility\ArrayUtility;
20
use TYPO3\CMS\Core\Utility\GeneralUtility;
21
use TYPO3\CMS\Core\Utility\PathUtility;
22
23
/**
24
 * A YAML file loader that allows to load YAML files, based on the Symfony/Yaml component
25
 *
26
 * In addition to just load a YAML file, it adds some special functionality.
27
 *
28
 * - A special "imports" key in the YAML file allows to include other YAML files recursively.
29
 *   The actual YAML file gets loaded after the import statements, which are interpreted first,
30
 *   at the very beginning. Imports can be referenced with a relative path.
31
 *
32
 * - Merging configuration options of import files when having simple "lists" will add items to the list instead
33
 *   of overwriting them.
34
 *
35
 * - Special placeholder values set via %optionA.suboptionB% replace the value with the named path of the configuration
36
 *   The placeholders will act as a full replacement of this value.
37
 *
38
 * - Environment placeholder values set via %env(option)% will be replaced by env variables of the same name
39
 */
40
class YamlFileLoader
41
{
42
    public const PROCESS_PLACEHOLDERS = 1;
43
    public const PROCESS_IMPORTS = 2;
44
45
    /**
46
     * @var int
47
     */
48
    private $flags;
49
50
    /**
51
     * Loads and parses a YAML file, and returns an array with the found data
52
     *
53
     * @param string $fileName either relative to TYPO3's base project folder or prefixed with EXT:...
54
     * @param int $flags Flags to configure behaviour of the loader: see public PROCESS_ constants above
55
     * @return array the configuration as array
56
     */
57
    public function load(string $fileName, int $flags = self::PROCESS_PLACEHOLDERS | self::PROCESS_IMPORTS): array
58
    {
59
        $this->flags = $flags;
60
        return $this->loadAndParse($fileName, null);
61
    }
62
63
    /**
64
     * Internal method which does all the logic. Built so it can be re-used recursively.
65
     *
66
     * @param string $fileName either relative to TYPO3's base project folder or prefixed with EXT:...
67
     * @param string|null $currentFileName when called recursively
68
     * @return array the configuration as array
69
     */
70
    protected function loadAndParse(string $fileName, ?string $currentFileName): array
71
    {
72
        $sanitizedFileName = $this->getStreamlinedFileName($fileName, $currentFileName);
73
        $content = $this->getFileContents($sanitizedFileName);
74
        $content = Yaml::parse($content);
75
76
        if (!is_array($content)) {
77
            throw new \RuntimeException(
78
                'YAML file "' . $fileName . '" could not be parsed into valid syntax, probably empty?',
79
                1497332874
80
            );
81
        }
82
83
        if ($this->hasFlag(self::PROCESS_IMPORTS)) {
84
            $content = $this->processImports($content, $sanitizedFileName);
85
        }
86
        if ($this->hasFlag(self::PROCESS_PLACEHOLDERS)) {
87
            // Check for "%" placeholders
88
            $content = $this->processPlaceholders($content, $content);
89
        }
90
        return $content;
91
    }
92
93
    /**
94
     * Put into a separate method to ease the pains with unit tests
95
     *
96
     * @param string $fileName
97
     * @return string the contents
98
     */
99
    protected function getFileContents(string $fileName): string
100
    {
101
        return file_get_contents($fileName);
102
    }
103
104
    /**
105
     * Fetches the absolute file name, but if a different file name is given, it is built relative to that.
106
     *
107
     * @param string $fileName either relative to TYPO3's base project folder or prefixed with EXT:...
108
     * @param string|null $currentFileName when called recursively this contains the absolute file name of the file that included this file
109
     * @return string the contents of the file
110
     * @throws \RuntimeException when the file was not accessible
111
     */
112
    protected function getStreamlinedFileName(string $fileName, ?string $currentFileName): string
113
    {
114
        if (!empty($currentFileName)) {
115
            if (strpos($fileName, 'EXT:') === 0 || GeneralUtility::isAbsPath($fileName)) {
116
                $streamlinedFileName = GeneralUtility::getFileAbsFileName($fileName);
117
            } else {
118
                // Now this path is considered to be relative the current file name
119
                $streamlinedFileName = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath(
120
                    $currentFileName,
121
                    $fileName
122
                );
123
                if (!GeneralUtility::isAllowedAbsPath($streamlinedFileName)) {
124
                    throw new \RuntimeException(
125
                        'Referencing a file which is outside of TYPO3s main folder',
126
                        1560319866
127
                    );
128
                }
129
            }
130
        } else {
131
            $streamlinedFileName = GeneralUtility::getFileAbsFileName($fileName);
132
        }
133
        if (!$streamlinedFileName) {
134
            throw new \RuntimeException('YAML File "' . $fileName . '" could not be loaded', 1485784246);
135
        }
136
        return $streamlinedFileName;
137
    }
138
139
    /**
140
     * Checks for the special "imports" key on the main level of a file,
141
     * which calls "load" recursively.
142
     * @param array $content
143
     * @param string|null $fileName
144
     * @return array
145
     */
146
    protected function processImports(array $content, ?string $fileName): array
147
    {
148
        if (isset($content['imports']) && is_array($content['imports'])) {
149
            foreach ($content['imports'] as $import) {
150
                $import = $this->processPlaceholders($import, $import);
151
                $importedContent = $this->loadAndParse($import['resource'], $fileName);
152
                // override the imported content with the one from the current file
153
                $content = ArrayUtility::replaceAndAppendScalarValuesRecursive($importedContent, $content);
154
            }
155
            unset($content['imports']);
156
        }
157
        return $content;
158
    }
159
160
    /**
161
     * Main function that gets called recursively to check for %...% placeholders
162
     * inside the array
163
     *
164
     * @param array $content the current sub-level content array
165
     * @param array $referenceArray the global configuration array
166
     *
167
     * @return array the modified sub-level content array
168
     */
169
    protected function processPlaceholders(array $content, array $referenceArray): array
170
    {
171
        foreach ($content as $k => $v) {
172
            if (is_array($v)) {
173
                $content[$k] = $this->processPlaceholders($v, $referenceArray);
174
            } elseif ($this->containsPlaceholder($v)) {
175
                $content[$k] = $this->processPlaceholderLine($v, $referenceArray);
176
            }
177
        }
178
        return $content;
179
    }
180
181
    /**
182
     * @param string $line
183
     * @param array $referenceArray
184
     * @return mixed
185
     */
186
    protected function processPlaceholderLine(string $line, array $referenceArray)
187
    {
188
        $parts = $this->getParts($line);
189
        foreach ($parts as $partKey => $part) {
190
            $result = $this->processSinglePlaceholder($partKey, $part, $referenceArray);
191
            // Replace whole content if placeholder is the only thing in this line
192
            if ($line === $partKey) {
193
                $line = $result;
194
            } elseif (is_string($result) || is_numeric($result)) {
195
                $line = str_replace($partKey, $result, $line);
196
            } else {
197
                throw new \UnexpectedValueException(
198
                    'Placeholder can not be substituted if result is not string or numeric',
199
                    1581502783
200
                );
201
            }
202
            if ($result !== $partKey && $this->containsPlaceholder($line)) {
203
                $line = $this->processPlaceholderLine($line, $referenceArray);
204
            }
205
        }
206
        return $line;
207
    }
208
209
    /**
210
     * @param string $placeholder
211
     * @param string $value
212
     * @param array $referenceArray
213
     * @return mixed
214
     */
215
    protected function processSinglePlaceholder(string $placeholder, string $value, array $referenceArray)
216
    {
217
        $processorList = GeneralUtility::makeInstance(
218
            PlaceholderProcessorList::class,
219
            $GLOBALS['TYPO3_CONF_VARS']['SYS']['yamlLoader']['placeholderProcessors']
220
        );
221
        foreach ($processorList->compile() as $processor) {
222
            if ($processor->canProcess($placeholder, $referenceArray)) {
223
                try {
224
                    $result = $processor->process($value, $referenceArray);
225
                } catch (\UnexpectedValueException $e) {
226
                    $result = $placeholder;
227
                }
228
                if (is_array($result)) {
229
                    $result = $this->processPlaceholders($result, $referenceArray);
230
                }
231
                break;
232
            }
233
        }
234
        return $result ?? $placeholder;
235
    }
236
237
    /**
238
     * @param string $placeholders
239
     * @return array
240
     */
241
    protected function getParts(string $placeholders): array
242
    {
243
        // find occurences of placeholders like %some()% and %array.access%.
244
        // Only find the innermost ones, so we can nest them.
245
        preg_match_all(
246
            '/%[^(%]+?\([\'"]?([^(]*?)[\'"]?\)%|%([^%()]*?)%/',
247
            $placeholders,
248
            $parts,
249
            PREG_UNMATCHED_AS_NULL
250
        );
251
        $matches = array_filter(
252
            array_merge($parts[1], $parts[2])
253
        );
254
        return array_combine($parts[0], $matches);
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_combine($parts[0], $matches) could return the type false which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
255
    }
256
257
    /**
258
     * Finds possible placeholders.
259
     * May find false positives for complexer structures, but they will be sorted later on.
260
     *
261
     * @param $value
262
     * @return bool
263
     */
264
    protected function containsPlaceholder($value): bool
265
    {
266
        return is_string($value) && substr_count($value, '%') >= 2;
267
    }
268
269
    protected function hasFlag(int $flag): bool
270
    {
271
        return ($this->flags & $flag) === $flag;
272
    }
273
}
274