contentDataProcessor = GeneralUtility::makeInstance(ContentDataProcessor::class); } /** * Fetches records from the database as an array * * @param ContentObjectRenderer $cObj The data of the content element or page * @param array $contentObjectConfiguration The configuration of Content Object * @param array $processorConfiguration The configuration of this processor * @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View) * * @return array the processed data as key/value store */ public function process( ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData ) { $this->cObj = $cObj; if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) { return $processedData; } // the table to query, if none given, exit $tableName = $cObj->stdWrapValue('table', $processorConfiguration); if (empty($tableName)) { return $processedData; } if (isset($processorConfiguration['table.'])) { unset($processorConfiguration['table.']); } if (isset($processorConfiguration['table'])) { unset($processorConfiguration['table']); } // The variable to be used within the result $targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'records'); // Execute a SQL statement to fetch the records $records = $this->getRecords($tableName, $processorConfiguration); $request = $cObj->getRequest(); $processedRecordVariables = []; foreach ($records as $key => $record) { $recordContentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class); $recordContentObjectRenderer->start($record, $tableName, $request); $processedRecordVariables[$key] = ['data' => $record]; $processedRecordVariables[$key] = $this->contentDataProcessor->process( $recordContentObjectRenderer, $processorConfiguration, $processedRecordVariables[$key] ); } $processedData[$targetVariableName] = $processedRecordVariables; return $processedData; } protected function getRecords(string $tableName, array $queryConfiguration): array { $records = []; $statement = $this->exec_getQuery($tableName, $queryConfiguration); $tsfe = $this->getTypoScriptFrontendController(); while ($row = $statement->fetchAssociative()) { // Versioning preview: $tsfe->sys_page->versionOL($tableName, $row, true); // Language overlay: if (is_array($row)) { $row = $tsfe->sys_page->getLanguageOverlay($tableName, $row); } // Might be unset in the language overlay if (is_array($row)) { $records[] = $row; } } return $records; } protected function exec_getQuery(string $table, array $conf): Result { $statement = $this->getQuery($table, $conf); $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); return $connection->executeQuery($statement); } public function getQuery($table, $conf, $returnQueryArray = false) { // Resolve stdWrap in these properties first $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); $properties = [ 'pidInList', 'uidInList', 'languageField', 'selectFields', 'max', 'begin', 'groupBy', 'orderBy', 'join', 'leftjoin', 'rightjoin', 'recursive', 'where', ]; foreach ($properties as $property) { $conf[$property] = trim( isset($conf[$property . '.']) ? (string)$this->cObj->stdWrap($conf[$property] ?? '', $conf[$property . '.'] ?? []) : (string)($conf[$property] ?? '') ); if ($conf[$property] === '') { unset($conf[$property]); } elseif (in_array($property, ['languageField', 'selectFields', 'join', 'leftjoin', 'rightjoin', 'where'], true)) { $conf[$property] = QueryHelper::quoteDatabaseIdentifiers($connection, $conf[$property]); } if (isset($conf[$property . '.'])) { // stdWrapping already done, so remove the sub-array unset($conf[$property . '.']); } } // Handle PDO-style named parameter markers first $queryMarkers = $this->getQueryMarkers($table, $conf); // Replace the markers in the non-stdWrap properties foreach ($queryMarkers as $marker => $markerValue) { $properties = [ 'uidInList', 'selectFields', 'where', 'max', 'begin', 'groupBy', 'orderBy', 'join', 'leftjoin', 'rightjoin', ]; foreach ($properties as $property) { if ($conf[$property] ?? false) { $conf[$property] = str_replace('###' . $marker . '###', (string)$markerValue, $conf[$property]); } } } // Construct WHERE clause: // Handle recursive function for the pidInList if (isset($conf['recursive'])) { $conf['recursive'] = (int)$conf['recursive']; if ($conf['recursive'] > 0) { $pidList = GeneralUtility::trimExplode(',', $conf['pidInList'], true); array_walk($pidList, function (&$storagePid) { if ($storagePid === 'this') { $storagePid = $this->getTypoScriptFrontendController()->id; } }); $expandedPidList = $this->getTypoScriptFrontendController()->sys_page->getPageIdsRecursive($pidList, $conf['recursive']); $conf['pidInList'] = implode(',', $expandedPidList); } } if ((string)($conf['pidInList'] ?? '') === '') { $conf['pidInList'] = 'this'; } $queryParts = $this->getQueryConstraints($table, $conf); $queryBuilder = $connection->createQueryBuilder(); // @todo Check against getQueryConstraints, can probably use FrontendRestrictions // @todo here and remove enableFields there. $queryBuilder->getRestrictions()->removeAll(); $queryBuilder->select('*')->from($table); if ($queryParts['where'] ?? false) { $queryBuilder->where($queryParts['where']); } if ($queryParts['groupBy'] ?? false) { $queryBuilder->groupBy(...$queryParts['groupBy']); } if (is_array($queryParts['orderBy'] ?? false)) { foreach ($queryParts['orderBy'] as $orderBy) { $queryBuilder->addOrderBy(...$orderBy); } } // Fields: if ($conf['selectFields'] ?? false) { $queryBuilder->selectLiteral($this->sanitizeSelectPart($conf['selectFields'], $table)); } // Setting LIMIT: $error = false; if (($conf['max'] ?? false) || ($conf['begin'] ?? false)) { // Finding the total number of records, if used: if (str_contains(strtolower(($conf['begin'] ?? '') . ($conf['max'] ?? '')), 'total')) { $countQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); $countQueryBuilder->getRestrictions()->removeAll(); $countQueryBuilder->count('*') ->from($table) ->where($queryParts['where']); if ($queryParts['groupBy']) { $countQueryBuilder->groupBy(...$queryParts['groupBy']); } try { $count = $countQueryBuilder->executeQuery()->fetchOne(); if (isset($conf['max'])) { $conf['max'] = str_ireplace('total', $count, (string)$conf['max']); } if (isset($conf['begin'])) { $conf['begin'] = str_ireplace('total', $count, (string)$conf['begin']); } } catch (DBALException $e) { $this->getTimeTracker()->setTSlogMessage($e->getPrevious()->getMessage()); $error = true; } } if (!$error) { if (isset($conf['begin']) && $conf['begin'] > 0) { $conf['begin'] = MathUtility::forceIntegerInRange((int)ceil($this->calc($conf['begin'])), 0); $queryBuilder->setFirstResult($conf['begin']); } if (isset($conf['max'])) { $conf['max'] = MathUtility::forceIntegerInRange((int)ceil($this->calc($conf['max'])), 0); $queryBuilder->setMaxResults($conf['max'] ?: 100000); } } } if (!$error) { // Setting up tablejoins: if ($conf['join'] ?? false) { $joinParts = QueryHelper::parseJoin($conf['join']); $queryBuilder->join( $table, $joinParts['tableName'], $joinParts['tableAlias'], $joinParts['joinCondition'] ); } elseif ($conf['leftjoin'] ?? false) { $joinParts = QueryHelper::parseJoin($conf['leftjoin']); $queryBuilder->leftJoin( $table, $joinParts['tableName'], $joinParts['tableAlias'], $joinParts['joinCondition'] ); } elseif ($conf['rightjoin'] ?? false) { $joinParts = QueryHelper::parseJoin($conf['rightjoin']); $queryBuilder->rightJoin( $table, $joinParts['tableName'], $joinParts['tableAlias'], $joinParts['joinCondition'] ); } // Convert the QueryBuilder object into a SQL statement. $query = $queryBuilder->getSQL(); // Replace the markers in the queryParts to handle stdWrap enabled properties foreach ($queryMarkers as $marker => $markerValue) { // @todo Ugly hack that needs to be cleaned up, with the current architecture // @todo for exec_Query / getQuery it's the best we can do. $query = str_replace('###' . $marker . '###', (string)$markerValue, $query); } return $returnQueryArray ? $this->getQueryArray($queryBuilder) : $query; } return ''; } /** * Helper to transform a QueryBuilder object into a queryParts array that can be used * with exec_SELECT_queryArray * * @return array * @throws \RuntimeException */ protected function getQueryArray(QueryBuilder $queryBuilder) { $fromClauses = []; $knownAliases = []; $queryParts = []; // Loop through all FROM clauses foreach ($queryBuilder->getQueryPart('from') as $from) { if ($from['alias'] === null) { $tableSql = $from['table']; $tableReference = $from['table']; } else { $tableSql = $from['table'] . ' ' . $from['alias']; $tableReference = $from['alias']; } $knownAliases[$tableReference] = true; $fromClauses[$tableReference] = $tableSql . $this->getQueryArrayJoinHelper( $tableReference, $queryBuilder->getQueryPart('join'), $knownAliases ); } $queryParts['SELECT'] = implode(', ', $queryBuilder->getQueryPart('select')); $queryParts['FROM'] = implode(', ', $fromClauses); $queryParts['WHERE'] = (string)$queryBuilder->getQueryPart('where') ?: ''; $queryParts['GROUPBY'] = implode(', ', $queryBuilder->getQueryPart('groupBy')); $queryParts['ORDERBY'] = implode(', ', $queryBuilder->getQueryPart('orderBy')); if ($queryBuilder->getFirstResult() > 0) { $queryParts['LIMIT'] = $queryBuilder->getFirstResult() . ',' . $queryBuilder->getMaxResults(); } elseif ($queryBuilder->getMaxResults() > 0) { $queryParts['LIMIT'] = $queryBuilder->getMaxResults(); } return $queryParts; } /** * Helper to transform the QueryBuilder join part into a SQL fragment. * * @throws \RuntimeException */ protected function getQueryArrayJoinHelper(string $fromAlias, array $joinParts, array &$knownAliases): string { $sql = ''; if (isset($joinParts['join'][$fromAlias])) { foreach ($joinParts['join'][$fromAlias] as $join) { if (array_key_exists($join['joinAlias'], $knownAliases)) { throw new \RuntimeException( 'Non unique join alias: "' . $join['joinAlias'] . '" found.', 1472748872 ); } $sql .= ' ' . strtoupper($join['joinType']) . ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias'] . ' ON ' . ((string)$join['joinCondition']); $knownAliases[$join['joinAlias']] = true; } foreach ($joinParts['join'][$fromAlias] as $join) { $sql .= $this->getQueryArrayJoinHelper($join['joinAlias'], $joinParts, $knownAliases); } } return $sql; } /** * Builds list of marker values for handling PDO-like parameter markers in select parts. * Marker values support stdWrap functionality thus allowing a way to use stdWrap functionality in various * properties of 'select' AND prevents SQL-injection problems by quoting and escaping of numeric values, strings, * NULL values and comma separated lists. * * @param string $table Table to select records from * @param array $conf Select part of CONTENT definition * @return array List of values to replace markers with * @internal * @see getQuery() */ public function getQueryMarkers(string $table, array $conf): array { if (!isset($conf['markers.']) || !is_array($conf['markers.'])) { return []; } // Parse markers and prepare their values $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); $markerValues = []; foreach ($conf['markers.'] as $dottedMarker => $dummy) { $marker = rtrim($dottedMarker, '.'); if ($dottedMarker != $marker . '.') { continue; } // Parse definition // todo else value is always null $tempValue = isset($conf['markers.'][$dottedMarker]) ? $this->cObj->stdWrap($conf['markers.'][$dottedMarker]['value'] ?? '', $conf['markers.'][$dottedMarker]) : $conf['markers.'][$dottedMarker]['value']; // Quote/escape if needed if (is_numeric($tempValue)) { if ((int)$tempValue == $tempValue) { // Handle integer $markerValues[$marker] = (int)$tempValue; } else { // Handle float $markerValues[$marker] = (float)$tempValue; } } elseif ($tempValue === null) { // It represents NULL $markerValues[$marker] = 'NULL'; } elseif (!empty($conf['markers.'][$dottedMarker]['commaSeparatedList'])) { // See if it is really a comma separated list of values $explodeValues = GeneralUtility::trimExplode(',', $tempValue); if (count($explodeValues) > 1) { // Handle each element of list separately $tempArray = []; foreach ($explodeValues as $listValue) { if (is_numeric($listValue)) { if ((int)$listValue == $listValue) { $tempArray[] = (int)$listValue; } else { $tempArray[] = (float)$listValue; } } else { // If quoted, remove quotes before // escaping. if (preg_match('/^\'([^\']*)\'$/', $listValue, $matches)) { $listValue = $matches[1]; } elseif (preg_match('/^\\"([^\\"]*)\\"$/', $listValue, $matches)) { $listValue = $matches[1]; } $tempArray[] = $connection->quote($listValue); } } $markerValues[$marker] = implode(',', $tempArray); } else { // Handle remaining values as string $markerValues[$marker] = $connection->quote($tempValue); } } else { // Handle remaining values as string $markerValues[$marker] = $connection->quote($tempValue); } } return $markerValues; } /** * Helper function for getQuery(), creating the WHERE clause of the SELECT query * * @param string $table The table name * @param array $conf The TypoScript configuration properties * @return array Associative array containing the prepared data for WHERE, ORDER BY and GROUP BY fragments * @throws \InvalidArgumentException * @see getQuery() */ protected function getQueryConstraints(string $table, array $conf): array { // Init: $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); $expressionBuilder = $queryBuilder->expr(); $tsfe = $this->getTypoScriptFrontendController(); $constraints = []; $pid_uid_flag = 0; $enableFieldsIgnore = []; $queryParts = [ 'where' => null, 'groupBy' => null, 'orderBy' => null, ]; $isInWorkspace = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'isOffline'); $considerMovePointers = ( $isInWorkspace && $table !== 'pages' && !empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) ); if (trim($conf['uidInList'] ?? '')) { $listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$tsfe->contentPid, $conf['uidInList'])); // If moved records shall be considered, select via t3ver_oid if ($considerMovePointers) { $constraints[] = (string)$expressionBuilder->orX( $expressionBuilder->in($table . '.uid', $listArr), $expressionBuilder->andX( $expressionBuilder->eq( $table . '.t3ver_state', (int)(string)VersionState::cast(VersionState::MOVE_POINTER) ), $expressionBuilder->in($table . '.t3ver_oid', $listArr) ) ); } else { $constraints[] = (string)$expressionBuilder->in($table . '.uid', $listArr); } $pid_uid_flag++; } // Static_* tables are allowed to be fetched from root page if (strpos($table, 'static_') === 0) { $pid_uid_flag++; } if (trim($conf['pidInList'])) { $listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$tsfe->contentPid, $conf['pidInList'])); // Removes all pages which are not visible for the user! $listArr = $this->checkPidArray($listArr); if (GeneralUtility::inList($conf['pidInList'], 'root')) { $listArr[] = 0; } if (GeneralUtility::inList($conf['pidInList'], '-1')) { $listArr[] = -1; $enableFieldsIgnore['pid'] = true; } if (!empty($listArr)) { $constraints[] = $expressionBuilder->in($table . '.pid', array_map('intval', $listArr)); $pid_uid_flag++; } else { // If not uid and not pid then uid is set to 0 - which results in nothing!! $pid_uid_flag = 0; } } // If not uid and not pid then uid is set to 0 - which results in nothing!! if (!$pid_uid_flag && trim($conf['pidInList'] ?? '') != 'ignore') { $constraints[] = $expressionBuilder->eq($table . '.uid', 0); } $where = trim((string)$this->cObj->stdWrapValue('where', $conf ?? [])); if ($where) { $constraints[] = QueryHelper::stripLogicalOperatorPrefix($where); } // Check if the default language should be fetched (= doing overlays), or if only the records of a language should be fetched // but only do this for TCA tables that have languages enabled $languageConstraint = $this->getLanguageRestriction($expressionBuilder, $table, $conf, GeneralUtility::makeInstance(Context::class)); if ($languageConstraint !== null) { $constraints[] = $languageConstraint; } // Enablefields if ($table === 'pages') { $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_hid_del); $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_groupAccess); } else { $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->enableFields($table, -1, $enableFieldsIgnore)); } // MAKE WHERE: if (count($constraints) !== 0) { $queryParts['where'] = $expressionBuilder->andX(...$constraints); } // GROUP BY $groupBy = trim((string)$this->cObj->stdWrapValue('groupBy', $conf ?? [])); if ($groupBy) { $queryParts['groupBy'] = QueryHelper::parseGroupBy($groupBy); } // ORDER BY $orderByString = trim((string)$this->cObj->stdWrapValue('orderBy', $conf ?? [])); if ($orderByString) { $queryParts['orderBy'] = QueryHelper::parseOrderBy($orderByString); } // Return result: return $queryParts; } /** * Removes Page UID numbers from the input array which are not available due to enableFields() or the list of bad doktype numbers ($this->checkPid_badDoktypeList) * * @param int[] $pageIds Array of Page UID numbers for select and for which pages with enablefields and bad doktypes should be removed. * @return array Returns the array of remaining page UID numbers * @internal */ public function checkPidArray(array $pageIds): array { if (empty($pageIds)) { return []; } $restrictionContainer = GeneralUtility::makeInstance(FrontendRestrictionContainer::class); $restrictionContainer->add(GeneralUtility::makeInstance( DocumentTypeExclusionRestriction::class, GeneralUtility::intExplode(',', (string)$this->cObj->checkPid_badDoktypeList, true) )); return $this->getTypoScriptFrontendController()->sys_page->filterAccessiblePageIds($pageIds, $restrictionContainer); } /** * Adds parts to the WHERE clause that are related to language. * This only works on TCA tables which have the [ctrl][languageField] field set or if they * have select.languageField = my_language_field set explicitly. * * It is also possible to disable the language restriction for a query by using select.languageField = 0, * if select.languageField is not explicitly set, the TCA default values are taken. * * If the table is "localizeable" (= any of the criteria above is met), then the DB query is restricted: * * If the current language aspect has overlays enabled, then the only records with language "0" or "-1" are * fetched (the overlays are taken care of later-on). * if the current language has overlays but also records without localization-parent (free mode) available, * then these are fetched as well. This can explicitly set via select.includeRecordsWithoutDefaultTranslation = 1 * which overrules the overlayType within the language aspect. * * If the language aspect has NO overlays enabled, it behaves as in "free mode" (= only fetch the records * for the current language. * * @param ExpressionBuilder $expressionBuilder * @param string $table * @param array $conf * @param Context $context * @return string|\TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression|null * @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException */ protected function getLanguageRestriction(ExpressionBuilder $expressionBuilder, string $table, array $conf, Context $context) { $languageField = ''; $localizationParentField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null; // Check if the table is translatable, and set the language field by default from the TCA information if (!empty($conf['languageField']) || !isset($conf['languageField'])) { if (isset($conf['languageField']) && !empty($GLOBALS['TCA'][$table]['columns'][$conf['languageField']])) { $languageField = $conf['languageField']; } elseif (!empty($GLOBALS['TCA'][$table]['ctrl']['languageField']) && !empty($localizationParentField)) { $languageField = $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['languageField']; } } // No language restriction enabled explicitly or available via TCA if (empty($languageField)) { return null; } /** @var LanguageAspect $languageAspect */ $languageAspect = $context->getAspect('language'); if ($languageAspect->doOverlays() && !empty($localizationParentField)) { // Sys language content is set to zero/-1 - and it is expected that whatever routine processes the output will // OVERLAY the records with localized versions! $languageQuery = $expressionBuilder->in($languageField, [0, -1]); // Use this option to include records that don't have a default language counterpart ("free mode") // (originalpointerfield is 0 and the language field contains the requested language) if (isset($conf['includeRecordsWithoutDefaultTranslation']) || !empty($conf['includeRecordsWithoutDefaultTranslation.'])) { $includeRecordsWithoutDefaultTranslation = isset($conf['includeRecordsWithoutDefaultTranslation.']) ? $this->cObj->stdWrap($conf['includeRecordsWithoutDefaultTranslation'], $conf['includeRecordsWithoutDefaultTranslation.']) : $conf['includeRecordsWithoutDefaultTranslation']; $includeRecordsWithoutDefaultTranslation = trim($includeRecordsWithoutDefaultTranslation) !== ''; } else { // Option was not explicitly set, check what's in for the language overlay type. $includeRecordsWithoutDefaultTranslation = $languageAspect->getOverlayType() === $languageAspect::OVERLAYS_ON_WITH_FLOATING; } if ($includeRecordsWithoutDefaultTranslation) { $languageQuery = $expressionBuilder->orX( $languageQuery, $expressionBuilder->andX( $expressionBuilder->eq($table . '.' . $localizationParentField, 0), $expressionBuilder->eq($languageField, $languageAspect->getContentId()) ) ); } return $languageQuery; } // No overlays = only fetch records given for the requested language and "all languages" return $expressionBuilder->in($languageField, [$languageAspect->getContentId(), -1]); } /** * Helper function for getQuery, sanitizing the select part * * This functions checks if the necessary fields are part of the select * and adds them if necessary. * * @param string $selectPart Select part * @param string $table Table to select from * @return string Sanitized select part * @internal * @see getQuery */ protected function sanitizeSelectPart(string $selectPart, string $table): string { $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); // Pattern matching parts $matchStart = '/(^\\s*|,\\s*|' . $table . '\\.)'; $matchEnd = '(\\s*,|\\s*$)/'; $necessaryFields = ['uid', 'pid']; $wsFields = ['t3ver_state']; if (isset($GLOBALS['TCA'][$table]) && !preg_match($matchStart . '\\*' . $matchEnd, $selectPart) && !preg_match('/(count|max|min|avg|sum)\\([^\\)]+\\)|distinct/i', $selectPart)) { foreach ($necessaryFields as $field) { $match = $matchStart . $field . $matchEnd; if (!preg_match($match, $selectPart)) { $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field); } } if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ?? false) { foreach ($wsFields as $field) { $match = $matchStart . $field . $matchEnd; if (!preg_match($match, $selectPart)) { $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field); } } } } return $selectPart; } /** * Performs basic mathematical evaluation of the input string. Does NOT take parenthesis and operator precedence into account! (for that, see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithPriorityToAdditionAndSubtraction()) * * @param string $val The string to evaluate. Example: "3+4*10/5" will generate "35". Only integer numbers can be used. * @return int The result (might be a float if you did a division of the numbers). * @see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithPriorityToAdditionAndSubtraction() */ public function calc($val) { $parts = GeneralUtility::splitCalc($val, '+-*/'); $value = 0; foreach ($parts as $part) { $theVal = $part[1]; $sign = $part[0]; if ((string)(int)$theVal === (string)$theVal) { $theVal = (int)$theVal; } else { $theVal = 0; } if ($sign === '-') { $value -= $theVal; } if ($sign === '+') { $value += $theVal; } if ($sign === '/') { if ((int)$theVal) { $value /= (int)$theVal; } } if ($sign === '*') { $value *= $theVal; } } return $value; } /** * @return TimeTracker */ protected function getTimeTracker() { return GeneralUtility::makeInstance(TimeTracker::class); } /** * Returns the current BE user. * * @return FrontendBackendUserAuthentication */ protected function getFrontendBackendUser() { return $GLOBALS['BE_USER']; } /** * @return TypoScriptFrontendController|null */ protected function getTypoScriptFrontendController() { return $GLOBALS['TSFE'] ?? null; } }