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 (strpos($structure['name'], '/') !== false) { 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 $this->targetContent = file_get_contents($structure['targetContentFile']); 85 } 86 } 87 88 /** 89 * Get own status 90 * Returns warning if file not exists 91 * Returns error if file exists but content is not as expected (can / shouldn't be fixed) 92 * 93 * @return FlashMessage[] 94 */ 95 public function getStatus(): array 96 { 97 $result = []; 98 if (!$this->exists()) { 99 $result[] = new FlashMessage( 100 'By using "Try to fix errors" we can try to create it', 101 'File ' . $this->getRelativePathBelowSiteRoot() . ' does not exist', 102 FlashMessage::WARNING 103 ); 104 } else { 105 $result = $this->getSelfStatus(); 106 } 107 return $result; 108 } 109 110 /** 111 * Fix structure 112 * 113 * If there is nothing to fix, returns an empty array 114 * 115 * @return FlashMessage[] 116 */ 117 public function fix(): array 118 { 119 $result = $this->fixSelf(); 120 return $result; 121 } 122 123 /** 124 * Fix this node: create if not there, fix permissions 125 * 126 * @return FlashMessage[] 127 */ 128 protected function fixSelf(): array 129 { 130 $result = []; 131 if (!$this->exists()) { 132 $resultCreateFile = $this->createFile(); 133 $result[] = $resultCreateFile; 134 if ($resultCreateFile->getSeverity() === FlashMessage::OK 135 && $this->targetContent !== null 136 ) { 137 $result[] = $this->setContent(); 138 if (!$this->isPermissionCorrect()) { 139 $result[] = $this->fixPermission(); 140 } 141 } 142 } elseif (!$this->isFile()) { 143 $fileType = @filetype($this->getAbsolutePath()); 144 if ($fileType) { 145 $messageBody = 146 'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a file,' . 147 ' but is of type ' . $fileType . '. This cannot be fixed automatically. Please investigate.' 148 ; 149 } else { 150 $messageBody = 151 'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a file,' . 152 ' but is of unknown type, probably because an upper level directory does not exist. Please investigate.' 153 ; 154 } 155 $result[] = new FlashMessage( 156 $messageBody, 157 'Path ' . $this->getRelativePathBelowSiteRoot() . ' is not a file', 158 FlashMessage::ERROR 159 ); 160 } elseif (!$this->isPermissionCorrect()) { 161 $result[] = $this->fixPermission(); 162 } 163 return $result; 164 } 165 166 /** 167 * Create file if not exists 168 * 169 * @throws Exception 170 * @return FlashMessage 171 */ 172 protected function createFile(): FlashMessage 173 { 174 if ($this->exists()) { 175 throw new Exception( 176 'File ' . $this->getRelativePathBelowSiteRoot() . ' already exists', 177 1367048077 178 ); 179 } 180 $result = @touch($this->getAbsolutePath()); 181 if ($result === true) { 182 return new FlashMessage( 183 '', 184 'File ' . $this->getRelativePathBelowSiteRoot() . ' successfully created.' 185 ); 186 } 187 return new FlashMessage( 188 'The target file could not be created. There is probably a' 189 . ' group or owner permission problem on the parent directory.', 190 'File ' . $this->getRelativePathBelowSiteRoot() . ' not created!', 191 FlashMessage::ERROR 192 ); 193 } 194 195 /** 196 * Get status of file 197 * 198 * @return FlashMessage[] 199 */ 200 protected function getSelfStatus(): array 201 { 202 $result = []; 203 if (!$this->isFile()) { 204 $result[] = new FlashMessage( 205 'Path ' . $this->getAbsolutePath() . ' should be a file,' 206 . ' but is of type ' . filetype($this->getAbsolutePath()), 207 $this->getRelativePathBelowSiteRoot() . ' is not a file', 208 FlashMessage::ERROR 209 ); 210 } elseif (!$this->isWritable()) { 211 $result[] = new FlashMessage( 212 'File ' . $this->getRelativePathBelowSiteRoot() . ' exists, but is not writable.', 213 'File ' . $this->getRelativePathBelowSiteRoot() . ' is not writable', 214 FlashMessage::NOTICE 215 ); 216 } elseif (!$this->isPermissionCorrect()) { 217 $result[] = new FlashMessage( 218 'Default configured permissions are ' . $this->getTargetPermission() 219 . ' but file permissions are ' . $this->getCurrentPermission(), 220 'File ' . $this->getRelativePathBelowSiteRoot() . ' permissions mismatch', 221 FlashMessage::NOTICE 222 ); 223 } 224 if ($this->isFile() && !$this->isContentCorrect()) { 225 $result[] = new FlashMessage( 226 'File content is not identical to default content. This file may have been changed manually.' 227 . ' The Install Tool will not overwrite the current version!', 228 'File ' . $this->getRelativePathBelowSiteRoot() . ' content differs', 229 FlashMessage::NOTICE 230 ); 231 } else { 232 $result[] = new FlashMessage( 233 'Is a file with the default content and configured permissions of ' . $this->getTargetPermission(), 234 'File ' . $this->getRelativePathBelowSiteRoot() 235 ); 236 } 237 return $result; 238 } 239 240 /** 241 * Compare current file content with target file content 242 * 243 * @throws Exception If file does not exist 244 * @return bool TRUE if current and target file content are identical 245 */ 246 protected function isContentCorrect() 247 { 248 $absolutePath = $this->getAbsolutePath(); 249 if (is_link($absolutePath) || !is_file($absolutePath)) { 250 throw new Exception( 251 'File ' . $absolutePath . ' must exist', 252 1367056363 253 ); 254 } 255 $result = false; 256 if ($this->targetContent === null) { 257 $result = true; 258 } else { 259 $targetContentHash = md5($this->targetContent); 260 $currentContentHash = md5((string)file_get_contents($absolutePath)); 261 if ($targetContentHash === $currentContentHash) { 262 $result = true; 263 } 264 } 265 return $result; 266 } 267 268 /** 269 * Sets content of file to target content 270 * 271 * @throws Exception If file does not exist 272 * @return FlashMessage 273 */ 274 protected function setContent(): FlashMessage 275 { 276 $absolutePath = $this->getAbsolutePath(); 277 if (is_link($absolutePath) || !is_file($absolutePath)) { 278 throw new Exception( 279 'File ' . $absolutePath . ' must exist', 280 1367060201 281 ); 282 } 283 if ($this->targetContent === null) { 284 throw new Exception( 285 'Target content not defined for ' . $absolutePath, 286 1367060202 287 ); 288 } 289 $result = @file_put_contents($absolutePath, $this->targetContent); 290 if ($result !== false) { 291 return new FlashMessage( 292 '', 293 'Set content to ' . $this->getRelativePathBelowSiteRoot() 294 ); 295 } 296 return new FlashMessage( 297 'Setting content of the file failed for unknown reasons.', 298 'Setting content to ' . $this->getRelativePathBelowSiteRoot() . ' failed', 299 FlashMessage::ERROR 300 ); 301 } 302 303 /** 304 * Checks if not is a file 305 * 306 * @return bool 307 */ 308 protected function isFile() 309 { 310 $path = $this->getAbsolutePath(); 311 return !is_link($path) && is_file($path); 312 } 313} 314