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