[!!!][TASK] Deprecate "device" TypoScript condition
[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 int
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 bool
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 int $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 int 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 bool $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 bool 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. "[applicationContext = Production][...(other condition)...]"
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,device,version,system,useragent', strtolower($key))) {
198 GeneralUtility::deprecationLog(
199 'Usage of client related conditions (browser, device, version, system, useragent) is deprecated since 7.0.'
200 );
201 $browserInfo = $this->getBrowserInfo(GeneralUtility::getIndpEnv('HTTP_USER_AGENT'));
202 }
203 $keyParts = GeneralUtility::trimExplode('|', $key);
204 switch ($keyParts[0]) {
205 case 'applicationContext':
206 $values = GeneralUtility::trimExplode(',', $value, TRUE);
207 $currentApplicationContext = GeneralUtility::getApplicationContext();
208 foreach ($values as $applicationContext) {
209 if ($this->searchStringWildcard($currentApplicationContext, $applicationContext)) {
210 return TRUE;
211 }
212 }
213 return FALSE;
214 break;
215 case 'browser':
216 $values = GeneralUtility::trimExplode(',', $value, TRUE);
217 // take all identified browsers into account, eg chrome deliver
218 // webkit=>532.5, chrome=>4.1, safari=>532.5
219 // so comparing string will be
220 // "webkit532.5 chrome4.1 safari532.5"
221 $all = '';
222 foreach ($browserInfo['all'] as $key => $value) {
223 $all .= $key . $value . ' ';
224 }
225 foreach ($values as $test) {
226 if (stripos($all, $test) !== FALSE) {
227 return TRUE;
228 }
229 }
230 return FALSE;
231 break;
232 case 'version':
233 $values = GeneralUtility::trimExplode(',', $value, TRUE);
234 foreach ($values as $test) {
235 if (strcspn($test, '=<>') == 0) {
236 switch ($test[0]) {
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 case '>':
248 if (doubleval(substr($test, 1)) < $browserInfo['version']) {
249 return TRUE;
250 }
251 break;
252 }
253 } elseif (strpos(' ' . $browserInfo['version'], $test) == 1) {
254 return TRUE;
255 }
256 }
257 return FALSE;
258 break;
259 case 'system':
260 $values = GeneralUtility::trimExplode(',', $value, TRUE);
261 // Take all identified systems into account, e.g. mac for iOS, Linux
262 // for android and Windows NT for Windows XP
263 $allSystems = ' ' . implode(' ', $browserInfo['all_systems']);
264 foreach ($values as $test) {
265 if (stripos($allSystems, $test) !== FALSE) {
266 return TRUE;
267 }
268 }
269 return FALSE;
270 break;
271 case 'device':
272 if (!isset($this->deviceInfo)) {
273 $this->deviceInfo = $this->getDeviceType(GeneralUtility::getIndpEnv('HTTP_USER_AGENT'));
274 }
275 $values = GeneralUtility::trimExplode(',', $value, TRUE);
276 foreach ($values as $test) {
277 if ($this->deviceInfo == $test) {
278 return TRUE;
279 }
280 }
281 return FALSE;
282 break;
283 case 'useragent':
284 $test = trim($value);
285 if ($test !== '') {
286 return $this->searchStringWildcard((string)$browserInfo['useragent'], $test);
287 } else {
288 return FALSE;
289 }
290 break;
291 case 'language':
292 if (GeneralUtility::getIndpEnv('HTTP_ACCEPT_LANGUAGE') === $value) {
293 return TRUE;
294 }
295 $values = GeneralUtility::trimExplode(',', $value, TRUE);
296 foreach ($values as $test) {
297 if (preg_match('/^\\*.+\\*$/', $test)) {
298 $allLanguages = preg_split('/[,;]/', GeneralUtility::getIndpEnv('HTTP_ACCEPT_LANGUAGE'));
299 if (in_array(substr($test, 1, -1), $allLanguages)) {
300 return TRUE;
301 }
302 } elseif (GeneralUtility::getIndpEnv('HTTP_ACCEPT_LANGUAGE') == $test) {
303 return TRUE;
304 }
305 }
306 return FALSE;
307 break;
308 case 'IP':
309 if ($value === 'devIP') {
310 $value = trim($GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']);
311 }
312
313 return (bool)GeneralUtility::cmpIP(GeneralUtility::getIndpEnv('REMOTE_ADDR'), $value);
314 break;
315 case 'hostname':
316 return (bool)GeneralUtility::cmpFQDN(GeneralUtility::getIndpEnv('REMOTE_ADDR'), $value);
317 break;
318 case 'hour':
319 case 'minute':
320 case 'month':
321 case 'year':
322 case 'dayofweek':
323 case 'dayofmonth':
324 case 'dayofyear':
325 // In order to simulate time properly in templates.
326 $theEvalTime = $GLOBALS['SIM_EXEC_TIME'];
327 switch ($key) {
328 case 'hour':
329 $theTestValue = date('H', $theEvalTime);
330 break;
331 case 'minute':
332 $theTestValue = date('i', $theEvalTime);
333 break;
334 case 'month':
335 $theTestValue = date('m', $theEvalTime);
336 break;
337 case 'year':
338 $theTestValue = date('Y', $theEvalTime);
339 break;
340 case 'dayofweek':
341 $theTestValue = date('w', $theEvalTime);
342 break;
343 case 'dayofmonth':
344 $theTestValue = date('d', $theEvalTime);
345 break;
346 case 'dayofyear':
347 $theTestValue = date('z', $theEvalTime);
348 break;
349 }
350 $theTestValue = (int)$theTestValue;
351 // comp
352 $values = GeneralUtility::trimExplode(',', $value, TRUE);
353 foreach ($values as $test) {
354 if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($test)) {
355 $test = '=' . $test;
356 }
357 if ($this->compareNumber($test, $theTestValue)) {
358 return TRUE;
359 }
360 }
361 return FALSE;
362 break;
363 case 'compatVersion':
364 return GeneralUtility::compat_version($value);
365 break;
366 case 'loginUser':
367 if ($this->isUserLoggedIn()) {
368 $values = GeneralUtility::trimExplode(',', $value, TRUE);
369 foreach ($values as $test) {
370 if ($test == '*' || (string)$this->getUserId() === (string)$test) {
371 return TRUE;
372 }
373 }
374 } elseif ($value === '') {
375 return TRUE;
376 }
377 return FALSE;
378 break;
379 case 'page':
380 if ($keyParts[1]) {
381 $page = $this->getPage();
382 $property = $keyParts[1];
383 if (!empty($page) && isset($page[$property]) && (string)$page[$property] === (string)$value) {
384 return TRUE;
385 }
386 }
387 return FALSE;
388 break;
389 case 'globalVar':
390 $values = GeneralUtility::trimExplode(',', $value, TRUE);
391 foreach ($values as $test) {
392 $point = strcspn($test, '!=<>');
393 $theVarName = substr($test, 0, $point);
394 $nv = $this->getVariable(trim($theVarName));
395 $testValue = substr($test, $point);
396 if ($this->compareNumber($testValue, $nv)) {
397 return TRUE;
398 }
399 }
400 return FALSE;
401 break;
402 case 'globalString':
403 $values = GeneralUtility::trimExplode(',', $value, TRUE);
404 foreach ($values as $test) {
405 $point = strcspn($test, '=');
406 $theVarName = substr($test, 0, $point);
407 $nv = (string)$this->getVariable(trim($theVarName));
408 $testValue = substr($test, $point + 1);
409 if ($this->searchStringWildcard($nv, trim($testValue))) {
410 return TRUE;
411 }
412 }
413 return FALSE;
414 break;
415 case 'userFunc':
416 $matches = array();
417 preg_match_all('/^\s*([^\(\s]+)\s*(?:\((.*)\))?\s*$/', $value, $matches);
418 $funcName = $matches[1][0];
419 $funcValues = $matches[2][0] ? $this->parseUserFuncArguments($matches[2][0]) : array();
420 if (is_callable($funcName) && call_user_func_array($funcName, $funcValues)) {
421 return TRUE;
422 }
423 return FALSE;
424 break;
425 }
426 return NULL;
427 }
428
429 /**
430 * Parses arguments to the userFunc.
431 *
432 * @param string $arguments
433 * @return array
434 */
435 protected function parseUserFuncArguments($arguments) {
436 $result = array();
437 $arguments = trim($arguments);
438 while ($arguments) {
439 if ($arguments[0] === ',') {
440 $result[] = '';
441 $arguments = substr($arguments, 1);
442 } else {
443 $pos = strcspn($arguments, ',\'"');
444 if ($pos == 0) {
445 // We hit a quote of some kind
446 $quote = $arguments[0];
447 $segment = preg_replace('/^(.*?[^\\\])' . $quote . '.*$/', '\1', substr($arguments, 1));
448 $segment = str_replace('\\' . $quote, $quote, $segment);
449 $result[] = $segment;
450 $offset = strpos($arguments, ',', strlen($segment) + 2);
451 if ($offset === FALSE) {
452 $offset = strlen($arguments);
453 }
454 $arguments = substr($arguments, $offset);
455 } else {
456 $result[] = trim(substr($arguments, 0, $pos));
457 $arguments = substr($arguments, $pos + 1);
458 }
459 }
460 $arguments = trim($arguments);
461 };
462 return $result;
463 }
464
465 /**
466 * Get variable common
467 *
468 * @param array $vars
469 * @return mixed Whatever value. If none, then NULL.
470 */
471 protected function getVariableCommon(array $vars) {
472 $value = NULL;
473 if (count($vars) == 1) {
474 $value = $this->getGlobal($vars[0]);
475 } else {
476 $splitAgain = explode('|', $vars[1], 2);
477 $k = trim($splitAgain[0]);
478 if ($k) {
479 switch ((string)trim($vars[0])) {
480 case 'GP':
481 $value = GeneralUtility::_GP($k);
482 break;
483 case 'ENV':
484 $value = getenv($k);
485 break;
486 case 'IENV':
487 $value = GeneralUtility::getIndpEnv($k);
488 break;
489 case 'LIT':
490 return trim($vars[1]);
491 break;
492 default:
493 return NULL;
494 }
495 // If array:
496 if (count($splitAgain) > 1) {
497 if (is_array($value) && trim($splitAgain[1])) {
498 $value = $this->getGlobal($splitAgain[1], $value);
499 } else {
500 $value = '';
501 }
502 }
503 }
504 }
505 return $value;
506 }
507
508 /**
509 * Evaluates a $leftValue based on an operator: "<", ">", "<=", ">=", "!=" or "="
510 *
511 * @param string $test The value to compare with on the form [operator][number]. Eg. "< 123
512 * @param float $leftValue The value on the left side
513 * @return bool If $value is "50" and $test is "< 123" then it will return TRUE.
514 */
515 protected function compareNumber($test, $leftValue) {
516 if (preg_match('/^(!?=+|<=?|>=?)\\s*([^\\s]*)\\s*$/', $test, $matches)) {
517 $operator = $matches[1];
518 $rightValue = $matches[2];
519 switch ($operator) {
520 case '>=':
521 return $leftValue >= doubleval($rightValue);
522 break;
523 case '<=':
524 return $leftValue <= doubleval($rightValue);
525 break;
526 case '!=':
527 // multiple values may be split with '|'
528 // see if none matches ("not in list")
529 $found = FALSE;
530 $rightValueParts = GeneralUtility::trimExplode('|', $rightValue);
531 foreach ($rightValueParts as $rightValueSingle) {
532 if ($leftValue == doubleval($rightValueSingle)) {
533 $found = TRUE;
534 break;
535 }
536 }
537 return $found === FALSE;
538 break;
539 case '<':
540 return $leftValue < doubleval($rightValue);
541 break;
542 case '>':
543 return $leftValue > doubleval($rightValue);
544 break;
545 default:
546 // nothing valid found except '=', use '='
547 // multiple values may be split with '|'
548 // see if one matches ("in list")
549 $found = FALSE;
550 $rightValueParts = GeneralUtility::trimExplode('|', $rightValue);
551 foreach ($rightValueParts as $rightValueSingle) {
552 if ($leftValue == $rightValueSingle) {
553 $found = TRUE;
554 break;
555 }
556 }
557 return $found;
558 }
559 }
560 return FALSE;
561 }
562
563 /**
564 * Matching two strings against each other, supporting a "*" wildcard or (if wrapped in "/") PCRE regular expressions
565 *
566 * @param string $haystack The string in which to find $needle.
567 * @param string $needle The string to find in $haystack
568 * @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.
569 */
570 protected function searchStringWildcard($haystack, $needle) {
571 $result = FALSE;
572 if ($haystack === $needle) {
573 $result = TRUE;
574 } elseif ($needle) {
575 if (preg_match('/^\\/.+\\/$/', $needle)) {
576 // Regular expression, only "//" is allowed as delimiter
577 $regex = $needle;
578 } else {
579 $needle = str_replace(array('*', '?'), array('###MANY###', '###ONE###'), $needle);
580 $regex = '/^' . preg_quote($needle, '/') . '$/';
581 // Replace the marker with .* to match anything (wildcard)
582 $regex = str_replace(array('###MANY###', '###ONE###'), array('.*', '.'), $regex);
583 }
584 $result = (bool)preg_match($regex, $haystack);
585 }
586 return $result;
587 }
588
589 /**
590 * Generates an array with abstracted browser information
591 *
592 * @param string $userAgent The useragent string, \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('HTTP_USER_AGENT')
593 * @return array Contains keys "browser", "version", "system
594 */
595 protected function getBrowserInfo($userAgent) {
596 return \TYPO3\CMS\Core\Utility\ClientUtility::getBrowserInfo($userAgent);
597 }
598
599 /**
600 * Gets a code for a browsing device based on the input useragent string.
601 *
602 * @param string $userAgent The useragent string, \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('HTTP_USER_AGENT')
603 * @return string Code for the specific device type
604 * @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8
605 */
606 protected function getDeviceType($userAgent) {
607 return \TYPO3\CMS\Core\Utility\ClientUtility::getDeviceType($userAgent);
608 }
609
610 /**
611 * Return global variable where the input string $var defines array keys separated by "|"
612 * Example: $var = "HTTP_SERVER_VARS | something" will return the value $GLOBALS['HTTP_SERVER_VARS']['something'] value
613 *
614 * @param string $var Global var key, eg. "HTTP_GET_VAR" or "HTTP_GET_VARS|id" to get the GET parameter "id" back.
615 * @param array $source Alternative array than $GLOBAL to get variables from.
616 * @return mixed Whatever value. If none, then blank string.
617 */
618 protected function getGlobal($var, $source = NULL) {
619 $vars = explode('|', $var);
620 $c = count($vars);
621 $k = trim($vars[0]);
622 $theVar = isset($source) ? $source[$k] : $GLOBALS[$k];
623 for ($a = 1; $a < $c; $a++) {
624 if (!isset($theVar)) {
625 break;
626 }
627 $key = trim($vars[$a]);
628 if (is_object($theVar)) {
629 $theVar = $theVar->{$key};
630 } elseif (is_array($theVar)) {
631 $theVar = $theVar[$key];
632 } else {
633 return '';
634 }
635 }
636 if (!is_array($theVar) && !is_object($theVar)) {
637 return $theVar;
638 } else {
639 return '';
640 }
641 }
642
643 /**
644 * Evaluates a TypoScript condition given as input, eg. "[browser=net][...(other conditions)...]"
645 *
646 * @param string $string The condition to match against its criterias.
647 * @return bool Whether the condition matched
648 * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::parse()
649 */
650 abstract protected function evaluateCondition($string);
651
652 /**
653 * Gets the value of a variable.
654 *
655 * Examples of names:
656 * + TSFE:id
657 * + GP:firstLevel|secondLevel
658 * + _GET|firstLevel|secondLevel
659 * + LIT:someLiteralValue
660 *
661 * @param string $name The name of the variable to fetch the value from
662 * @return mixed The value of the given variable (string) or NULL if variable did not exist
663 */
664 abstract protected function getVariable($name);
665
666 /**
667 * Gets the usergroup list of the current user.
668 *
669 * @return string The usergroup list of the current user
670 */
671 abstract protected function getGroupList();
672
673 /**
674 * Determines the current page Id.
675 *
676 * @return int The current page Id
677 */
678 abstract protected function determinePageId();
679
680 /**
681 * Gets the properties for the current page.
682 *
683 * @return array The properties for the current page.
684 */
685 abstract protected function getPage();
686
687 /**
688 * Determines the rootline for the current page.
689 *
690 * @return array The rootline for the current page.
691 */
692 abstract protected function determineRootline();
693
694 /**
695 * Gets the id of the current user.
696 *
697 * @return int The id of the current user
698 */
699 abstract protected function getUserId();
700
701 /**
702 * Determines if a user is logged in.
703 *
704 * @return bool Determines if a user is logged in
705 */
706 abstract protected function isUserLoggedIn();
707
708 /**
709 * Sets a log message.
710 *
711 * @param string $message The log message to set/write
712 * @return void
713 */
714 abstract protected function log($message);
715
716 }