[BUGFIX] language condition does not match exact string
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Configuration / TypoScript / ConditionMatching / AbstractConditionMatcher.php
1 <?php
2 namespace TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2009-2013 Oliver Hader <oliver@typo3.org>
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 * A copy is found in the text file GPL.txt and important notices to the license
19 * from the author is found in LICENSE.txt distributed with these scripts.
20 *
21 *
22 * This script is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
26 *
27 * This copyright notice MUST APPEAR in all copies of the script!
28 ***************************************************************/
29
30 use TYPO3\CMS\Core\Utility\GeneralUtility;
31
32 /**
33 * Matching TypoScript conditions
34 *
35 * Used with the TypoScript parser.
36 * Matches browserinfo, IPnumbers for use with templates
37 *
38 * @author Oliver Hader <oliver@typo3.org>
39 */
40 abstract class AbstractConditionMatcher {
41
42 /**
43 * Id of the current page.
44 *
45 * @var integer
46 */
47 protected $pageId;
48
49 /**
50 * The rootline for the current page.
51 *
52 * @var array
53 */
54 protected $rootline;
55
56 /**
57 * Whether to simulate the behaviour and match all conditions
58 * (used in TypoScript object browser).
59 *
60 * @var boolean
61 */
62 protected $simulateMatchResult = FALSE;
63
64 /**
65 * Whether to simulat the behaviour and match specific conditions
66 * (used in TypoScript object browser).
67 *
68 * @var array
69 */
70 protected $simulateMatchConditions = array();
71
72 /**
73 * Sets the id of the page to evaluate conditions for.
74 *
75 * @param integer $pageId Id of the page (must be positive)
76 * @return void
77 */
78 public function setPageId($pageId) {
79 if (is_integer($pageId) && $pageId > 0) {
80 $this->pageId = $pageId;
81 }
82 }
83
84 /**
85 * Gets the id of the page to evaluate conditions for.
86 *
87 * @return integer Id of the page
88 */
89 public function getPageId() {
90 return $this->pageId;
91 }
92
93 /**
94 * Sets the rootline.
95 *
96 * @param array $rootline The rootline to be used for matching (must have elements)
97 * @return void
98 */
99 public function setRootline(array $rootline) {
100 if (count($rootline)) {
101 $this->rootline = $rootline;
102 }
103 }
104
105 /**
106 * Gets the rootline.
107 *
108 * @return array The rootline to be used for matching
109 */
110 public function getRootline() {
111 return $this->rootline;
112 }
113
114 /**
115 * Sets whether to simulate the behaviour and match all conditions.
116 *
117 * @param boolean $simulateMatchResult Whether to simulate positive matches
118 * @return void
119 */
120 public function setSimulateMatchResult($simulateMatchResult) {
121 if (is_bool($simulateMatchResult)) {
122 $this->simulateMatchResult = $simulateMatchResult;
123 }
124 }
125
126 /**
127 * Sets whether to simulate the behaviour and match specific conditions.
128 *
129 * @param array $simulateMatchConditions Conditions to simulate a match for
130 * @return void
131 */
132 public function setSimulateMatchConditions(array $simulateMatchConditions) {
133 $this->simulateMatchConditions = $simulateMatchConditions;
134 }
135
136 /**
137 * Normalizes an expression and removes the first and last square bracket.
138 * + OR normalization: "...]OR[...", "...]||[...", "...][..." --> "...]||[..."
139 * + AND normalization: "...]AND[...", "...]&&[..." --> "...]&&[..."
140 *
141 * @param string $expression The expression to be normalized (e.g. "[A] && [B] OR [C]")
142 * @return string The normalized expression (e.g. "[A]&&[B]||[C]")
143 */
144 protected function normalizeExpression($expression) {
145 $normalizedExpression = preg_replace(array(
146 '/\\]\\s*(OR|\\|\\|)?\\s*\\[/i',
147 '/\\]\\s*(AND|&&)\\s*\\[/i'
148 ), array(
149 ']||[',
150 ']&&['
151 ), trim($expression));
152 return $normalizedExpression;
153 }
154
155 /**
156 * Matches a TypoScript condition expression.
157 *
158 * @param string $expression The expression to match
159 * @return boolean Whether the expression matched
160 */
161 public function match($expression) {
162 // Return directly if result should be simulated:
163 if ($this->simulateMatchResult) {
164 return $this->simulateMatchResult;
165 }
166 // Return directly if matching for specific condition is simulated only:
167 if (count($this->simulateMatchConditions)) {
168 return in_array($expression, $this->simulateMatchConditions);
169 }
170 // Sets the current pageId if not defined yet:
171 if (!isset($this->pageId)) {
172 $this->pageId = $this->determinePageId();
173 }
174 // Sets the rootline if not defined yet:
175 if (!isset($this->rootline)) {
176 $this->rootline = $this->determineRootline();
177 }
178 $result = FALSE;
179 $normalizedExpression = $this->normalizeExpression($expression);
180 // First and last character must be square brackets (e.g. "[A]&&[B]":
181 if ($normalizedExpression[0] === '[' && substr($normalizedExpression, -1) === ']') {
182 $innerExpression = substr($normalizedExpression, 1, -1);
183 $orParts = explode(']||[', $innerExpression);
184 foreach ($orParts as $orPart) {
185 $andParts = explode(']&&[', $orPart);
186 foreach ($andParts as $andPart) {
187 $result = $this->evaluateCondition($andPart);
188 // If condition in AND context fails, the whole block is FALSE:
189 if ($result === FALSE) {
190 break;
191 }
192 }
193 // If condition in OR context succeeds, the whole expression is TRUE:
194 if ($result === TRUE) {
195 break;
196 }
197 }
198 }
199 return $result;
200 }
201
202 /**
203 * Evaluates a TypoScript condition given as input, eg. "[browser=net][...(other conditions)...]"
204 *
205 * @param string $key The condition to match against its criterias.
206 * @param string $value
207 * @return NULL|boolean Result of the evaluation; NULL if condition could not be evaluated
208 */
209 protected function evaluateConditionCommon($key, $value) {
210 if (GeneralUtility::inList('browser,version,system,useragent', strtolower($key))) {
211 $browserInfo = $this->getBrowserInfo(GeneralUtility::getIndpEnv('HTTP_USER_AGENT'));
212 }
213 $keyParts = GeneralUtility::trimExplode('|', $key);
214 switch ($keyParts[0]) {
215 case 'applicationContext':
216 $values = GeneralUtility::trimExplode(',', $value, TRUE);
217 $currentApplicationContext = GeneralUtility::getApplicationContext();
218 foreach ($values as $applicationContext) {
219 if ($this->searchStringWildcard($currentApplicationContext, $applicationContext)) {
220 return TRUE;
221 }
222 }
223 break;
224 case 'browser':
225 $values = GeneralUtility::trimExplode(',', $value, TRUE);
226 // take all identified browsers into account, eg chrome deliver
227 // webkit=>532.5, chrome=>4.1, safari=>532.5
228 // so comparing string will be
229 // "webkit532.5 chrome4.1 safari532.5"
230 $all = '';
231 foreach ($browserInfo['all'] as $key => $value) {
232 $all .= $key . $value . ' ';
233 }
234 foreach ($values as $test) {
235 if (stripos($all, $test) !== FALSE) {
236 return TRUE;
237 }
238 }
239 break;
240 case 'version':
241 $values = GeneralUtility::trimExplode(',', $value, TRUE);
242 foreach ($values as $test) {
243 if (strcspn($test, '=<>') == 0) {
244 switch ($test[0]) {
245 case '=':
246 if (doubleval(substr($test, 1)) == $browserInfo['version']) {
247 return TRUE;
248 }
249 break;
250 case '<':
251 if (doubleval(substr($test, 1)) > $browserInfo['version']) {
252 return TRUE;
253 }
254 break;
255 case '>':
256 if (doubleval(substr($test, 1)) < $browserInfo['version']) {
257 return TRUE;
258 }
259 break;
260 }
261 } elseif (strpos(' ' . $browserInfo['version'], $test) == 1) {
262 return TRUE;
263 }
264 }
265 break;
266 case 'system':
267 $values = GeneralUtility::trimExplode(',', $value, TRUE);
268 // Take all identified systems into account, e.g. mac for iOS, Linux
269 // for android and Windows NT for Windows XP
270 $allSystems = ' ' . implode(' ', $browserInfo['all_systems']);
271 foreach ($values as $test) {
272 if (stripos($allSystems, $test) !== FALSE) {
273 return TRUE;
274 }
275 }
276 break;
277 case 'device':
278 if (!isset($this->deviceInfo)) {
279 $this->deviceInfo = $this->getDeviceType(GeneralUtility::getIndpEnv('HTTP_USER_AGENT'));
280 }
281 $values = GeneralUtility::trimExplode(',', $value, TRUE);
282 foreach ($values as $test) {
283 if ($this->deviceInfo == $test) {
284 return TRUE;
285 }
286 }
287 break;
288 case 'useragent':
289 $test = trim($value);
290 if (strlen($test)) {
291 return $this->searchStringWildcard($browserInfo['useragent'], $test);
292 }
293 break;
294 case 'language':
295 if (GeneralUtility::getIndpEnv('HTTP_ACCEPT_LANGUAGE') === $value) {
296 return TRUE;
297 }
298 $values = GeneralUtility::trimExplode(',', $value, TRUE);
299 foreach ($values as $test) {
300 if (preg_match('/^\\*.+\\*$/', $test)) {
301 $allLanguages = preg_split('/[,;]/', GeneralUtility::getIndpEnv('HTTP_ACCEPT_LANGUAGE'));
302 if (in_array(substr($test, 1, -1), $allLanguages)) {
303 return TRUE;
304 }
305 } elseif (GeneralUtility::getIndpEnv('HTTP_ACCEPT_LANGUAGE') == $test) {
306 return TRUE;
307 }
308 }
309 break;
310 case 'IP':
311 if ($value === 'devIP') {
312 $value = trim($GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']);
313 }
314
315 if (GeneralUtility::cmpIP(GeneralUtility::getIndpEnv('REMOTE_ADDR'), $value)) {
316 return TRUE;
317 }
318 break;
319 case 'hostname':
320 if (GeneralUtility::cmpFQDN(GeneralUtility::getIndpEnv('REMOTE_ADDR'), $value)) {
321 return TRUE;
322 }
323 break;
324 case 'hour':
325
326 case 'minute':
327
328 case 'month':
329
330 case 'year':
331
332 case 'dayofweek':
333
334 case 'dayofmonth':
335
336 case 'dayofyear':
337 // In order to simulate time properly in templates.
338 $theEvalTime = $GLOBALS['SIM_EXEC_TIME'];
339 switch ($key) {
340 case 'hour':
341 $theTestValue = date('H', $theEvalTime);
342 break;
343 case 'minute':
344 $theTestValue = date('i', $theEvalTime);
345 break;
346 case 'month':
347 $theTestValue = date('m', $theEvalTime);
348 break;
349 case 'year':
350 $theTestValue = date('Y', $theEvalTime);
351 break;
352 case 'dayofweek':
353 $theTestValue = date('w', $theEvalTime);
354 break;
355 case 'dayofmonth':
356 $theTestValue = date('d', $theEvalTime);
357 break;
358 case 'dayofyear':
359 $theTestValue = date('z', $theEvalTime);
360 break;
361 }
362 $theTestValue = (int)$theTestValue;
363 // comp
364 $values = GeneralUtility::trimExplode(',', $value, TRUE);
365 foreach ($values as $test) {
366 if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($test)) {
367 $test = '=' . $test;
368 }
369 if ($this->compareNumber($test, $theTestValue)) {
370 return TRUE;
371 }
372 }
373 break;
374 case 'compatVersion':
375 return GeneralUtility::compat_version($value);
376 break;
377 case 'loginUser':
378 if ($this->isUserLoggedIn()) {
379 $values = GeneralUtility::trimExplode(',', $value, TRUE);
380 foreach ($values as $test) {
381 if ($test == '*' || (string)$this->getUserId() === (string)$test) {
382 return TRUE;
383 }
384 }
385 } elseif ($value === '') {
386 return TRUE;
387 }
388 break;
389 case 'page':
390 if ($keyParts[1]) {
391 $page = $this->getPage();
392 $property = $keyParts[1];
393 if (!empty($page) && isset($page[$property]) && (string)$page[$property] === (string)$value) {
394 return TRUE;
395 }
396 }
397 break;
398 case 'globalVar':
399 $values = GeneralUtility::trimExplode(',', $value, TRUE);
400 foreach ($values as $test) {
401 $point = strcspn($test, '!=<>');
402 $theVarName = substr($test, 0, $point);
403 $nv = $this->getVariable(trim($theVarName));
404 $testValue = substr($test, $point);
405 if ($this->compareNumber($testValue, $nv)) {
406 return TRUE;
407 }
408 }
409 break;
410 case 'globalString':
411 $values = GeneralUtility::trimExplode(',', $value, TRUE);
412 foreach ($values as $test) {
413 $point = strcspn($test, '=');
414 $theVarName = substr($test, 0, $point);
415 $nv = $this->getVariable(trim($theVarName));
416 $testValue = substr($test, $point + 1);
417 if ($this->searchStringWildcard($nv, trim($testValue))) {
418 return TRUE;
419 }
420 }
421 break;
422 case 'userFunc':
423 $matches = array();
424 preg_match_all('/^\s*([^\(\s]+)\s*(?:\((.*)\))?\s*$/', $value, $matches);
425 $funcName = $matches[1][0];
426 $funcValues = $matches[2][0] ? $this->parseUserFuncArguments($matches[2][0]) : array();
427 if (function_exists($funcName) && call_user_func_array($funcName, $funcValues)) {
428 return TRUE;
429 }
430 break;
431 }
432 return NULL;
433 }
434
435 /**
436 * Parses arguments to the userFunc.
437 *
438 * @param string $arguments
439 * @return array
440 */
441 protected function parseUserFuncArguments($arguments) {
442 $result = array();
443 $arguments = trim($arguments);
444 while ($arguments) {
445 if ($arguments[0] === ',') {
446 $result[] = '';
447 $arguments = substr($arguments, 1);
448 } else {
449 $pos = strcspn($arguments, ',\'"');
450 if ($pos == 0) {
451 // We hit a quote of some kind
452 $quote = $arguments[0];
453 $segment = preg_replace('/^(.*?[^\\\])' . $quote . '.*$/', '\1', substr($arguments, 1));
454 $segment = str_replace('\\' . $quote, $quote, $segment);
455 $result[] = $segment;
456 $offset = strpos($arguments, ',', strlen($segment) + 2);
457 if ($offset === FALSE) {
458 $offset = strlen($arguments);
459 }
460 $arguments = substr($arguments, $offset);
461 } else {
462 $result[] = trim(substr($arguments, 0, $pos));
463 $arguments = substr($arguments, $pos + 1);
464 }
465 }
466 $arguments = trim($arguments);
467 };
468 return $result;
469 }
470
471 /**
472 * Get variable common
473 *
474 * @param array $vars
475 * @return mixed Whatever value. If none, then NULL.
476 */
477 protected function getVariableCommon(array $vars) {
478 $value = NULL;
479 if (count($vars) == 1) {
480 $value = $this->getGlobal($vars[0]);
481 } else {
482 $splitAgain = explode('|', $vars[1], 2);
483 $k = trim($splitAgain[0]);
484 if ($k) {
485 switch ((string) trim($vars[0])) {
486 case 'GP':
487 $value = GeneralUtility::_GP($k);
488 break;
489 case 'ENV':
490 $value = getenv($k);
491 break;
492 case 'IENV':
493 $value = GeneralUtility::getIndpEnv($k);
494 break;
495 case 'LIT':
496 return trim($vars[1]);
497 break;
498 default:
499 return NULL;
500 }
501 // If array:
502 if (count($splitAgain) > 1) {
503 if (is_array($value) && trim($splitAgain[1])) {
504 $value = $this->getGlobal($splitAgain[1], $value);
505 } else {
506 $value = '';
507 }
508 }
509 }
510 }
511 return $value;
512 }
513
514 /**
515 * Evaluates a $leftValue based on an operator: "<", ">", "<=", ">=", "!=" or "="
516 *
517 * @param string $test The value to compare with on the form [operator][number]. Eg. "< 123
518 * @param integer $leftValue The value on the left side
519 * @return boolean If $value is "50" and $test is "< 123" then it will return TRUE.
520 */
521 protected function compareNumber($test, $leftValue) {
522 if (preg_match('/^(!?=+|<=?|>=?)\\s*([^\\s]*)\\s*$/', $test, $matches)) {
523 $operator = $matches[1];
524 $rightValue = $matches[2];
525 switch ($operator) {
526 case '>=':
527 return $leftValue >= doubleval($rightValue);
528 break;
529 case '<=':
530 return $leftValue <= doubleval($rightValue);
531 break;
532 case '!=':
533 // multiple values may be split with '|'
534 // see if none matches ("not in list")
535 $found = FALSE;
536 $rightValueParts = GeneralUtility::trimExplode('|', $rightValue);
537 foreach ($rightValueParts as $rightValueSingle) {
538 if ($leftValue == doubleval($rightValueSingle)) {
539 $found = TRUE;
540 break;
541 }
542 }
543 return $found === FALSE;
544 break;
545 case '<':
546 return $leftValue < doubleval($rightValue);
547 break;
548 case '>':
549 return $leftValue > doubleval($rightValue);
550 break;
551 default:
552 // nothing valid found except '=', use '='
553 // multiple values may be split with '|'
554 // see if one matches ("in list")
555 $found = FALSE;
556 $rightValueParts = GeneralUtility::trimExplode('|', $rightValue);
557 foreach ($rightValueParts as $rightValueSingle) {
558 if ($leftValue == $rightValueSingle) {
559 $found = TRUE;
560 break;
561 }
562 }
563 return $found;
564 }
565 }
566 return FALSE;
567 }
568
569 /**
570 * Matching two strings against each other, supporting a "*" wildcard or (if wrapped in "/") PCRE regular expressions
571 *
572 * @param string $haystack The string in which to find $needle.
573 * @param string $needle The string to find in $haystack
574 * @return boolean Returns TRUE if $needle matches or is found in (according to wildcards) in $haystack. Eg. if $haystack is "Netscape 6.5" and $needle is "Net*" or "Net*ape" then it returns TRUE.
575 */
576 protected function searchStringWildcard($haystack, $needle) {
577 $result = FALSE;
578 if ($haystack === $needle) {
579 $result = TRUE;
580 } elseif ($needle) {
581 if (preg_match('/^\\/.+\\/$/', $needle)) {
582 // Regular expression, only "//" is allowed as delimiter
583 $regex = $needle;
584 } else {
585 $needle = str_replace(array('*', '?'), array('###MANY###', '###ONE###'), $needle);
586 $regex = '/^' . preg_quote($needle, '/') . '$/';
587 // Replace the marker with .* to match anything (wildcard)
588 $regex = str_replace(array('###MANY###', '###ONE###'), array('.*', '.'), $regex);
589 }
590 $result = (bool) preg_match($regex, ((string) $haystack));
591 }
592 return $result;
593 }
594
595 /**
596 * Generates an array with abstracted browser information
597 *
598 * @param string $userAgent The useragent string, \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('HTTP_USER_AGENT')
599 * @return array Contains keys "browser", "version", "system
600 */
601 protected function getBrowserInfo($userAgent) {
602 return \TYPO3\CMS\Core\Utility\ClientUtility::getBrowserInfo($userAgent);
603 }
604
605 /**
606 * Gets a code for a browsing device based on the input useragent string.
607 *
608 * @param string $userAgent The useragent string, \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('HTTP_USER_AGENT')
609 * @return string Code for the specific device type
610 */
611 protected function getDeviceType($userAgent) {
612 return \TYPO3\CMS\Core\Utility\ClientUtility::getDeviceType($userAgent);
613 }
614
615 /**
616 * Return global variable where the input string $var defines array keys separated by "|"
617 * Example: $var = "HTTP_SERVER_VARS | something" will return the value $GLOBALS['HTTP_SERVER_VARS']['something'] value
618 *
619 * @param string $var Global var key, eg. "HTTP_GET_VAR" or "HTTP_GET_VARS|id" to get the GET parameter "id" back.
620 * @param array $source Alternative array than $GLOBAL to get variables from.
621 * @return mixed Whatever value. If none, then blank string.
622 */
623 protected function getGlobal($var, $source = NULL) {
624 $vars = explode('|', $var);
625 $c = count($vars);
626 $k = trim($vars[0]);
627 $theVar = isset($source) ? $source[$k] : $GLOBALS[$k];
628 for ($a = 1; $a < $c; $a++) {
629 if (!isset($theVar)) {
630 break;
631 }
632 $key = trim($vars[$a]);
633 if (is_object($theVar)) {
634 $theVar = $theVar->{$key};
635 } elseif (is_array($theVar)) {
636 $theVar = $theVar[$key];
637 } else {
638 return '';
639 }
640 }
641 if (!is_array($theVar) && !is_object($theVar)) {
642 return $theVar;
643 } else {
644 return '';
645 }
646 }
647
648 /**
649 * Evaluates a TypoScript condition given as input, eg. "[browser=net][...(other conditions)...]"
650 *
651 * @param string $string The condition to match against its criterias.
652 * @return boolean Whether the condition matched
653 * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::parse()
654 */
655 abstract protected function evaluateCondition($string);
656
657 /**
658 * Gets the value of a variable.
659 *
660 * Examples of names:
661 * + TSFE:id
662 * + GP:firstLevel|secondLevel
663 * + _GET|firstLevel|secondLevel
664 * + LIT:someLiteralValue
665 *
666 * @param string $name The name of the variable to fetch the value from
667 * @return mixed The value of the given variable (string) or NULL if variable did not exist
668 */
669 abstract protected function getVariable($name);
670
671 /**
672 * Gets the usergroup list of the current user.
673 *
674 * @return string The usergroup list of the current user
675 */
676 abstract protected function getGroupList();
677
678 /**
679 * Determines the current page Id.
680 *
681 * @return integer The current page Id
682 */
683 abstract protected function determinePageId();
684
685 /**
686 * Gets the properties for the current page.
687 *
688 * @return array The properties for the current page.
689 */
690 abstract protected function getPage();
691
692 /**
693 * Determines the rootline for the current page.
694 *
695 * @return array The rootline for the current page.
696 */
697 abstract protected function determineRootline();
698
699 /**
700 * Gets the id of the current user.
701 *
702 * @return integer The id of the current user
703 */
704 abstract protected function getUserId();
705
706 /**
707 * Determines if a user is logged in.
708 *
709 * @return boolean Determines if a user is logged in
710 */
711 abstract protected function isUserLoggedIn();
712
713 /**
714 * Sets a log message.
715 *
716 * @param string $message The log message to set/write
717 * @return void
718 */
719 abstract protected function log($message);
720
721 }