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