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\Core\Log\Writer; 17 18use TYPO3\CMS\Core\Core\Environment; 19use TYPO3\CMS\Core\Log\Exception\InvalidLogWriterConfigurationException; 20use TYPO3\CMS\Core\Log\LogRecord; 21use TYPO3\CMS\Core\Utility\GeneralUtility; 22use TYPO3\CMS\Core\Utility\PathUtility; 23 24/** 25 * Log writer that writes the log records into a file. 26 */ 27class FileWriter extends AbstractWriter 28{ 29 /** 30 * Log file path, relative to TYPO3's base project folder 31 */ 32 protected string $logFile = ''; 33 34 protected string $logFileInfix = ''; 35 36 /** 37 * Default log file path 38 */ 39 protected string $defaultLogFileTemplate = '/log/typo3_%s.log'; 40 41 /** 42 * Log file handle storage 43 * 44 * To avoid concurrent file handles on a the same file when using several FileWriter instances, 45 * we share the file handles in a static class variable 46 * 47 * @static 48 */ 49 protected static array $logFileHandles = []; 50 51 /** 52 * Keep track of used file handles by different fileWriter instances 53 * 54 * As the logger gets instantiated by class name but the resources 55 * are shared via the static $logFileHandles we need to track usage 56 * of file handles to avoid closing handles that are still needed 57 * by different instances. Only if the count is zero may the file 58 * handle be closed. 59 */ 60 protected static array $logFileHandlesCount = []; 61 62 /** 63 * Constructor, opens the log file handle 64 * 65 * @param array $options 66 */ 67 public function __construct(array $options = []) 68 { 69 // the parent constructor reads $options and sets them 70 parent::__construct($options); 71 if (empty($options['logFile'])) { 72 $this->setLogFile($this->getDefaultLogFileName()); 73 } 74 } 75 76 /** 77 * Destructor, closes the log file handle 78 */ 79 public function __destruct() 80 { 81 self::$logFileHandlesCount[$this->logFile]--; 82 if (self::$logFileHandlesCount[$this->logFile] <= 0) { 83 $this->closeLogFile(); 84 } 85 } 86 87 public function setLogFileInfix(string $infix) 88 { 89 $this->logFileInfix = $infix; 90 } 91 92 /** 93 * Sets the path to the log file. 94 * 95 * @param string $relativeLogFile path to the log file, relative to public web dir 96 * @return WriterInterface 97 * @throws InvalidLogWriterConfigurationException 98 */ 99 public function setLogFile(string $relativeLogFile) 100 { 101 $logFile = $relativeLogFile; 102 // Skip handling if logFile is a stream resource. This is used by unit tests with vfs:// directories 103 if (!PathUtility::hasProtocolAndScheme($logFile) && !PathUtility::isAbsolutePath($logFile)) { 104 $logFile = GeneralUtility::getFileAbsFileName($logFile); 105 if (empty($logFile)) { 106 throw new InvalidLogWriterConfigurationException( 107 'Log file path "' . $relativeLogFile . '" is not valid!', 108 1444374805 109 ); 110 } 111 } 112 $this->logFile = $logFile; 113 $this->openLogFile(); 114 115 return $this; 116 } 117 118 /** 119 * Gets the path to the log file. 120 */ 121 public function getLogFile(): string 122 { 123 return $this->logFile; 124 } 125 126 /** 127 * Writes the log record 128 * 129 * @param LogRecord $record Log record 130 * @return WriterInterface $this 131 * @throws \RuntimeException 132 */ 133 public function writeLog(LogRecord $record) 134 { 135 $data = ''; 136 $context = $record->getData(); 137 $message = $record->getMessage(); 138 if (!empty($context)) { 139 // Fold an exception into the message, and string-ify it into context so it can be jsonified. 140 if (isset($context['exception']) && $context['exception'] instanceof \Throwable) { 141 $message .= $this->formatException($context['exception']); 142 $context['exception'] = (string)$context['exception']; 143 } 144 $data = '- ' . json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); 145 } 146 147 $message = sprintf( 148 '%s [%s] request="%s" component="%s": %s %s', 149 date('r', (int)$record->getCreated()), 150 strtoupper($record->getLevel()), 151 $record->getRequestId(), 152 $record->getComponent(), 153 $this->interpolate($message, $context), 154 $data 155 ); 156 157 if (false === fwrite(self::$logFileHandles[$this->logFile], $message . LF)) { 158 throw new \RuntimeException('Could not write log record to log file', 1345036335); 159 } 160 161 return $this; 162 } 163 164 /** 165 * Opens the log file handle 166 * 167 * @throws \RuntimeException if the log file can't be opened. 168 */ 169 protected function openLogFile() 170 { 171 if (isset(self::$logFileHandlesCount[$this->logFile])) { 172 self::$logFileHandlesCount[$this->logFile]++; 173 } else { 174 self::$logFileHandlesCount[$this->logFile] = 1; 175 } 176 if (isset(self::$logFileHandles[$this->logFile]) && is_resource(self::$logFileHandles[$this->logFile] ?? false)) { 177 return; 178 } 179 180 $this->createLogFile(); 181 self::$logFileHandles[$this->logFile] = fopen($this->logFile, 'a'); 182 if (!is_resource(self::$logFileHandles[$this->logFile])) { 183 throw new \RuntimeException('Could not open log file "' . $this->logFile . '"', 1321804422); 184 } 185 } 186 187 /** 188 * Closes the log file handle. 189 */ 190 protected function closeLogFile() 191 { 192 if (!empty(self::$logFileHandles[$this->logFile]) && is_resource(self::$logFileHandles[$this->logFile])) { 193 fclose(self::$logFileHandles[$this->logFile]); 194 unset(self::$logFileHandles[$this->logFile]); 195 } 196 } 197 198 /** 199 * Creates the log file with correct permissions 200 * and parent directories, if needed 201 */ 202 protected function createLogFile() 203 { 204 if (file_exists($this->logFile)) { 205 return; 206 } 207 208 // skip mkdir if logFile refers to any scheme but vfs://, file:// or empty 209 $scheme = parse_url($this->logFile, PHP_URL_SCHEME); 210 if ($scheme === null || $scheme === 'file' || $scheme === 'vfs' || PathUtility::isAbsolutePath($this->logFile)) { 211 // remove file:/ before creating the directory 212 $logFileDirectory = PathUtility::dirname((string)preg_replace('#^file:/#', '', $this->logFile)); 213 if (!@is_dir($logFileDirectory)) { 214 GeneralUtility::mkdir_deep($logFileDirectory); 215 // create .htaccess file if log file is within the site path 216 if (PathUtility::getCommonPrefix([Environment::getPublicPath() . '/', $logFileDirectory]) === (Environment::getPublicPath() . '/')) { 217 // only create .htaccess, if we created the directory on our own 218 $this->createHtaccessFile($logFileDirectory . '/.htaccess'); 219 } 220 } 221 } 222 // create the log file 223 GeneralUtility::writeFile($this->logFile, ''); 224 } 225 226 /** 227 * Creates .htaccess file inside a new directory to access protect it 228 * 229 * @param string $htaccessFile Path of .htaccess file 230 */ 231 protected function createHtaccessFile($htaccessFile) 232 { 233 // write .htaccess file to protect the log file 234 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['generateApacheHtaccess']) && !file_exists($htaccessFile)) { 235 $htaccessContent = <<<END 236# Apache < 2.3 237<IfModule !mod_authz_core.c> 238 Order allow,deny 239 Deny from all 240 Satisfy All 241</IfModule> 242 243# Apache ≥ 2.3 244<IfModule mod_authz_core.c> 245 Require all denied 246</IfModule> 247END; 248 GeneralUtility::writeFile($htaccessFile, $htaccessContent); 249 } 250 } 251 252 /** 253 * Returns the path to the default log file. 254 * Uses the defaultLogFileTemplate and replaces the %s placeholder with a short MD5 hash 255 * based on a static string and the current encryption key. 256 * 257 * @return string 258 */ 259 protected function getDefaultLogFileName() 260 { 261 $namePart = substr(GeneralUtility::hmac($this->defaultLogFileTemplate, 'defaultLogFile'), 0, 10); 262 if ($this->logFileInfix !== '') { 263 $namePart = $this->logFileInfix . '_' . $namePart; 264 } 265 return Environment::getVarPath() . sprintf($this->defaultLogFileTemplate, $namePart); 266 } 267} 268