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