1<?php 2 3/* 4 * This file is part of the TYPO3 CMS project. 5 * 6 * It is free software; you can redistribute it and/or modify it under 7 * the terms of the GNU General Public License, either version 2 8 * of the License, or any later version. 9 * 10 * For the full copyright and license information, please read the 11 * LICENSE.txt file that was distributed with this source code. 12 * 13 * The TYPO3 project - inspiring people to share! 14 */ 15 16namespace TYPO3\CMS\Install\FolderStructure; 17 18use TYPO3\CMS\Core\Messaging\FlashMessage; 19use TYPO3\CMS\Install\FolderStructure\Exception\InvalidArgumentException; 20 21/** 22 * A file 23 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API. 24 */ 25class FileNode extends AbstractNode implements NodeInterface 26{ 27 /** 28 * @var string Default for files is octal 0664 == decimal 436 29 */ 30 protected $targetPermission = '0664'; 31 32 /** 33 * @var string|null Target content of file. If NULL, target content is ignored 34 */ 35 protected $targetContent; 36 37 /** 38 * Implement constructor 39 * 40 * @param array $structure Structure array 41 * @param NodeInterface $parent Parent object 42 * @throws Exception\InvalidArgumentException 43 */ 44 public function __construct(array $structure, NodeInterface $parent = null) 45 { 46 if ($parent === null) { 47 throw new InvalidArgumentException( 48 'File node must have parent', 49 1366927513 50 ); 51 } 52 $this->parent = $parent; 53 54 // Ensure name is a single segment, but not a path like foo/bar or an absolute path /foo 55 if (str_contains($structure['name'], '/')) { 56 throw new InvalidArgumentException( 57 'File name must not contain forward slash', 58 1366222207 59 ); 60 } 61 $this->name = $structure['name']; 62 63 if (isset($structure['targetPermission'])) { 64 $this->setTargetPermission($structure['targetPermission']); 65 } 66 67 if (isset($structure['targetContent']) && isset($structure['targetContentFile'])) { 68 throw new InvalidArgumentException( 69 'Either targetContent or targetContentFile can be set, but not both', 70 1380364361 71 ); 72 } 73 74 if (isset($structure['targetContent'])) { 75 $this->targetContent = $structure['targetContent']; 76 } 77 if (isset($structure['targetContentFile'])) { 78 if (!is_readable($structure['targetContentFile'])) { 79 throw new InvalidArgumentException( 80 'targetContentFile ' . $structure['targetContentFile'] . ' does not exist or is not readable', 81 1380364362 82 ); 83 } 84 $fileContent = file_get_contents($structure['targetContentFile']); 85 if ($fileContent === false) { 86 throw new InvalidArgumentException( 87 'Error while reading targetContentFile ' . $structure['targetContentFile'], 88 1380364363 89 ); 90 } 91 $this->targetContent = $fileContent; 92 } 93 } 94 95 /** 96 * Get own status 97 * Returns warning if file not exists 98 * Returns error if file exists but content is not as expected (can / shouldn't be fixed) 99 * 100 * @return FlashMessage[] 101 */ 102 public function getStatus(): array 103 { 104 $result = []; 105 if (!$this->exists()) { 106 $result[] = new FlashMessage( 107 'By using "Try to fix errors" we can try to create it', 108 'File ' . $this->getRelativePathBelowSiteRoot() . ' does not exist', 109 FlashMessage::WARNING 110 ); 111 } else { 112 $result = $this->getSelfStatus(); 113 } 114 return $result; 115 } 116 117 /** 118 * Fix structure 119 * 120 * If there is nothing to fix, returns an empty array 121 * 122 * @return FlashMessage[] 123 */ 124 public function fix(): array 125 { 126 $result = $this->fixSelf(); 127 return $result; 128 } 129 130 /** 131 * Fix this node: create if not there, fix permissions 132 * 133 * @return FlashMessage[] 134 */ 135 protected function fixSelf(): array 136 { 137 $result = []; 138 if (!$this->exists()) { 139 $resultCreateFile = $this->createFile(); 140 $result[] = $resultCreateFile; 141 if ($resultCreateFile->getSeverity() === FlashMessage::OK 142 && $this->targetContent !== null 143 ) { 144 $result[] = $this->setContent(); 145 if (!$this->isPermissionCorrect()) { 146 $result[] = $this->fixPermission(); 147 } 148 } 149 } elseif (!$this->isFile()) { 150 $fileType = @filetype($this->getAbsolutePath()); 151 if ($fileType) { 152 $messageBody = 153 'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a file,' . 154 ' but is of type ' . $fileType . '. This cannot be fixed automatically. Please investigate.' 155 ; 156 } else { 157 $messageBody = 158 'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a file,' . 159 ' but is of unknown type, probably because an upper level directory does not exist. Please investigate.' 160 ; 161 } 162 $result[] = new FlashMessage( 163 $messageBody, 164 'Path ' . $this->getRelativePathBelowSiteRoot() . ' is not a file', 165 FlashMessage::ERROR 166 ); 167 } elseif (!$this->isPermissionCorrect()) { 168 $result[] = $this->fixPermission(); 169 } 170 return $result; 171 } 172 173 /** 174 * Create file if not exists 175 * 176 * @throws Exception 177 * @return FlashMessage 178 */ 179 protected function createFile(): FlashMessage 180 { 181 if ($this->exists()) { 182 throw new Exception( 183 'File ' . $this->getRelativePathBelowSiteRoot() . ' already exists', 184 1367048077 185 ); 186 } 187 $result = @touch($this->getAbsolutePath()); 188 if ($result === true) { 189 return new FlashMessage( 190 '', 191 'File ' . $this->getRelativePathBelowSiteRoot() . ' successfully created.' 192 ); 193 } 194 return new FlashMessage( 195 'The target file could not be created. There is probably a' 196 . ' group or owner permission problem on the parent directory.', 197 'File ' . $this->getRelativePathBelowSiteRoot() . ' not created!', 198 FlashMessage::ERROR 199 ); 200 } 201 202 /** 203 * Get status of file 204 * 205 * @return FlashMessage[] 206 */ 207 protected function getSelfStatus(): array 208 { 209 $result = []; 210 if (!$this->isFile()) { 211 $result[] = new FlashMessage( 212 'Path ' . $this->getAbsolutePath() . ' should be a file,' 213 . ' but is of type ' . filetype($this->getAbsolutePath()), 214 $this->getRelativePathBelowSiteRoot() . ' is not a file', 215 FlashMessage::ERROR 216 ); 217 } elseif (!$this->isWritable()) { 218 $result[] = new FlashMessage( 219 'File ' . $this->getRelativePathBelowSiteRoot() . ' exists, but is not writable.', 220 'File ' . $this->getRelativePathBelowSiteRoot() . ' is not writable', 221 FlashMessage::NOTICE 222 ); 223 } elseif (!$this->isPermissionCorrect()) { 224 $result[] = new FlashMessage( 225 'Default configured permissions are ' . $this->getTargetPermission() 226 . ' but file permissions are ' . $this->getCurrentPermission(), 227 'File ' . $this->getRelativePathBelowSiteRoot() . ' permissions mismatch', 228 FlashMessage::NOTICE 229 ); 230 } 231 if ($this->isFile() && !$this->isContentCorrect()) { 232 $result[] = new FlashMessage( 233 'File content is not identical to default content. This file may have been changed manually.' 234 . ' The Install Tool will not overwrite the current version!', 235 'File ' . $this->getRelativePathBelowSiteRoot() . ' content differs', 236 FlashMessage::NOTICE 237 ); 238 } else { 239 $result[] = new FlashMessage( 240 'Is a file with the default content and configured permissions of ' . $this->getTargetPermission(), 241 'File ' . $this->getRelativePathBelowSiteRoot() 242 ); 243 } 244 return $result; 245 } 246 247 /** 248 * Compare current file content with target file content 249 * 250 * @throws Exception If file does not exist 251 * @return bool TRUE if current and target file content are identical 252 */ 253 protected function isContentCorrect() 254 { 255 $absolutePath = $this->getAbsolutePath(); 256 if (is_link($absolutePath) || !is_file($absolutePath)) { 257 throw new Exception( 258 'File ' . $absolutePath . ' must exist', 259 1367056363 260 ); 261 } 262 $result = false; 263 if ($this->targetContent === null) { 264 $result = true; 265 } else { 266 $targetContentHash = md5($this->targetContent); 267 $currentContentHash = md5((string)file_get_contents($absolutePath)); 268 if ($targetContentHash === $currentContentHash) { 269 $result = true; 270 } 271 } 272 return $result; 273 } 274 275 /** 276 * Sets content of file to target content 277 * 278 * @throws Exception If file does not exist 279 * @return FlashMessage 280 */ 281 protected function setContent(): FlashMessage 282 { 283 $absolutePath = $this->getAbsolutePath(); 284 if (is_link($absolutePath) || !is_file($absolutePath)) { 285 throw new Exception( 286 'File ' . $absolutePath . ' must exist', 287 1367060201 288 ); 289 } 290 if ($this->targetContent === null) { 291 throw new Exception( 292 'Target content not defined for ' . $absolutePath, 293 1367060202 294 ); 295 } 296 $result = @file_put_contents($absolutePath, $this->targetContent); 297 if ($result !== false) { 298 return new FlashMessage( 299 '', 300 'Set content to ' . $this->getRelativePathBelowSiteRoot() 301 ); 302 } 303 return new FlashMessage( 304 'Setting content of the file failed for unknown reasons.', 305 'Setting content to ' . $this->getRelativePathBelowSiteRoot() . ' failed', 306 FlashMessage::ERROR 307 ); 308 } 309 310 /** 311 * Checks if not is a file 312 * 313 * @return bool 314 */ 315 protected function isFile() 316 { 317 $path = $this->getAbsolutePath(); 318 return !is_link($path) && is_file($path); 319 } 320} 321