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