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