1<?php
2namespace TYPO3\CMS\Composer\Plugin;
3
4/*
5 * This file is part of the TYPO3 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
17use Composer\Composer;
18use Composer\IO\IOInterface;
19use Composer\IO\NullIO;
20use Composer\Package\RootPackageInterface;
21use TYPO3\CMS\Composer\Plugin\Util\Filesystem;
22
23/**
24 * Configuration wrapper to easily access extra configuration for installer
25 */
26class Config
27{
28    const RELATIVE_PATHS = 1;
29
30    /**
31     * @var array
32     */
33    public static $defaultConfig = [
34        'web-dir' => 'public',
35        'root-dir' => '{$web-dir}',
36        'app-dir' => '{$base-dir}',
37        // The following values are for internal use only and do not represent public API
38        // Names and behaviour of these values might change without notice
39        'composer-mode' => true,
40    ];
41
42    /**
43     * @var array
44     */
45    protected $config;
46
47    /**
48     * @var string
49     */
50    protected $baseDir;
51
52    /**
53     * @param string $baseDir
54     */
55    public function __construct($baseDir = null)
56    {
57        $this->baseDir = $baseDir;
58        // load defaults
59        $this->config = static::$defaultConfig;
60    }
61
62    /**
63     * Merges new config values with the existing ones (overriding)
64     *
65     * @param array $config
66     */
67    public function merge(array $config)
68    {
69        // Override defaults with given config
70        if (!empty($config['typo3/cms']) && is_array($config['typo3/cms'])) {
71            foreach ($config['typo3/cms'] as $key => $val) {
72                $this->config[$key] = $val;
73            }
74        }
75    }
76
77    /**
78     * Returns a setting
79     *
80     * @param  string $key
81     * @param  int $flags Options (see class constants)
82     * @return mixed
83     */
84    public function get($key, $flags = 0)
85    {
86        switch ($key) {
87            case 'web-dir':
88            case 'root-dir':
89            case 'app-dir':
90                $val = rtrim($this->process($this->config[$key], $flags), '/\\');
91                return ($flags & self::RELATIVE_PATHS === 1) ? $val : $this->realpath($val);
92            case 'base-dir':
93                return ($flags & self::RELATIVE_PATHS === 1) ? '' : $this->realpath($this->baseDir);
94            default:
95                if (!isset($this->config[$key])) {
96                    return null;
97                }
98                return $this->process($this->config[$key], $flags);
99        }
100    }
101
102    /**
103     * @param int $flags Options (see class constants)
104     * @return array
105     */
106    public function all($flags = 0)
107    {
108        $all = [];
109        foreach (array_keys($this->config) as $key) {
110            $all['config'][$key] = $this->get($key, $flags);
111        }
112
113        return $all;
114    }
115
116    /**
117     * @return array
118     */
119    public function raw()
120    {
121        return [
122            'config' => $this->config,
123        ];
124    }
125
126    /**
127     * Checks whether a setting exists
128     *
129     * @param  string $key
130     * @return bool
131     */
132    public function has($key)
133    {
134        return array_key_exists($key, $this->config);
135    }
136
137    /**
138     * Replaces {$refs} inside a config string
139     *
140     * @param  string $value a config string that can contain {$refs-to-other-config}
141     * @param  int $flags Options (see class constants)
142     * @return string
143     */
144    protected function process($value, $flags)
145    {
146        $config = $this;
147
148        if (!is_string($value)) {
149            return $value;
150        }
151
152        return preg_replace_callback(
153            '#\{\$(.+)\}#',
154            function ($match) use ($config, $flags) {
155                return $config->get($match[1], $flags);
156            },
157            $value
158        );
159    }
160
161    /**
162     * Turns relative paths in absolute paths without realpath()
163     *
164     * Since the dirs might not exist yet we can not call realpath or it will fail.
165     *
166     * @param  string $path
167     * @return string
168     */
169    protected function realpath($path)
170    {
171        if ($path === '') {
172            return $this->baseDir;
173        }
174        if ($path[0] === '/' || (!empty($path[1]) && $path[1] === ':')) {
175            return $path;
176        }
177
178        return $this->baseDir . '/' . $path;
179    }
180
181    /**
182     * @return string
183     */
184    public function getBaseDir()
185    {
186        return $this->baseDir;
187    }
188
189    /**
190     * @param Composer $composer
191     * @param IOInterface|null $io
192     * @return Config
193     */
194    public static function load(Composer $composer, IOInterface $io = null)
195    {
196        static $config;
197        if ($config === null) {
198            $io = $io ?? new NullIO();
199            $baseDir = static::extractBaseDir($composer->getConfig());
200            $rootPackageExtraConfig = self::handleRootPackageExtraConfig($io, $composer->getPackage());
201            $config = new static($baseDir);
202            $config->merge($rootPackageExtraConfig);
203        }
204        return $config;
205    }
206
207    private static function handleRootPackageExtraConfig(IOInterface $io, RootPackageInterface $rootPackage): array
208    {
209        if ($rootPackage->getName() === 'typo3/cms') {
210            // Configuration for the web dir is different, in case
211            // typo3/cms is the root package
212            self::$defaultConfig['web-dir'] = '.';
213
214            return [];
215        }
216        $rootPackageExtraConfig = $rootPackage->getExtra() ?: [];
217        $typo3Config = $rootPackageExtraConfig['typo3/cms'] ?? [];
218        if (empty($typo3Config)) {
219            return $rootPackageExtraConfig;
220        }
221        $fileSystem = new Filesystem();
222        $config = new static('/fake/root');
223        $config->merge($rootPackageExtraConfig);
224        $rootDir = $config->get('root-dir');
225        $appDir = $config->get('app-dir');
226        $relativePath = $fileSystem->findShortestPath($appDir, $rootDir, true);
227        if ($relativePath === './' || strpos($relativePath, '..') === 0) {
228            unset($rootPackageExtraConfig['typo3/cms']['app-dir']);
229            $io->writeError('<warning>TYPO3 public path must be a sub directory of application path. Resetting app-dir config to default.</warning>');
230        }
231
232        return $rootPackageExtraConfig;
233    }
234
235    /**
236     * @param \Composer\Config $config
237     * @return mixed
238     */
239    protected static function extractBaseDir(\Composer\Config $config)
240    {
241        $reflectionClass = new \ReflectionClass($config);
242        $reflectionProperty = $reflectionClass->getProperty('baseDir');
243        $reflectionProperty->setAccessible(true);
244        return $reflectionProperty->getValue($config);
245    }
246}
247