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