[SECURITY] Fix cross-Site Scripting vulnerability
[TYPO3CMS/Extensions/pagenotfoundhandling.git] / Classes / Controller / PagenotfoundController.php
1 <?php
2 /**
3 * **************************************************************
4 * Copyright notice
5 *
6 * (c) 2014 Agentur am Wasser | Maeder & Partner AG
7 * All rights reserved
8 *
9 * This script is part of the TYPO3 project. The TYPO3 project is
10 * free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
14 *
15 * The GNU General Public License can be found at
16 * http://www.gnu.org/copyleft/gpl.html.
17 *
18 * This script is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * This copyright notice MUST APPEAR in all copies of the script!
24 * **************************************************************
25 *
26 * @author Agentur am Wasser | Maeder & Partner AG <development@agenturamwasser.ch>
27 * @copyright Copyright (c) 2014 Agentur am Wasser | Maeder & Partner AG (http://www.agenturamwasser.ch)
28 * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
29 * @category TYPO3
30 * @package pagenotfoundhandling
31 * @version $Id$
32 */
33
34 /**
35 * 404 handling controller
36 *
37 * @author Agentur am Wasser | Maeder & Partner AG <development@agenturamwasser.ch>
38 * @category TYPO3
39 * @package pagenotfoundhandling
40 */
41 class Tx_Pagenotfoundhandling_Controller_PagenotfoundController
42 {
43 /**
44 * The params given from tslib_fe pageErrorHandler
45 *
46 * @see tslib_fe
47 * @var array
48 */
49 protected $_params = array();
50
51 /**
52 * Config from constants editor in EM
53 *
54 * @var array
55 */
56 protected $_conf = array();
57
58 /**
59 * Content of $_GET
60 *
61 * @var array
62 */
63 protected $_get = array();
64
65 /**
66 * Ignore the language parameter in _GET
67 *
68 * @var boolean
69 */
70 protected $_ignoreLanguage = false;
71
72 /**
73 * Default language parameter in _GET
74 *
75 * @var boolean
76 */
77 protected $_languageParam = 'L';
78
79 /**
80 * Language uid to force using
81 *
82 * @var int
83 */
84 protected $_forceLanguage = 0;
85
86 /**
87 * TYPO3 page to fetch as 404 page
88 *
89 * @var int
90 */
91 protected $_default404Page = 0;
92
93 /**
94 * Template file to render as 404 page
95 *
96 * @var string
97 */
98 protected $_defaultTemplateFile = '';
99
100 /**
101 * Disable the per-domain configuration
102 *
103 * @var boolean
104 */
105 protected $_disableDomainConfig = false;
106
107 /**
108 * Default language key
109 *
110 * @var string
111 */
112 protected $_defaultLanguageKey = 'default';
113
114 /**
115 * Wether the page not found error is because of 'no access' or not
116 *
117 * @var boolean
118 */
119 protected $_isForbiddenError = false;
120
121 /**
122 * HTTP header to be sent for request on restricted pages
123 *
124 * @var string
125 */
126 protected $_forbiddenHeader = '';
127
128 /**
129 * Additional _GET params
130 *
131 * These will be appended to the URL when fetching default404Page
132 *
133 * @var array
134 */
135 protected $_additional404GetParams = array();
136
137 /**
138 * Additional 403 _GET params
139 *
140 * These will be appended to the URL when fetching default404Page and
141 * $_forbiddenError is true
142 *
143 * @var array
144 */
145 protected $_additional403GetParams = array();
146
147 /**
148 * Passthrough for the HTTP header 'Content-Type'
149 *
150 * @var boolean
151 */
152 protected $_passthroughContentTypeHeader = false;
153
154 /**
155 * Send a 'X-Forwarded-For' HTTP header
156 *
157 * @var boolean
158 */
159 protected $_sendXForwardedForHeader = false;
160
161 /**
162 * Addtional HTTP headers to be sent with the 404/403 page
163 *
164 * @var array
165 */
166 protected $_additionalHeaders = array();
167
168 /**
169 * Absolute reference prefix
170 *
171 * Prefixes the URL which fetches the 404 page
172 *
173 * @var string
174 */
175 protected $_absoluteReferencePrefix = '';
176
177 /**
178 * Main method called through tslib_fe::pageErrorHandler()
179 *
180 * @param array $params
181 * @param tslib_fe $tslib_fe
182 * @return string
183 */
184 public function main($params, tslib_fe $tslib_fe)
185 {
186 $this->_get = t3lib_div::_GET();
187
188 // prevent infinite loops
189 if($this->_get['loopPrevention']) {
190 die('Caught infinite loop');
191 }
192
193 $this->_params = $params;
194
195 // check for access errors
196 if(isset($this->_params['pageAccessFailureReasons']['fe_group'])
197 && $this->_params['pageAccessFailureReasons']['fe_group'] !== array('' => 0)) {
198 $this->_isForbiddenError = true;
199 }
200
201 $this->_loadConstantsConfig();
202
203 if($this->_disableDomainConfig !== true) {
204 $this->_loadDomainConfig();
205 }
206
207 // send special HTTP header
208 if($this->_isForbiddenError && !empty($this->_forbiddenHeader)) {
209 header($this->_forbiddenHeader);
210 }
211
212 if(!$this->_ignoreLanguage && empty($this->_forceLanguage)) {
213 $this->_setupLanguage();
214 }
215
216 $return = $this->_getHtml();
217
218 return $return;
219 }
220
221 /**
222 * Replace the available markers with localized content
223 *
224 * Available markers:
225 * - ###TITLE###
226 * - ###MESSAGE###
227 *
228 * @param string $html
229 * @return string
230 */
231 protected function _processMarkers($html)
232 {
233 $lang = $this->_defaultLanguageKey;
234
235 if(t3lib_extMgm::isLoaded('static_info_tables') && !empty($this->_forceLanguage)) {
236 $res = $GLOBALS['TYPO3_DB']->sql_query('
237 SELECT
238 *
239 FROM
240 sys_language
241 LEFT JOIN
242 static_languages
243 ON sys_language.static_lang_isocode=static_languages.uid
244 WHERE
245 sys_language.uid='.$this->_forceLanguage.'
246 LIMIT 1');
247 if(($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res))) {
248 // workaround for english because it has no lg_typo3 but is default language
249 if($row['lg_iso_2'] === 'EN') {
250 $lang = 'default';
251 } elseif(!empty($row['lg_typo3'])) {
252 $lang = $row['lg_typo3'];
253 }
254 }
255 }
256
257 require_once PATH_typo3 . 'sysext/lang/lang.php';
258 $language = t3lib_div::makeInstance('language');
259 $language instanceof language;
260 $language->init($lang);
261 $language->includeLLFile('EXT:pagenotfoundhandling/locallang_404.xml');
262
263 if(!empty($this->_conf['locallangFile'])) {
264 $language->includeLLFile($this->_conf['locallangFile']);
265 }
266
267 $html = str_replace('###TITLE###', $language->getLL('page_title', 1), $html);
268 $html = str_replace('###MESSAGE###', $language->getLL('page_message', 1), $html);
269 $html = str_replace('###REASON_TITLE###', $language->getLL('reason_title', 1), $html);
270 $html = str_replace('###REASON###', htmlspecialchars($this->_params['reasonText']), $html);
271 $html = str_replace('###CURRENT_URL_TITLE###', $language->getLL('current_url_title', 1), $html);
272 $html = str_replace('###CURRENT_URL###', htmlspecialchars($this->_params['currentUrl']), $html);
273 return $html;
274 }
275
276 /**
277 * Setup language from URL
278 *
279 * @return void
280 */
281 protected function _setupLanguage()
282 {
283 $language = (int) $this->_get[$this->_languageParam];
284
285 if($language) {
286 require_once t3lib_extMgm::extPath('pagenotfoundhandling') . 'class.tx_pagenotfoundhandling_LanguageSelect.php';
287 if(array_key_exists($language, tx_pagenotfoundhandling_LanguageSelect::getLanguages(true))) {
288 $this->_forceLanguage = $language;
289 }
290 }
291 }
292
293 /**
294 * Store config from a possible domain record
295 *
296 * @return void
297 */
298 protected function _loadDomainConfig()
299 {
300 $domain = t3lib_div::getIndpEnv('TYPO3_HOST_ONLY');
301 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'sys_domain', 'domainName=\'' . $domain . '\' AND hidden=0');
302
303 if($GLOBALS['TYPO3_DB']->sql_num_rows($res) == 1) {
304 if($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
305 if($row['tx_pagenotfoundhandling_enable']) {
306 $this->_default404Page = (int) $row['tx_pagenotfoundhandling_default404Page'];
307 if ($row['tx_pagenotfoundhandling_defaultTemplateFile']) {
308 $this->_defaultTemplateFile = 'uploads/tx_pagenotfoundhandling/' . $row['tx_pagenotfoundhandling_defaultTemplateFile'];
309 }
310 $this->_ignoreLanguage = (bool) $row['tx_pagenotfoundhandling_ignoreLanguage'];
311 $this->_forceLanguage = (int) $row['tx_pagenotfoundhandling_forceLanguage'];
312 $this->_languageParam = $row['tx_pagenotfoundhandling_languageParam'];
313 $this->_passthroughContentTypeHeader = (bool) $row['tx_pagenotfoundhandling_passthroughContentTypeHeader'];
314 $this->_sendXForwardedForHeader = (bool) $row['tx_pagenotfoundhandling_sendXForwardedForHeader'];
315 $this->_additionalHeaders = \t3lib_div::trimExplode('|', $row['tx_pagenotfoundhandling_additionalHeaders'], true);
316
317 // override 404 page with its 403 equivalent (if needed and configured so)
318 if($this->_isForbiddenError) {
319 $this->_setForbiddenHeader($row['tx_pagenotfoundhandling_default403Header'], false);
320
321 if($row['tx_pagenotfoundhandling_default403Page']) {
322 $this->_default404Page = (int) $row['tx_pagenotfoundhandling_default403Page'];
323 }
324 }
325 }
326 }
327 }
328 }
329
330 /**
331 * Store the values from the constants editor
332 *
333 * @return void
334 */
335 protected function _loadConstantsConfig()
336 {
337 $conf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['pagenotfoundhandling']);
338
339 // store all configuration
340 $this->_conf = $conf;
341
342 if(isset($conf['default404Page'])) {
343 $this->_default404Page = (int) $conf['default404Page'];
344 }
345
346 if(isset($conf['defaultTemplateFile'])) {
347 $this->_defaultTemplateFile = (string) $conf['defaultTemplateFile'];
348 }
349
350 if(isset($conf['additional404GetParams'])) {
351 $this->_addAdditionalGetParams($conf['additional404GetParams']);
352 }
353
354 if(isset($conf['additional403GetParams'])) {
355 $this->_addAdditional403GetParams($conf['additional403GetParams']);
356 }
357
358 if(isset($conf['ignoreLanguage'])) {
359 $this->_ignoreLanguage = (bool) $conf['ignoreLanguage'];
360 }
361
362 if(isset($conf['forceLanguage']) && !empty($conf['forceLanguage'])) {
363 $this->_forceLanguage = (int) $conf['forceLanguage'];
364 }
365
366 if(isset($conf['disableDomainConfig'])) {
367 $this->_disableDomainConfig = (bool) $conf['disableDomainConfig'];
368 }
369
370 if(isset($conf['defaultLanguageKey'])) {
371 $this->_defaultLanguageKey = (string) $conf['defaultLanguageKey'];
372 }
373
374 if(isset($conf['languageParam'])) {
375 $this->_languageParam = $conf['languageParam'];
376 }
377
378 // override 404 page/template with the 403 equivalents (if needed and configured so)
379 if($this->_isForbiddenError) {
380 $this->_setForbiddenHeader($conf['default403Header']);
381
382 if(isset($conf['default403TemplateFile']) && !empty($conf['default403TemplateFile'])) {
383 // reset the 404Page, because the page could come from default404Page and override this templateFile setting
384 $this->_default404Page = 0;
385 $this->_defaultTemplateFile = (string) $conf['default403TemplateFile'];
386 }
387
388 if(isset($conf['default403Page']) && !empty($conf['default403Page'])) {
389 $this->_default404Page = (int) $conf['default403Page'];
390 }
391 }
392
393 if(isset($conf['passthroughContentTypeHeader'])) {
394 $this->_passthroughContentTypeHeader = (bool) $conf['passthroughContentTypeHeader'];
395 }
396
397 if(isset($conf['sendXForwardedForHeader'])) {
398 $this->_sendXForwardedForHeader = (bool) $conf['sendXForwardedForHeader'];
399 }
400
401 if(isset($conf['additionalHeaders'])) {
402 $this->_additionalHeaders = \t3lib_div::trimExplode('|', $conf['additionalHeaders'], true);
403 }
404
405 if(isset($conf['absoluteReferencePrefix'])) {
406 // remove '/' and whitespaces
407 $absoluteReferencePrefix = \trim(\trim($conf['absoluteReferencePrefix'], '/'));
408
409 // check for double dots (..) in the path
410 if (\preg_match('/([\.]|\%2e){2}/i', $absoluteReferencePrefix)) {
411 throw new \InvalidArgumentException('EXT:pagenotfoundhandling: absoluteReferencePrefix must not contain double dots', 1403536458);
412 }
413
414 $this->_absoluteReferencePrefix = $absoluteReferencePrefix;
415 }
416 }
417
418 /**
419 * Returns the content of the 404 page
420 *
421 * @return string
422 */
423 protected function _getHtml()
424 {
425 $html = null;
426 if(isset($this->_default404Page) && !empty($this->_default404Page)) {
427
428 $now = $GLOBALS['SIM_ACCESS_TIME'];
429 $where = 'uid=' . $this->_default404Page . ' AND deleted=0 AND hidden=0 AND (starttime=0 OR starttime =\'\' OR starttime<=' . $now .') AND (endtime=0 OR endtime =\'\' OR endtime>' . $now .')';
430
431 $pageRow = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('*', 'pages', $where);
432 if(count($pageRow) === 1) {
433 $pageRow = current($pageRow);
434 $url = \t3lib_div::getIndpEnv('TYPO3_REQUEST_HOST') . '/';
435
436 if ($this->_absoluteReferencePrefix) {
437 $url .= $this->_absoluteReferencePrefix . '/';
438 }
439
440 $url .= 'index.php?id=' . $this->_default404Page . '&loopPrevention=1';
441
442 if(!empty($this->_forceLanguage)) {
443 $url .= '&' . $this->_languageParam . '=' . $this->_forceLanguage;
444 }
445
446 if($this->_isForbiddenError) {
447 if(count($this->_additional403GetParams)) {
448 $url .= '&' . implode('&', $this->_additional403GetParams);
449 }
450 } else {
451 if(count($this->_additional404GetParams)) {
452 $url .= '&' . implode('&', $this->_additional404GetParams);
453 }
454 }
455
456 $url = str_replace('###CURRENT_URL###', urlencode($this->_params['currentUrl']), $url);
457
458 $headers = array(
459 'User-agent: ' . t3lib_div::getIndpEnv('HTTP_USER_AGENT'),
460 'Referer: ' . t3lib_div::getIndpEnv('TYPO3_REQUEST_URL')
461 );
462
463 if ($this->_sendXForwardedForHeader) {
464 $headers[] = 'X-Forwarded-For: ' . t3lib_div::getIndpEnv('REMOTE_ADDR');
465 }
466
467 $report = array();
468 $html = t3lib_div::getURL($url, (int) $this->_passthroughContentTypeHeader, $headers, $report);
469 if ($this->_passthroughContentTypeHeader && $html !== null) {
470 // split response header and body
471 list ($responseHeaders, $html) = t3lib_div::trimExplode(CRLF . CRLF, $html, false, 2);
472
473 // content-type passthrough
474 if (array_key_exists('content_type', $report) && strlen($report['content_type'])) {
475 header('Content-Type: ' . $report['content_type']);
476 }
477 }
478 }
479 }
480 if($html === null && !empty($this->_defaultTemplateFile)) {
481 $file = t3lib_div::getFileAbsFileName($this->_defaultTemplateFile);
482
483 $oldTemplateFilePath = t3lib_extMgm::extPath('pagenotfoundhandling') . 'res/defaultTemplate.tmpl';
484 if (\strcmp($file, $oldTemplateFilePath) === 0) {
485 t3lib_div::deprecationLog('pagenotfoundhandling: old defaultTemplate file (EXT:pagenotfoundhandling/res/defaultTemplate.tmpl) in use, deprecated since 2.0, will be removed in 2.2, use EXT:pagenotfoundhandling/Resources/Private/Templates/default.html instead');
486 }
487
488 if(!empty($file) && is_readable($file)) {
489 $html = file_get_contents($file);
490 }
491 }
492
493 // send additional HTTP headers
494 if (count($this->_additionalHeaders)) {
495 // disallow sending 'Location' header (redirecting)
496 foreach ($this->_additionalHeaders as $header) {
497 if (!preg_match('/^Location:/i', $header, $matches)) {
498 header($header);
499 }
500 }
501 }
502
503 if(!is_null($html)) {
504 return $this->_processMarkers($html);
505 }
506
507 return $this->_processMarkers('<?xml version="1.0" encoding="utf-8"?>
508 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
509 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
510
511 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
512 <head>
513 <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
514 <title>###TITLE###</title>
515 </head>
516 <body>
517 <div id="page">
518 <h1>###TITLE###</h1>
519 <p>###MESSAGE###</p>
520 </div>
521 </body>
522 </html>');
523 }
524
525 /**
526 * Sets the forbiddenHeader to the accurate value
527 *
528 * @param int $number
529 * @return void
530 */
531 protected function _setForbiddenHeader($number, $overrideIfEmpty = true)
532 {
533 switch((int) $number) {
534 case -1:
535 $this->_forbiddenHeader = '';
536 break;
537 case 1:
538 $this->_forbiddenHeader = t3lib_utility_Http::HTTP_STATUS_400;
539 break;
540 case 2:
541 $this->_forbiddenHeader = t3lib_utility_Http::HTTP_STATUS_401;
542 break;
543 case 3:
544 $this->_forbiddenHeader = t3lib_utility_Http::HTTP_STATUS_402;
545 break;
546 case 4:
547 $this->_forbiddenHeader = t3lib_utility_Http::HTTP_STATUS_403;
548 break;
549 default :
550 if($overrideIfEmpty) {
551 $this->_forbiddenHeader = '';
552 }
553 break;
554 }
555 }
556
557 /**
558 * Add additional _GET params
559 *
560 * @param string $params (works like additionalParams in typolink)
561 * @return void
562 */
563 protected function _addAdditionalGetParams($params)
564 {
565 $params = $this->_normalizeGetParams($params);
566 $this->_additional404GetParams = array_merge($this->_additional404GetParams, t3lib_div::trimExplode('&', $params, true));
567 }
568
569 /**
570 * Add additional 403 _GET params
571 *
572 * @param string $params (works like additionalParams in typolink)
573 * @return void
574 */
575 protected function _addAdditional403GetParams($params)
576 {
577 $params = $this->_normalizeGetParams($params);
578 $this->_additional403GetParams = array_merge($this->_additional403GetParams, t3lib_div::trimExplode('&', $params, true));
579 }
580
581 /**
582 * Normalize the _GET params for using in the page_fetching of _getHtml()
583 *
584 * @param string $params
585 * @return void
586 */
587 protected function _normalizeGetParams($params)
588 {
589 // strip out params that will be generated in _getHtml()
590 return preg_replace('/&?(id|loopPrevention|' . $this->_languageParam . ')=[^&]*/', '', $params);
591 }
592 }