7f43ed50e9578bf17a38722db710907b210c9591
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Log / Writer / FileWriter.php
1 <?php
2
3 namespace TYPO3\CMS\Core\Log\Writer;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use TYPO3\CMS\Core\Core\Environment;
19 use TYPO3\CMS\Core\Log\Exception\InvalidLogWriterConfigurationException;
20 use TYPO3\CMS\Core\Log\LogLevel;
21 use TYPO3\CMS\Core\Log\LogRecord;
22 use TYPO3\CMS\Core\Utility\GeneralUtility;
23 use TYPO3\CMS\Core\Utility\PathUtility;
24
25 /**
26 * Log writer that writes the log records into a file.
27 */
28 class FileWriter extends AbstractWriter
29 {
30 /**
31 * Log file path, relative to TYPO3's base project folder
32 *
33 * @var string
34 */
35 protected $logFile = '';
36
37 /**
38 * @var string
39 */
40 protected $logFileInfix = '';
41
42 /**
43 * Default log file path
44 *
45 * @var string
46 */
47 protected $defaultLogFileTemplate = '/log/typo3_%s.log';
48
49 /**
50 * Log file handle storage
51 *
52 * To avoid concurrent file handles on a the same file when using several FileWriter instances,
53 * we share the file handles in a static class variable
54 *
55 * @static
56 * @var array
57 */
58 protected static $logFileHandles = [];
59
60 /**
61 * Keep track of used file handles by different fileWriter instances
62 * As the logger gets instantiated by class name but the resources
63 * are shared via the static $logFileHandles we need to track usage
64 * of file handles to avoid closing handles that are still needed
65 * by different instances. Only if the count is zero may the file
66 * handle be closed.
67 *
68 * @var array
69 */
70 protected static $logFileHandlesCount = [];
71
72 /**
73 * Constructor, opens the log file handle
74 *
75 * @param array $options
76 * @return FileWriter
77 */
78 public function __construct(array $options = [])
79 {
80 // the parent constructor reads $options and sets them
81 parent::__construct($options);
82 if (empty($options['logFile'])) {
83 $this->setLogFile($this->getDefaultLogFileName());
84 }
85 }
86
87 /**
88 * Destructor, closes the log file handle
89 */
90 public function __destruct()
91 {
92 self::$logFileHandlesCount[$this->logFile]--;
93 if (self::$logFileHandlesCount[$this->logFile] <= 0) {
94 $this->closeLogFile();
95 }
96 }
97
98 public function setLogFileInfix(string $infix)
99 {
100 $this->logFileInfix = $infix;
101 }
102
103 /**
104 * Sets the path to the log file.
105 *
106 * @param string $relativeLogFile path to the log file, relative to public web dir
107 * @return WriterInterface
108 * @throws InvalidLogWriterConfigurationException
109 */
110 public function setLogFile($relativeLogFile)
111 {
112 $logFile = $relativeLogFile;
113 // Skip handling if logFile is a stream resource. This is used by unit tests with vfs:// directories
114 if (false === strpos($logFile, '://') && !PathUtility::isAbsolutePath($logFile)) {
115 $logFile = GeneralUtility::getFileAbsFileName($logFile);
116 if (empty($logFile)) {
117 throw new InvalidLogWriterConfigurationException(
118 'Log file path "' . $relativeLogFile . '" is not valid!',
119 1444374805
120 );
121 }
122 }
123 $this->logFile = $logFile;
124 $this->openLogFile();
125
126 return $this;
127 }
128
129 /**
130 * Gets the path to the log file.
131 *
132 * @return string Path to the log file.
133 */
134 public function getLogFile()
135 {
136 return $this->logFile;
137 }
138
139 /**
140 * Writes the log record
141 *
142 * @param LogRecord $record Log record
143 * @return WriterInterface $this
144 * @throws \RuntimeException
145 */
146 public function writeLog(LogRecord $record)
147 {
148 $timestamp = date('r', (int)$record->getCreated());
149 $levelName = LogLevel::getName($record->getLevel());
150 $data = '';
151 $recordData = $record->getData();
152 if (!empty($recordData)) {
153 // According to PSR3 the exception-key may hold an \Exception
154 // Since json_encode() does not encode an exception, we run the _toString() here
155 if (isset($recordData['exception']) && $recordData['exception'] instanceof \Exception) {
156 $recordData['exception'] = (string)$recordData['exception'];
157 }
158 $data = '- ' . json_encode($recordData);
159 }
160
161 $message = sprintf(
162 '%s [%s] request="%s" component="%s": %s %s',
163 $timestamp,
164 $levelName,
165 $record->getRequestId(),
166 $record->getComponent(),
167 $record->getMessage(),
168 $data
169 );
170
171 if (false === fwrite(self::$logFileHandles[$this->logFile], $message . LF)) {
172 throw new \RuntimeException('Could not write log record to log file', 1345036335);
173 }
174
175 return $this;
176 }
177
178 /**
179 * Opens the log file handle
180 *
181 * @throws \RuntimeException if the log file can't be opened.
182 */
183 protected function openLogFile()
184 {
185 if (isset(self::$logFileHandlesCount[$this->logFile])) {
186 self::$logFileHandlesCount[$this->logFile]++;
187 } else {
188 self::$logFileHandlesCount[$this->logFile] = 1;
189 }
190 if (isset(self::$logFileHandles[$this->logFile]) && is_resource(self::$logFileHandles[$this->logFile] ?? false)) {
191 return;
192 }
193
194 $this->createLogFile();
195 self::$logFileHandles[$this->logFile] = fopen($this->logFile, 'a');
196 if (!is_resource(self::$logFileHandles[$this->logFile])) {
197 throw new \RuntimeException('Could not open log file "' . $this->logFile . '"', 1321804422);
198 }
199 }
200
201 /**
202 * Closes the log file handle.
203 */
204 protected function closeLogFile()
205 {
206 if (!empty(self::$logFileHandles[$this->logFile]) && is_resource(self::$logFileHandles[$this->logFile])) {
207 fclose(self::$logFileHandles[$this->logFile]);
208 unset(self::$logFileHandles[$this->logFile]);
209 }
210 }
211
212 /**
213 * Creates the log file with correct permissions
214 * and parent directories, if needed
215 */
216 protected function createLogFile()
217 {
218 if (file_exists($this->logFile)) {
219 return;
220 }
221 $logFileDirectory = PathUtility::dirname($this->logFile);
222 if (!@is_dir($logFileDirectory)) {
223 GeneralUtility::mkdir_deep($logFileDirectory);
224 // create .htaccess file if log file is within the site path
225 if (PathUtility::getCommonPrefix([Environment::getPublicPath() . '/', $logFileDirectory]) === (Environment::getPublicPath() . '/')) {
226 // only create .htaccess, if we created the directory on our own
227 $this->createHtaccessFile($logFileDirectory . '/.htaccess');
228 }
229 }
230 // create the log file
231 GeneralUtility::writeFile($this->logFile, '');
232 }
233
234 /**
235 * Creates .htaccess file inside a new directory to access protect it
236 *
237 * @param string $htaccessFile Path of .htaccess file
238 */
239 protected function createHtaccessFile($htaccessFile)
240 {
241 // write .htaccess file to protect the log file
242 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['generateApacheHtaccess']) && !file_exists($htaccessFile)) {
243 $htaccessContent = '
244 # Apache < 2.3
245 <IfModule !mod_authz_core.c>
246 Order allow,deny
247 Deny from all
248 Satisfy All
249 </IfModule>
250
251 # Apache ≥ 2.3
252 <IfModule mod_authz_core.c>
253 Require all denied
254 </IfModule>
255 ';
256 GeneralUtility::writeFile($htaccessFile, $htaccessContent);
257 }
258 }
259
260 /**
261 * Returns the path to the default log file.
262 * Uses the defaultLogFileTemplate and replaces the %s placeholder with a short MD5 hash
263 * based on a static string and the current encryption key.
264 *
265 * @return string
266 */
267 protected function getDefaultLogFileName()
268 {
269 $namePart = substr(GeneralUtility::hmac($this->defaultLogFileTemplate, 'defaultLogFile'), 0, 10);
270 if ($this->logFileInfix !== '') {
271 $namePart = $this->logFileInfix . '_' . $namePart;
272 }
273 return Environment::getVarPath() . sprintf($this->defaultLogFileTemplate, $namePart);
274 }
275
276 /**
277 * Allow serialization of logger - reinitialize log file on unserializing
278 */
279 public function __wakeup()
280 {
281 self::$logFileHandlesCount[$this->logFile]++;
282 $this->setLogFile($this->logFile ?: $this->getDefaultLogFileName());
283 }
284
285 /**
286 * Property 'logFile' should be kept
287 *
288 * @return array
289 */
290 public function __sleep(): array
291 {
292 return ['logFile'];
293 }
294 }