[BUGFIX] Formmail: generates incorrect file upload error in syslog
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / Controller / DataSubmissionController.php
1 <?php
2 namespace TYPO3\CMS\Frontend\Controller;
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\Utility;
18
19 /**
20 * Formmail class
21 *
22 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
23 */
24 class DataSubmissionController {
25
26 /**
27 * @var string
28 */
29 protected $reserved_names = 'recipient,recipient_copy,auto_respond_msg,auto_respond_checksum,redirect,subject,attachment,from_email,from_name,replyto_email,replyto_name,organisation,priority,html_enabled,quoted_printable,submit_x,submit_y';
30
31 /**
32 * Collection of suspicious header data, used for logging
33 *
34 * @var array
35 */
36 protected $dirtyHeaders = array();
37
38 /**
39 * @var string
40 */
41 protected $characterSet;
42
43 /**
44 * @var string
45 */
46 protected $subject;
47
48 /**
49 * @var string
50 */
51 protected $fromName;
52
53 /**
54 * @var string
55 */
56 protected $replyToName;
57
58 /**
59 * @var string
60 */
61 protected $organisation;
62
63 /**
64 * @var string
65 */
66 protected $fromAddress;
67
68 /**
69 * @var string
70 */
71 protected $replyToAddress;
72
73 /**
74 * @var int
75 */
76 protected $priority;
77
78 /**
79 * @var string
80 */
81 protected $autoRespondMessage;
82
83 /**
84 * @var string
85 */
86 protected $encoding = 'quoted-printable';
87
88 /**
89 * @var \TYPO3\CMS\Core\Mail\MailMessage
90 */
91 protected $mailMessage;
92
93 /**
94 * @var string
95 */
96 protected $recipient;
97
98 /**
99 * @var string
100 */
101 protected $plainContent = '';
102
103 /**
104 * @var array Files to clean up at the end (attachments)
105 */
106 protected $temporaryFiles = array();
107
108 /**
109 * Start function
110 * This class is able to generate a mail in formmail-style from the data in $V
111 * Fields:
112 *
113 * [recipient]: email-adress of the one to receive the mail. If array, then all values are expected to be recipients
114 * [attachment]: ....
115 *
116 * [subject]: The subject of the mail
117 * [from_email]: Sender email. If not set, [email] is used
118 * [from_name]: Sender name. If not set, [name] is used
119 * [replyto_email]: Reply-to email. If not set [from_email] is used
120 * [replyto_name]: Reply-to name. If not set [from_name] is used
121 * [organisation]: Organization (header)
122 * [priority]: Priority, 1-5, default 3
123 * [html_enabled]: If mail is sent as html
124 * [use_base64]: If set, base64 encoding will be used instead of quoted-printable
125 *
126 * @param array $valueList Contains values for the field names listed above (with slashes removed if from POST input)
127 * @param bool $base64 Whether to base64 encode the mail content
128 * @return void
129 */
130 public function start($valueList, $base64 = FALSE) {
131 $this->mailMessage = Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
132 if ($GLOBALS['TSFE']->config['config']['formMailCharset']) {
133 // Respect formMailCharset if it was set
134 $this->characterSet = $GLOBALS['TSFE']->csConvObj->parse_charset($GLOBALS['TSFE']->config['config']['formMailCharset']);
135 } elseif ($GLOBALS['TSFE']->metaCharset != $GLOBALS['TSFE']->renderCharset) {
136 // Use metaCharset for mail if different from renderCharset
137 $this->characterSet = $GLOBALS['TSFE']->metaCharset;
138 } else {
139 // Otherwise use renderCharset as default
140 $this->characterSet = $GLOBALS['TSFE']->renderCharset;
141 }
142 if ($base64 || $valueList['use_base64']) {
143 $this->encoding = 'base64';
144 }
145 if (isset($valueList['recipient'])) {
146 // Convert form data from renderCharset to mail charset
147 $this->subject = $valueList['subject'] ? $valueList['subject'] : 'Formmail on ' . Utility\GeneralUtility::getIndpEnv('HTTP_HOST');
148 $this->subject = $this->sanitizeHeaderString($this->subject);
149 $this->fromName = $valueList['from_name'] ? $valueList['from_name'] : ($valueList['name'] ? $valueList['name'] : '');
150 $this->fromName = $this->sanitizeHeaderString($this->fromName);
151 $this->replyToName = $valueList['replyto_name'] ? $valueList['replyto_name'] : $this->fromName;
152 $this->replyToName = $this->sanitizeHeaderString($this->replyToName);
153 $this->organisation = $valueList['organisation'] ? $valueList['organisation'] : '';
154 $this->organisation = $this->sanitizeHeaderString($this->organisation);
155 $this->fromAddress = $valueList['from_email'] ? $valueList['from_email'] : ($valueList['email'] ? $valueList['email'] : '');
156 if (!Utility\GeneralUtility::validEmail($this->fromAddress)) {
157 $this->fromAddress = Utility\MailUtility::getSystemFromAddress();
158 $this->fromName = Utility\MailUtility::getSystemFromName();
159 }
160 $this->replyToAddress = $valueList['replyto_email'] ? $valueList['replyto_email'] : $this->fromAddress;
161 $this->priority = $valueList['priority'] ? Utility\MathUtility::forceIntegerInRange($valueList['priority'], 1, 5) : 3;
162 // Auto responder
163 $this->autoRespondMessage = trim($valueList['auto_respond_msg']) && $this->fromAddress ? trim($valueList['auto_respond_msg']) : '';
164 if ($this->autoRespondMessage !== '') {
165 // Check if the value of the auto responder message has been modified with evil intentions
166 $autoRespondChecksum = $valueList['auto_respond_checksum'];
167 $correctHmacChecksum = Utility\GeneralUtility::hmac($this->autoRespondMessage, 'content_form');
168 if ($autoRespondChecksum !== $correctHmacChecksum) {
169 Utility\GeneralUtility::sysLog('Possible misuse of DataSubmissionController auto respond method. Subject: ' . $valueList['subject'], 'Core', Utility\GeneralUtility::SYSLOG_SEVERITY_ERROR);
170 return;
171 } else {
172 $this->autoRespondMessage = $this->sanitizeHeaderString($this->autoRespondMessage);
173 }
174 }
175 $plainTextContent = '';
176 $htmlContent = '<table border="0" cellpadding="2" cellspacing="2">';
177 // Runs through $V and generates the mail
178 if (is_array($valueList)) {
179 foreach ($valueList as $key => $val) {
180 if (!Utility\GeneralUtility::inList($this->reserved_names, $key)) {
181 $space = strlen($val) > 60 ? LF : '';
182 $val = is_array($val) ? implode($val, LF) : $val;
183 // Convert form data from renderCharset to mail charset (HTML may use entities)
184 $plainTextValue = $val;
185 $HtmlValue = htmlspecialchars($val);
186 $plainTextContent .= strtoupper($key) . ': ' . $space . $plainTextValue . LF . $space;
187 $htmlContent .= '<tr><td bgcolor="#eeeeee"><font face="Verdana" size="1"><strong>' . strtoupper($key) . '</strong></font></td><td bgcolor="#eeeeee"><font face="Verdana" size="1">' . nl2br($HtmlValue) . '&nbsp;</font></td></tr>';
188 }
189 }
190 }
191 $htmlContent .= '</table>';
192 $this->plainContent = $plainTextContent;
193 if ($valueList['html_enabled']) {
194 $this->mailMessage->setBody($htmlContent, 'text/html', $this->characterSet);
195 $this->mailMessage->addPart($plainTextContent, 'text/plain', $this->characterSet);
196 } else {
197 $this->mailMessage->setBody($plainTextContent, 'text/plain', $this->characterSet);
198 }
199 for ($a = 0; $a < 10; $a++) {
200 $variableName = 'attachment' . ($a ?: '');
201 if (!isset($_FILES[$variableName])) {
202 continue;
203 }
204
205 if ($_FILES[$variableName]['error'] !== UPLOAD_ERR_OK) {
206 Utility\GeneralUtility::sysLog(
207 'Error in uploaded file in DataSubmissionController: temporary file "' .
208 $_FILES[$variableName]['tmp_name'] . '" ("' . $_FILES[$variableName]['name'] . '") Error code: ' .
209 $_FILES[$variableName]['error'],
210 'Core',
211 Utility\GeneralUtility::SYSLOG_SEVERITY_ERROR
212 );
213 continue;
214 }
215
216 if (!is_uploaded_file($_FILES[$variableName]['tmp_name'])) {
217 Utility\GeneralUtility::sysLog(
218 'Possible abuse of DataSubmissionController: temporary file "' . $_FILES[$variableName]['tmp_name'] .
219 '" ("' . $_FILES[$variableName]['name'] . '") was not an uploaded file.',
220 'Core',
221 Utility\GeneralUtility::SYSLOG_SEVERITY_ERROR
222 );
223 continue;
224 }
225
226 $theFile = Utility\GeneralUtility::upload_to_tempfile($_FILES[$variableName]['tmp_name']);
227 $theName = $_FILES[$variableName]['name'];
228 if ($theFile && file_exists($theFile)) {
229 if (filesize($theFile) < $GLOBALS['TYPO3_CONF_VARS']['FE']['formmailMaxAttachmentSize']) {
230 $this->mailMessage->attach(\Swift_Attachment::fromPath($theFile)->setFilename($theName));
231 }
232 }
233 $this->temporaryFiles[] = $theFile;
234 }
235 $from = $this->fromName ? array($this->fromAddress => $this->fromName) : array($this->fromAddress);
236 $this->recipient = $this->parseAddresses($valueList['recipient']);
237 $this->mailMessage->setSubject($this->subject)->setFrom($from)->setTo($this->recipient)->setPriority($this->priority);
238 $replyTo = $this->replyToName ? array($this->replyToAddress => $this->replyToName) : array($this->replyToAddress);
239 $this->mailMessage->setReplyTo($replyTo);
240 $this->mailMessage->getHeaders()->addTextHeader('Organization', $this->organisation);
241 if ($valueList['recipient_copy']) {
242 $this->mailMessage->setCc($this->parseAddresses($valueList['recipient_copy']));
243 }
244 $this->mailMessage->setCharset($this->characterSet);
245 // Ignore target encoding. This is handled automatically by Swift Mailer and overriding the defaults
246 // is not worth the trouble
247 // Log dirty header lines
248 if ($this->dirtyHeaders) {
249 Utility\GeneralUtility::sysLog('Possible misuse of DataSubmissionController: see TYPO3 devLog', 'Core', Utility\GeneralUtility::SYSLOG_SEVERITY_ERROR);
250 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['enable_DLOG']) {
251 Utility\GeneralUtility::devLog('DataSubmissionController: ' . Utility\GeneralUtility::arrayToLogString($this->dirtyHeaders, '', 200), 'Core', 3);
252 }
253 }
254 }
255 }
256
257 /**
258 * Checks string for suspicious characters
259 *
260 * @param string $string String to check
261 * @return string Valid or empty string
262 */
263 protected function sanitizeHeaderString($string) {
264 $pattern = '/[\\r\\n\\f\\e]/';
265 if (preg_match($pattern, $string) > 0) {
266 $this->dirtyHeaders[] = $string;
267 $string = '';
268 }
269 return $string;
270 }
271
272 /**
273 * Parses mailbox headers and turns them into an array.
274 *
275 * Mailbox headers are a comma separated list of 'name <email@example.org' combinations or plain email addresses (or a mix
276 * of these).
277 * The resulting array has key-value pairs where the key is either a number (no display name in the mailbox header) and the
278 * value is the email address, or the key is the email address and the value is the display name.
279 *
280 * @param string $rawAddresses Comma separated list of email addresses (optionally with display name)
281 * @return array Parsed list of addresses.
282 */
283 protected function parseAddresses($rawAddresses = '') {
284 /** @var $addressParser \TYPO3\CMS\Core\Mail\Rfc822AddressesParser */
285 $addressParser = Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\Rfc822AddressesParser::class, $rawAddresses);
286 $addresses = $addressParser->parseAddressList();
287 $addressList = array();
288 foreach ($addresses as $address) {
289 if ($address->personal) {
290 // Item with name found ( name <email@example.org> )
291 $addressList[$address->mailbox . '@' . $address->host] = $address->personal;
292 } else {
293 // Item without name found ( email@example.org )
294 $addressList[] = $address->mailbox . '@' . $address->host;
295 }
296 }
297 return $addressList;
298 }
299
300 /**
301 * Sends the actual mail and handles autorespond message
302 *
303 * @return bool
304 */
305 public function sendTheMail() {
306 // Sending the mail requires the recipient and message to be set.
307 if (!$this->mailMessage->getTo() || !trim($this->mailMessage->getBody())) {
308 return FALSE;
309 }
310 $this->mailMessage->send();
311 // Auto response
312 if ($this->autoRespondMessage) {
313 $theParts = explode('/', $this->autoRespondMessage, 2);
314 $theParts[0] = str_replace('###SUBJECT###', $this->subject, $theParts[0]);
315 $theParts[1] = str_replace(
316 array('/', '###MESSAGE###'),
317 array(LF, $this->plainContent),
318 $theParts[1]
319 );
320 /** @var $autoRespondMail \TYPO3\CMS\Core\Mail\MailMessage */
321 $autoRespondMail = Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
322 $autoRespondMail->setTo($this->fromAddress)->setSubject($theParts[0])->setFrom($this->recipient)->setBody($theParts[1]);
323 $autoRespondMail->send();
324 }
325 return $this->mailMessage->isSent();
326 }
327
328 /**
329 * Do some cleanup at the end (deleting attachment files)
330 */
331 public function __destruct() {
332 foreach ($this->temporaryFiles as $file) {
333 if (Utility\GeneralUtility::isAllowedAbsPath($file) && Utility\GeneralUtility::isFirstPartOfStr($file, PATH_site . 'typo3temp/upload_temp_')) {
334 Utility\GeneralUtility::unlink_tempfile($file);
335 }
336 }
337 }
338
339 }