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