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