* @author Kasper Skaarhoj * @author Dmitry Dulepov */ /** * Class for translating page ids to/from path strings (Speaking URLs) * * @author Martin Poelstra * @author Kasper Skaarhoj * @author Dmitry Dulepov * @package realurl * @subpackage tx_realurl */ class tx_realurl_advanced { /** @var tx_realurl_apiwrapper */ protected $apiWrapper; /** * t3lib_page object for finding rootline on the fly * * @var t3lib_pageSelect|\TYPO3\CMS\Frontend\Page\PageRepository */ protected $sysPage; /** * Reference to parent object * * @var tx_realurl */ protected $pObj; /** * Class configuration * * @var array $conf */ protected $conf; /** * Configuration for the current domain * * @var array */ protected $extConf; public function __construct() { $this->apiWrapper = tx_realurl_apiwrapper::getInstance(); } /** * Main function, called for both encoding and deconding of URLs. * Based on the "mode" key in the $params array it branches out to either decode or encode functions. * * @param array $params Parameters passed from parent object, "tx_realurl". Some values are passed by reference! (paramKeyValues, pathParts and pObj) * @param tx_realurl $parent Copy of parent object. Not used. * @return mixed Depends on branching. */ public function main(array $params, tx_realurl $parent) { // Setting internal variables $this->pObj = $parent; $this->conf = $params['conf']; $this->extConf = $this->pObj->getConfiguration(); // Branching out based on type $result = false; switch ((string)$params['mode']) { case 'encode': $this->IDtoPagePath($params['paramKeyValues'], $params['pathParts']); $result = NULL; break; case 'decode': $result = $this->pagePathtoID($params['pathParts']); break; } return $result; } /******************************* * * "path" ID-to-URL methods * ******************************/ /** * Retrieve the page path for the given page-id. * If the page is a shortcut to another page, it returns the page path to the shortcutted page. * MP get variables are also encoded with the page id. * * @param array $paramKeyValues GETvar parameters containing eg. "id" key with the page id/alias (passed by reference) * @param array $pathParts Path parts array (passed by reference) * @return void * @see encodeSpURL_pathFromId() */ protected function IDtoPagePath(array &$paramKeyValues, &$pathParts) { $pageId = $paramKeyValues['id']; unset($paramKeyValues['id']); $mpvar = (string)$paramKeyValues['MP']; unset($paramKeyValues['MP']); // Convert a page-alias to a page-id if needed $pageId = $this->resolveAlias($pageId); $pageId = $this->resolveShortcuts($pageId, $mpvar); if ($pageId) { // Set error if applicable. if ($this->isExcludedPage($pageId)) { $this->pObj->setEncodeError(); } else { $lang = $this->getLanguageVar($paramKeyValues); $cachedPagePath = $this->getPagePathFromCache($pageId, $lang, $mpvar); if ($cachedPagePath !== false) { $pagePath = $cachedPagePath; } else { $pagePath = $this->createPagePathAndUpdateURLCache($pageId, $mpvar, $lang, $cachedPagePath); } // Set error if applicable. if ($pagePath === '__ERROR') { $this->pObj->setEncodeError(); } else { $this->mergeWithPathParts($pathParts, $pagePath); } } } } /** * If page id is not numeric, try to resolve it from alias. * * @param int|string $pageId * @return mixed */ private function resolveAlias($pageId) { if (!is_numeric($pageId)) { $pageId = $GLOBALS['TSFE']->sys_page->getPageIdFromAlias($pageId); } return $pageId; } /** * Checks if the page should be excluded from processing. * * @param int $pageId * @return boolean */ protected function isExcludedPage($pageId) { return $this->conf['excludePageIds'] && $this->apiWrapper->inList($this->conf['excludePageIds'], $pageId); } /** * Merges the path with existing path parts and creates an array of path * segments. * * @param array $pathParts * @param string $pagePath * @return void */ protected function mergeWithPathParts(array &$pathParts, $pagePath) { if (strlen($pagePath)) { $pagePathParts = explode('/', $pagePath); $pathParts = array_merge($pathParts, $pagePathParts); } } /** * Resolves shortcuts if necessary and returns the final destination page id. * * @param int $pageId * @param array $mpvar * @return mixed false if not found or int */ protected function resolveShortcuts($pageId, &$mpvar) { $disableGroupAccessCheck = true; $loopCount = 20; // Max 20 shortcuts, to prevent an endless loop while ($pageId > 0 && $loopCount > 0) { $loopCount--; $page = $GLOBALS['TSFE']->sys_page->getPage($pageId, $disableGroupAccessCheck); if (!$page) { $pageId = false; break; } if (!$this->conf['dontResolveShortcuts'] && $page['doktype'] == 4) { // Shortcut $pageId = $this->resolveShortcut($page, $disableGroupAccessCheck, array(), $mpvar); } else { $pageId = $page['uid']; break; } $disableGroupAccessCheck = ($GLOBALS['TSFE']->config['config']['typolinkLinkAccessRestrictedPages'] ? true : false); } return $pageId; } /** * Retireves page path from cache. * * @param int $pageid * @param int $lang * @param string $mpvar * @return mixed Page path (string) or false if not found */ private function getPagePathFromCache($pageid, $lang, $mpvar) { $result = false; if (!$this->conf['disablePathCache']) { /** @noinspection PhpUndefinedMethodInspection */ list($cachedPagePath) = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('pagepath', 'tx_realurl_pathcache', 'page_id=' . intval($pageid) . ' AND language_id=' . intval($lang) . ' AND rootpage_id=' . intval($this->conf['rootpage_id']) . ' AND mpvar=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($mpvar, 'tx_realurl_pathcache') . ' AND expire=0', '', '', 1); if (is_array($cachedPagePath)) { $result = $cachedPagePath['pagepath']; } } return $result; } /** * Creates the path and inserts into the path cache (if enabled). * * @param int $id Page id * @param string $mpvar MP variable string * @param int $lang Language uid * @param string $cachedPagePath If set, then a new entry will be inserted ONLY if it is different from $cachedPagePath * @return string The page path */ protected function createPagePathAndUpdateURLCache($id, $mpvar, $lang, $cachedPagePath = '') { $pagePathRec = $this->getPagePathRec($id, $mpvar, $lang); if (!$pagePathRec) { return '__ERROR'; } $this->updateURLCache($id, $cachedPagePath, $pagePathRec['pagepath'], $pagePathRec['langID'], $pagePathRec['rootpage_id'], $mpvar); return $pagePathRec['pagepath']; } /** * Adds a new entry to the path cache. * * @param int $pageId * @param int $cachedPagePath * @param int $pagePath * @param int $langId * @param int $rootPageId * @param string $mpvar * @return void */ private function updateURLCache($pageId, $cachedPagePath, $pagePath, $langId, $rootPageId, $mpvar) { $canCachePaths = !$this->conf['disablePathCache'] && !$this->pObj->isBEUserLoggedIn(); $newPathDiffers = ((string)$pagePath !== (string)$cachedPagePath); if ($canCachePaths && $newPathDiffers) { /** @noinspection PhpUndefinedMethodInspection */ $cacheCondition = 'page_id=' . intval($pageId) . ' AND language_id=' . intval($langId) . ' AND rootpage_id=' . intval($rootPageId) . ' AND mpvar=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($mpvar, 'tx_realurl_pathcache'); /** @noinspection PhpUndefinedMethodInspection */ $GLOBALS['TYPO3_DB']->sql_query('START TRANSACTION'); $this->removeExpiredPathCacheEntries(); $this->setExpirationOnOldPathCacheEntries($pagePath, $cacheCondition); $this->addNewPagePathEntry($pagePath, $cacheCondition, $pageId, $mpvar, $langId, $rootPageId); /** @noinspection PhpUndefinedMethodInspection */ $GLOBALS['TYPO3_DB']->sql_query('COMMIT'); } } /** * Obtains a page path record. * * @param int $id * @param string $mpvar * @param int $lang * @return mixed array(pagepath,langID,rootpage_id) if successful, false otherwise */ protected function getPagePathRec($id, $mpvar, $lang) { static $IDtoPagePathCache = array(); $cacheKey = $id . '.' . $mpvar . '.' . $lang; if (isset($IDtoPagePathCache[$cacheKey])) { $pagePathRec = $IDtoPagePathCache[$cacheKey]; } else { $pagePathRec = $this->IDtoPagePathThroughOverride($id, $mpvar, $lang); if (!$pagePathRec) { // Build the new page path, in the correct language $pagePathRec = $this->IDtoPagePathSegments($id, $mpvar, $lang); } $IDtoPagePathCache[$cacheKey] = $pagePathRec; } return $pagePathRec; } /** * Checks if the page has a path to override. * * @param int $id * @param string $mpvar * @param int $lang * @return array */ protected function IDtoPagePathThroughOverride($id, /** @noinspection PhpUnusedParameterInspection */ $mpvar, $lang) { $result = false; $page = $this->getPage($id, $lang); if ($page['tx_realurl_pathoverride']) { if ($page['tx_realurl_pathsegment']) { $result = array( 'pagepath' => trim($page['tx_realurl_pathsegment'], '/'), 'langID' => intval($lang), // TODO Might be better to fetch root line here to process mount // points and inner subdomains correctly. 'rootpage_id' => intval($this->conf['rootpage_id']) ); } else { $message = sprintf('Path override is set for page=%d (language=%d) but no segment defined!', $id, $lang); $this->apiWrapper->sysLog($message, 'realurl', 3); $this->pObj->devLog($message, false, 2); } } return $result; } /** * Obtains a page and its translation (if necessary). The reason to use this * function instead of $GLOBALS['TSFE']->sys_page->getPage() is that * $GLOBALS['TSFE']->sys_page->getPage() always applies a language overlay * (even if we have a different language id). * * @param int $pageId * @param int $languageId * @return mixed Page row or false if not found */ protected function getPage($pageId, $languageId) { $condition = 'uid=' . intval($pageId) . $GLOBALS['TSFE']->sys_page->where_hid_del; // Note: we do not use $GLOBALS['TSFE']->sys_page->where_groupAccess here // because we will not come here unless typolinkLinkAccessRestrictedPages // was active in 'config' or 'typolink' /** @noinspection PhpUndefinedMethodInspection */ list($row) = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('*', 'pages', $condition); if (is_array($row) && $languageId > 0) { $row = $GLOBALS['TSFE']->sys_page->getPageOverlay($row, $languageId); } return $row; } /** * Adds a new entry to the path cache or revitalizes existing ones * * @param string $currentPagePath * @param string $pathCacheCondition * @param int $pageId * @param string $mpvar * @param int $langId * @param int $rootPageId * @return void */ protected function addNewPagePathEntry($currentPagePath, $pathCacheCondition, $pageId, $mpvar, $langId, $rootPageId) { /** @noinspection PhpUndefinedMethodInspection */ $condition = $pathCacheCondition . ' AND pagepath=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($currentPagePath, 'tx_realurl_pathcache'); $revitalizationCondition = $condition . ' AND expire<>0'; /** @noinspection PhpUndefinedMethodInspection */ list($revitalizationCount) = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('COUNT(*) AS t', 'tx_realurl_pathcache', $revitalizationCondition); if ($revitalizationCount['t'] > 0) { /** @noinspection PhpUndefinedMethodInspection */ $GLOBALS['TYPO3_DB']->exec_UPDATEquery('tx_realurl_pathcache', $revitalizationCondition, array('expire' => 0)); } else { $createCondition = $condition . ' AND expire=0'; /** @noinspection PhpUndefinedMethodInspection */ list($createCount) = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('COUNT(*) AS t', 'tx_realurl_pathcache', $createCondition); if ($createCount['t'] == 0) { $insertArray = array( 'page_id' => $pageId, 'language_id' => $langId, 'pagepath' => $currentPagePath, 'expire' => 0, 'rootpage_id' => $rootPageId, 'mpvar' => $mpvar ); /** @noinspection PhpUndefinedMethodInspection */ $GLOBALS['TYPO3_DB']->exec_INSERTquery('tx_realurl_pathcache', $insertArray); } } } /** * Sets expiration time for the old path cache entries * * @param string $currentPagePath * @param string $pathCacheCondition * @return void */ protected function setExpirationOnOldPathCacheEntries($currentPagePath, $pathCacheCondition) { $expireDays = (isset($this->conf['expireDays']) ? $this->conf['expireDays'] : 60) * 24 * 3600; /** @noinspection PhpUndefinedMethodInspection */ $condition = $pathCacheCondition . ' AND expire=0 AND pagepath<>' . $GLOBALS['TYPO3_DB']->fullQuoteStr($currentPagePath, 'tx_realurl_pathcache'); /** @noinspection PhpUndefinedMethodInspection */ $GLOBALS['TYPO3_DB']->exec_UPDATEquery('tx_realurl_pathcache', $condition, array( 'expire' => $this->makeExpirationTime($expireDays) ), 'expire' ); } /** * Removes all expired path cache entries * * @return void */ protected function removeExpiredPathCacheEntries() { $lastCleanUpFileName = PATH_site . 'typo3temp/realurl_last_clean_up'; $lastCleanUpTime = @filemtime($lastCleanUpFileName); if ($lastCleanUpTime === false || (time() - $lastCleanUpTime >= 6*60*60)) { touch($lastCleanUpFileName); /** @noinspection PhpUndefinedMethodInspection */ $GLOBALS['TYPO3_DB']->exec_DELETEquery('tx_realurl_pathcache', 'expire>0 AND expire<' . $this->makeExpirationTime()); } } /** * Fetch the page path (in the correct language) * Return it in an array like: * array( * 'pagepath' => 'product_omschrijving/another_page_title/', * 'langID' => '2', * ); * * @param int $id Page ID * @param string $mpvar MP variable string * @param int $langID Language id * @return array The page path etc. */ protected function IDtoPagePathSegments($id, $mpvar, $langID) { $result = false; // Get rootLine for current site (overlaid with any language overlay records). $this->createSysPageIfNecessary(); $this->sysPage->sys_language_uid = $langID; $rootLine = $this->sysPage->getRootLine($id, $mpvar); $numberOfRootlineEntries = count($rootLine); $newRootLine = array(); $rootFound = FALSE; if (!$GLOBALS['TSFE']->tmpl->rootLine) { $GLOBALS['TSFE']->tmpl->start($GLOBALS['TSFE']->rootLine); } // Pass #1 -- check if linking a page in subdomain inside main domain $innerSubDomain = false; for ($i = $numberOfRootlineEntries - 1; $i >= 0; $i--) { if ($rootLine[$i]['is_siteroot']) { $this->pObj->devLog('Found siteroot in the rootline for id=' . $id); $rootFound = true; $innerSubDomain = true; for ( ; $i < $numberOfRootlineEntries; $i++) { $newRootLine[] = $rootLine[$i]; } break; } } if (!$rootFound) { // Pass #2 -- check normal page $this->pObj->devLog('Starting to walk rootline for id=' . $id . ' from index=' . $i, $rootLine); for ($i = 0; $i < $numberOfRootlineEntries; $i++) { if ($GLOBALS['TSFE']->tmpl->rootLine[0]['uid'] == $rootLine[$i]['uid']) { $this->pObj->devLog('Found rootline', array('uid' => $id, 'rootline start pid' => $rootLine[$i]['uid'])); $rootFound = true; for ( ; $i < $numberOfRootlineEntries; $i++) { $newRootLine[] = $rootLine[$i]; } break; } } } if ($rootFound) { // Translate the rootline to a valid path (rootline contains localized titles at this point!) $pagePath = $this->rootLineToPath($newRootLine, $langID); $this->pObj->devLog('Got page path', array('uid' => $id, 'pagepath' => $pagePath)); $rootPageId = $this->conf['rootpage_id']; if ($innerSubDomain) { $parts = parse_url($pagePath); $this->pObj->devLog('$innerSubDomain=true, showing page path parts', $parts); if ($parts['host'] == '') { foreach ($newRootLine as $rl) { /** @noinspection PhpUndefinedMethodInspection */ $rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('domainName', 'sys_domain', 'pid=' . $rl['uid'] . ' AND redirectTo=\'\' AND hidden=0', '', 'sorting'); if (count($rows)) { $domain = $rows[0]['domainName']; $this->pObj->devLog('Found domain', $domain); $rootPageId = $rl['uid']; } } } } $result = array( 'pagepath' => $pagePath, 'langID' => intval($langID), 'rootpage_id' => intval($rootPageId), ); } return $result; } /** * Build a virtual path for a page, like "products/product_1/features/" * The path is language dependant. * There is also a function $TSFE->sysPage->getPathFromRootline, but that one can only be used for a visual * indication of the path in the backend, not for a real page path. * Note also that the for-loop starts with 1 so the first page is stripped off. This is (in most cases) the * root of the website (which is 'handled' by the domainname). * * @param array $rl Rootline array for the current website (rootLine from TSFE->tmpl->rootLine but with modified localization according to language of the URL) * @param int $lang Language identifier (as in sys_languages) * @return string Path for the page, eg. * @see IDtoPagePathSegments() */ protected function rootLineToPath($rl, $lang) { $paths = array(); array_shift($rl); // Ignore the first path, as this is the root of the website $c = count($rl); $stopUsingCache = false; $this->pObj->devLog('rootLineToPath starts searching', array('rootline size' => count($rl))); for ($i = 1; $i <= $c; $i++) { $page = array_shift($rl); // First, check for cached path of this page $cachedPagePath = false; if (!$page['tx_realurl_exclude'] && !$stopUsingCache && !$this->conf['disablePathCache']) { // Using pathq2 index! /** @noinspection PhpUndefinedMethodInspection */ list($cachedPagePath) = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('pagepath', 'tx_realurl_pathcache', 'page_id=' . intval($page['uid']) . ' AND language_id=' . intval($lang) . ' AND rootpage_id=' . intval($this->conf['rootpage_id']) . ' AND mpvar=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($page['_MP_PARAM'], 'tx_realurl_pathcache') . ' AND expire=0', '', '', 1); if (is_array($cachedPagePath)) { $lastPath = implode('/', $paths); $this->pObj->devLog('rootLineToPath found path', $lastPath); if ($cachedPagePath != false && substr($cachedPagePath['pagepath'], 0, strlen($lastPath)) != $lastPath) { // Oops. Cached path does not start from already generated path. // It means that path was mapped from a parallel mount point. // We cannot not rely on cache any more. Stop using it. $cachedPagePath = false; $stopUsingCache = true; $this->pObj->devLog('rootLineToPath stops searching'); } } } // If a cached path was found for the page it will be inserted as the base of the new path, overriding anything build prior to this if ($cachedPagePath) { $paths = array(); $paths[$i] = $cachedPagePath['pagepath']; } else { // Building up the path from page title etc. if (!$page['tx_realurl_exclude'] || count($rl) == 0) { // List of "pages" fields to traverse for a "directory title" in the speaking URL (only from RootLine!!) $segTitleFieldArray = $this->apiWrapper->trimExplode(',', $this->conf['segTitleFieldList'] ? $this->conf['segTitleFieldList'] : TX_REALURL_SEGTITLEFIELDLIST_DEFAULT, 1); $theTitle = ''; foreach ($segTitleFieldArray as $fieldName) { if ($page[$fieldName]) { $theTitle = $page[$fieldName]; break; } } $paths[$i] = $this->encodeTitle($theTitle); } } } return implode('/', $paths); } /******************************* * * URL-to-ID methods * ******************************/ /** * Convert a page path to an ID. * * @param array $pathParts Array of segments from virtual path * @return integer Page ID * @see decodeSpURL_idFromPath() */ protected function pagePathtoID(&$pathParts) { $row = $postVar = false; $copy_pathParts = array(); // If pagePath cache is not disabled, look for entry if (!$this->conf['disablePathCache']) { // Work from outside-in to look up path in cache $postVar = false; $copy_pathParts = $pathParts; $charset = $GLOBALS['TYPO3_CONF_VARS']['BE']['forceCharset'] ? $GLOBALS['TYPO3_CONF_VARS']['BE']['forceCharset'] : $GLOBALS['TSFE']->defaultCharSet; foreach ($copy_pathParts as $key => $value) { $copy_pathParts[$key] = $GLOBALS['TSFE']->csConvObj->conv_case($charset, $value, 'toLower'); } while (count($copy_pathParts)) { // Using pathq1 index! /** @noinspection PhpUndefinedMethodInspection */ list($row) = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows( 'tx_realurl_pathcache.*', 'tx_realurl_pathcache,pages', 'tx_realurl_pathcache.page_id=pages.uid AND pages.deleted=0' . ' AND rootpage_id=' . intval($this->conf['rootpage_id']) . ' AND pagepath=' . $GLOBALS['TYPO3_DB']->fullQuoteStr(implode('/', $copy_pathParts), 'tx_realurl_pathcache'), '', 'expire', '1'); // This lookup does not include language and MP var since those are supposed to be fully reflected in the built url! if (is_array($row)) { break; } // If no row was found, we simply pop off one element of the path and try again until there are no more elements in the array - which means we didn't find a match! $postVar = array_pop($copy_pathParts); } } // It could be that entry point to a page but it is not in the cache. If we popped // any items from path parts, we need to check if they are defined as postSetVars or // fixedPostVars on this page. This does not guarantie 100% success. For example, // if path to page is /hello/world/how/are/you and hello/world found in cache and // there is a postVar 'how' on this page, the check below will not work. But it is still // better than nothing. if ($row && $postVar) { $postVars = $this->pObj->getPostVarSetConfig($row['page_id'], 'postVarSets'); if (!is_array($postVars) || !isset($postVars[$postVar])) { // Check fixed $postVars = $this->pObj->getPostVarSetConfig($row['page_id'], 'fixedPostVars'); if (!is_array($postVars) || !isset($postVars[$postVar])) { // Not a postVar, so page most likely in not in cache. Clear row. // TODO It would be great to update cache in this case but usually TYPO3 is not // complitely initialized at this place. So we do not do it... $row = false; } } } // Process row if found if ($row) { // We found it in the cache // Check for expiration. We can get one of three // 1. expire = 0 // 2. expire <= time() // 3. expire > time() // 1 is permanent, we do not process it. 2 is expired, we look for permanent or non-expired // (in this order!) entry for the same page od and redirect to corresponding path. 3 - same as // 1 but means that entry is going to expire eventually, nothing to do for us yet. if ($row['expire'] > 0) { $this->pObj->devLog('pagePathToId found row', $row); // 'expire' in the query is only for logging // Using pathq2 index! /** @noinspection PhpUndefinedMethodInspection */ list($newEntry) = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('pagepath,expire', 'tx_realurl_pathcache', 'page_id=' . intval($row['page_id']) . ' AND language_id=' . intval($row['language_id']) . ' AND (expire=0 OR expire>' . $row['expire'] . ')', '', 'expire', '1'); $this->pObj->devLog('pagePathToId searched for new entry', $newEntry); // Redirect to new path immediately if it is found if ($newEntry) { // Replace path-segments with new ones $originalDirs = $this->pObj->dirParts; // All original $cp_pathParts = $pathParts; // Popping of pages of original dirs (as many as are remaining in $pathParts) for ($a = 0; $a < count($pathParts); $a++) { array_pop($originalDirs); // Finding all preVars here } for ($a = 0; $a < count($copy_pathParts); $a++) { array_shift($cp_pathParts); // Finding all postVars here } $newPathSegments = explode('/', $newEntry['pagepath']); // Split new pagepath into segments. $newUrlSegments = array_merge($originalDirs, $newPathSegments, $cp_pathParts); // Merge those segments. $this->pObj->appendFilePart($newUrlSegments); $redirectUrl = implode('/', $newUrlSegments); header('HTTP/1.1 301 TYPO3 RealURL Redirect A' . __LINE__); header('Location: ' . $this->apiWrapper->locationHeaderUrl($redirectUrl)); exit(); } $this->pObj->disableDecodeCache = true; // Do not cache this! } // Unshift the number of segments that must have defined the page $cc = count($copy_pathParts); for ($a = 0; $a < $cc; $a++) { array_shift($pathParts); } // Assume we can use this info at first $id = $row['page_id']; $GET_VARS = $row['mpvar'] ? array('MP' => $row['mpvar']) : ''; } else { // Find it list($id, $GET_VARS) = $this->findIDByURL($pathParts); } // Return found ID return array($id, $GET_VARS); } /** * Search recursively for the URL in the page tree and return the ID of the path ("manual" id resolve) * * @param array $urlParts Path parts, passed by reference. * @return array Info array, currently with "id" set to the ID. */ protected function findIDByURL(array &$urlParts) { $id = 0; $GET_VARS = ''; $startPid = $this->getRootPid(); if ($startPid && count($urlParts)) { list($id) = $this->findIDByPathOverride($startPid, $urlParts); if ($id != 0) { $startPid = $id; } list($id, $mpvar) = $this->findIDBySegment($startPid, '', $urlParts); if ($mpvar) { $GET_VARS = array('MP' => $mpvar); } } return array(intval($id), $GET_VARS); } /** * Obtains root page id for the current request. * * @return int */ protected function getRootPid() { if ($this->conf['rootpage_id']) { // Take PID from rootpage_id if any: $startPid = intval($this->conf['rootpage_id']); } else { $startPid = $this->pObj->findRootPageId(); } return intval($startPid); } /** * Attempts to find the page inside the root page that has a path override * that fits into the passed segments. * * @param int $rootPid * @param array $urlParts * @return array Key 0 is pid (or 0), key 2 is empty string */ protected function findIDByPathOverride($rootPid, array &$urlParts) { $pageInfo = array(0, ''); $extraUrlSegments = array(); while (count($urlParts) > 0) { // Search for the path inside the root page $url = implode('/', $urlParts); $pageInfo = $this->findPageByPath($rootPid, $url); if ($pageInfo[0]) { break; } // Not found, try smaller segment array_unshift($extraUrlSegments, array_pop($urlParts)); } $urlParts = $extraUrlSegments; return $pageInfo; } /** * Attempts to find the page inside the root page that has the given path. * * @param int $rootPid * @param string $url * @return array Key 0 is pid (or 0), key 2 is empty string */ protected function findPageByPath($rootPid, $url) { $pages = $this->fetchPagesForPath($url); foreach ($pages as $key => $page) { if (!$this->isAnyChildOf($page['pid'], $rootPid)) { unset($pages[$key]); } } if (count($pages) > 1) { $idList = array(); foreach ($pages as $page) { $idList[] = $page['uid']; } // No need for hsc() because TSFE does that $this->pObj->decodeSpURL_throw404(sprintf( 'Multiple pages exist for path "%s": %s', $url, implode(', ', $idList))); } reset($pages); $page = current($pages); return array($page['uid'], ''); } /** * Checks if the the page is any child of the root page. * * @param int $pid * @param int $rootPid * @return boolean */ protected function isAnyChildOf($pid, $rootPid) { $this->createSysPageIfNecessary(); $rootLine = $this->sysPage->getRootLine($pid); foreach ($rootLine as $page) { if ($page['uid'] == $rootPid) { return true; } } return false; } /** * Fetches a list of pages (uid,pid) for path. The priority of search is: * - pages * - pages_language_overlay * * @param string $url * @return array */ protected function fetchPagesForPath($url) { $pages = array(); $language = intval($this->pObj->getDetectedLanguage()); if ($language > 0) { /** @noinspection PhpUndefinedMethodInspection */ $pagesOverlay = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('t1.pid', 'pages_language_overlay t1, pages t2', 't1.hidden=0 AND t1.deleted=0 AND ' . 't2.hidden=0 AND t2.deleted=0 AND ' . 't1.pid=t2.uid AND ' . 't2.tx_realurl_pathoverride=1 AND ' . 't1.sys_language_uid=' . $language . ' AND ' . 't1.tx_realurl_pathsegment=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($url, 'pages_language_overlay'), '', '', '', 'pid' ); if (count($pagesOverlay) > 0) { /** @noinspection PhpUndefinedMethodInspection */ $pages = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('uid,pid', 'pages', 'hidden=0 AND deleted=0 AND uid IN (' . implode(',', array_keys($pagesOverlay)) . ')', '', '', '', 'uid'); } } // $pages has strings as keys. Therefore array_merge will ensure uniqueness. // Selection from 'pages' table will override selection from // pages_language_overlay. /** @noinspection PhpUndefinedMethodInspection */ $pages2 = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('uid,pid', 'pages', 'hidden=0 AND deleted=0 AND tx_realurl_pathoverride=1 AND tx_realurl_pathsegment=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($url, 'pages'), '', '', '', 'uid'); if (count($pages2)) { $pages = $pages + $pages2; } return $pages; } /** * Recursively search the subpages of $pid for the first part of $urlParts * * @param int $startPid Page id in which to search subpages matching first part of urlParts * @param string $mpvar MP variable string * @param array $urlParts Segments of the virtual path (passed by reference; items removed) * @param array|string $currentIdMp Array with the current pid/mpvar to return if no processing is done. * @param bool $foundUID * @return array With resolved id and $mpvar */ protected function findIDBySegment($startPid, $mpvar, array &$urlParts, $currentIdMp = '', $foundUID = false) { // Creating currentIdMp variable if not set if (!is_array($currentIdMp)) { $currentIdMp = array($startPid, $mpvar, $foundUID); } // No more urlparts? Return what we have. if (count($urlParts) == 0) { return $currentIdMp; } // Get the title we need to find now $segment = array_shift($urlParts); // Perform search list($uid, $row, $exclude, $possibleMatch) = $this->findPageBySegmentAndPid($startPid, $segment); // If a title was found... if ($uid) { return $this->processFoundPage($row, $mpvar, $urlParts, true); } elseif (count($exclude)) { // There were excluded pages, we have to process those! foreach ($exclude as $row) { $urlPartsCopy = $urlParts; array_unshift($urlPartsCopy, $segment); $result = $this->processFoundPage($row, $mpvar, $urlPartsCopy, false); if ($result[2]) { $urlParts = $urlPartsCopy; return $result; } } } // the possible "exclude in URL segment" match must be checked if no other results in // deeper tree branches were found, because we want to access this page also // + Books <-- excluded in URL (= possibleMatch) // - TYPO3 // - ExtJS if (count($possibleMatch) > 0) { return $this->processFoundPage($possibleMatch, $mpvar, $urlParts, true); } // No title, so we reached the end of the id identifying part of the path and now put back the current non-matched title segment before we return the PID array_unshift($urlParts, $segment); return $currentIdMp; } /** * Process title search result. This is executed both when title is found and * when excluded segment is found * * @param array $row Row to process * @param array $mpvar MP var * @param array $urlParts URL segments * @param bool $foundUID * @return array Resolved id and mpvar * @see findPageBySegment() */ protected function processFoundPage($row, $mpvar, array &$urlParts, $foundUID) { $uid = $row['uid']; // Set base currentIdMp for next level $currentIdMp = array( $uid, $mpvar, $foundUID); // Modify values if it was a mount point if (is_array($row['_IS_MOUNTPOINT'])) { $mpvar .= ($mpvar ? ',' : '') . $row['_IS_MOUNTPOINT']['MPvar']; if ($row['_IS_MOUNTPOINT']['overlay']) { $currentIdMp[1] = $mpvar; // Change mpvar for the currentIdMp variable. } else { $uid = $row['_IS_MOUNTPOINT']['mount_pid']; } } // Yep, go search for the next subpage return $this->findIDBySegment($uid, $mpvar, $urlParts, $currentIdMp, $foundUID); } /** * Search for a title in a certain PID * * @param int $searchPid Page id in which to search subpages matching title * @param string $title Title to search for * @return array First entry is uid, second entry is the row selected, including information about the page as a mount point. * @see findPageBySegment() */ protected function findPageBySegmentAndPid($searchPid, $title) { // List of "pages" fields to traverse for a "directory title" in the speaking URL (only from RootLine!!) $segTitleFieldList = $this->conf['segTitleFieldList'] ? $this->conf['segTitleFieldList'] : TX_REALURL_SEGTITLEFIELDLIST_DEFAULT; $selList = $this->apiWrapper->uniqueList('uid,pid,doktype,mount_pid,mount_pid_ol,tx_realurl_exclude,' . $segTitleFieldList); $segTitleFieldArray = $this->apiWrapper->trimExplode(',', $segTitleFieldList, 1); // page select object - used to analyse mount points. $pageRepository = $this->apiWrapper->getPageRepository(); // Build an array with encoded values from the segTitleFieldArray of the subpages // First we find field values from the default language // Pages are selected in menu order and if duplicate titles are found the first takes precedence! $titles = array(); // array(title => uid); $exclude = array(); $uidTrack = array(); /** @noinspection PhpUndefinedMethodInspection */ $result = $GLOBALS['TYPO3_DB']->exec_SELECTquery($selList, 'pages', 'pid=' . intval($searchPid) . ' AND deleted=0 AND doktype<>255', '', 'sorting'); /** @noinspection PhpUndefinedMethodInspection */ while (false != ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($result))) { // Mount points $mount_info = $pageRepository->getMountPointInfo($row['uid'], $row); if (is_array($mount_info)) { // There is a valid mount point. if ($mount_info['overlay']) { // Overlay mode: Substitute WHOLE record /** @noinspection PhpUndefinedMethodInspection */ $result2 = $GLOBALS['TYPO3_DB']->exec_SELECTquery($selList, 'pages', 'uid=' . intval($mount_info['mount_pid']) . ' AND deleted=0 AND doktype<>255'); /** @noinspection PhpUndefinedMethodInspection */ $mp_row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($result2); if (is_array($mp_row)) { $row = $mp_row; } else { unset($row); // If the mount point could not be fetched, unset the row } } $row['_IS_MOUNTPOINT'] = $mount_info; } // Collect titles from selected row if (is_array($row)) { if ($row['tx_realurl_exclude']) { // segment is excluded $exclude[] = $row; } // Process titles. Note that excluded segments are also searched // otherwise they will never be found $uidTrack[$row['uid']] = $row; foreach ($segTitleFieldArray as $fieldName) { if ($row[$fieldName]) { $encodedTitle = $this->encodeTitle($row[$fieldName]); if (!isset($titles[$fieldName][$encodedTitle])) { $titles[$fieldName][$encodedTitle] = $row['uid']; } } } } } /** @noinspection PhpUndefinedMethodInspection */ $GLOBALS['TYPO3_DB']->sql_free_result($result); // We have to search the language overlay too, if: a) the language isn't the default (0), b) if it's not set (-1) $uidTrackKeys = array_keys($uidTrack); $language = $this->pObj->getDetectedLanguage(); if ($language != 0) { foreach ($uidTrackKeys as $l_id) { /** @noinspection PhpUndefinedMethodInspection */ $result = $GLOBALS['TYPO3_DB']->exec_SELECTquery(TX_REALURL_SEGTITLEFIELDLIST_PLO, 'pages_language_overlay', 'pid=' . intval($l_id) . ' AND deleted=0' . ($language > 0 ? ' AND sys_language_uid=' . $language : '') ); /** @noinspection PhpUndefinedMethodInspection */ while (false != ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($result))) { foreach ($segTitleFieldArray as $fieldName) { if ($row[$fieldName]) { $encodedTitle = $this->encodeTitle($row[$fieldName]); if (!isset($titles[$fieldName][$encodedTitle])) { $titles[$fieldName][$encodedTitle] = $l_id; } } } } /** @noinspection PhpUndefinedMethodInspection */ $GLOBALS['TYPO3_DB']->sql_free_result($result); } } // Merge titles $segTitleFieldArray = array_reverse($segTitleFieldArray); // To observe the priority order... $allTitles = array(); foreach ($segTitleFieldArray as $fieldName) { if (is_array($titles[$fieldName])) { $allTitles = $this->apiWrapper->array_merge($allTitles, $titles[$fieldName]); } } // Return $encodedTitle = $this->encodeTitle($title); $possibleMatch = array(); if (isset($allTitles[$encodedTitle])) { if (!$uidTrack[$allTitles[$encodedTitle]]['tx_realurl_exclude']) { return array($allTitles[$encodedTitle], $uidTrack[$allTitles[$encodedTitle]], false, array()); } $possibleMatch = $uidTrack[$allTitles[$encodedTitle]]; } return array(false, false, $exclude, $possibleMatch); } /******************************* * * Helper functions * ******************************/ /** * Convert a title to something that can be used in an page path: * - Convert spaces to underscores * - Convert non A-Z characters to ASCII equivalents * - Convert some special things like the 'ae'-character * - Strip off all other symbols * Works with the character set defined as "forceCharset" * * WARNING!!! The signature or visibility of this function may change at any moment! * * @param string $title Input title to clean * @return string Encoded title, passed through rawurlencode() = ready to put in the URL. * @see rootLineToPath() */ public function encodeTitle($title) { // Fetch character set $charset = $GLOBALS['TYPO3_CONF_VARS']['BE']['forceCharset'] ? $GLOBALS['TYPO3_CONF_VARS']['BE']['forceCharset'] : $GLOBALS['TSFE']->defaultCharSet; // Convert to lowercase $processedTitle = $GLOBALS['TSFE']->csConvObj->conv_case($charset, $title, 'toLower'); // Strip tags $processedTitle = strip_tags($processedTitle); // Convert some special tokens to the space character $space = isset($this->conf['spaceCharacter']) ? $this->conf['spaceCharacter'] : '_'; $processedTitle = preg_replace('/[ \-+_]+/', $space, $processedTitle); // convert spaces // Convert extended letters to ascii equivalents $processedTitle = $GLOBALS['TSFE']->csConvObj->specCharsToASCII($charset, $processedTitle); // Strip the rest if ($this->extConf['init']['enableAllUnicodeLetters']) { // Warning: slow!!! $processedTitle = preg_replace('/[^\p{L}0-9' . ($space ? preg_quote($space) : '') . ']/u', '', $processedTitle); } else { $processedTitle = preg_replace('/[^a-zA-Z0-9' . ($space ? preg_quote($space) : '') . ']/', '', $processedTitle); } $processedTitle = preg_replace('/\\' . $space . '{2,}/', $space, $processedTitle); // Convert multiple 'spaces' to a single one $processedTitle = trim($processedTitle, $space); if ($this->conf['encodeTitle_userProc']) { $encodingConfiguration = array('strtolower' => true, 'spaceCharacter' => $this->conf['spaceCharacter']); $params = array('pObj' => &$this, 'title' => $title, 'processedTitle' => $processedTitle, 'encodingConfiguration' => $encodingConfiguration); $processedTitle = $this->apiWrapper->callUserFunction($this->conf['encodeTitle_userProc'], $params, $this); } // Return encoded URL return rawurlencode(strtolower($processedTitle)); } /** * Makes expiration timestamp for SQL queries * * @param int $offsetFromNow Offset to expiration * @return int Expiration time stamp */ protected function makeExpirationTime($offsetFromNow = 0) { if (!$this->apiWrapper->isExtLoaded('adodb') && (TYPO3_db_host == '127.0.0.1' || TYPO3_db_host == 'localhost')) { // Same host, same time, optimize return $offsetFromNow ? '(UNIX_TIMESTAMP()+(' . $offsetFromNow . '))' : 'UNIX_TIMESTAMP()'; } // External database or non-mysql -> round to next day $date = getdate(time() + $offsetFromNow); return mktime(0, 0, 0, $date['mon'], $date['mday'], $date['year']); } /** * Gets the value of current language. Defaults to value * taken from the system configuration. * * @param array $urlParameters * @return integer Current language or system default */ protected function getLanguageVar(array $urlParameters) { // Get the default language from the TSFE $lang = intval($GLOBALS['TSFE']->config['config']['sys_language_uid']); // Setting the language variable based on GETvar in URL which has been configured to carry the language uid if ($this->conf['languageGetVar']) { if (isset($urlParameters[$this->conf['languageGetVar']])) { $lang = intval($urlParameters[$this->conf['languageGetVar']]); } elseif (isset($this->pObj->orig_paramKeyValues[$this->conf['languageGetVar']])) { $lang = intval($this->pObj->orig_paramKeyValues[$this->conf['languageGetVar']]); } } // Might be excepted (like you should for CJK cases which does not translate to ASCII equivalents) if (isset($this->conf['languageExceptionUids']) && $this->apiWrapper->inList($this->conf['languageExceptionUids'], $lang)) { $lang = 0; } return $lang; } /** * Resolves shortcut to the page * * @param array $page Page record * @param bool $disableGroupAccessCheck Flag for getPage() * @param array $log Internal log * @param string|null $mpvar * @return int Found page id */ protected function resolveShortcut($page, $disableGroupAccessCheck, $log = array(), &$mpvar = null) { if (isset($log[$page['uid']])) { // loop detected! return $page['uid']; } $log[$page['uid']] = ''; $pageid = $page['uid']; if ($page['shortcut_mode'] == 0) { // Jumps to a certain page if ($page['shortcut']) { $pageid = intval($page['shortcut']); $page = $GLOBALS['TSFE']->sys_page->getPage($pageid, $disableGroupAccessCheck); if ($page && $page['doktype'] == 4) { $mpvar = ''; $pageid = $this->resolveShortcut($page, $disableGroupAccessCheck, $log, $mpvar); } } } elseif ($page['shortcut_mode'] == 1) { // Jumps to the first subpage $rows = $GLOBALS['TSFE']->sys_page->getMenu($page['uid']); if (count($rows) > 0) { reset($rows); $row = current($rows); $pageid = ($row['doktype'] == 4 ? $this->resolveShortcut($row, $disableGroupAccessCheck, $log, $mpvar) : $row['uid']); } if (isset($row['_MP_PARAM'])) { if ($mpvar) { $mpvar .= ','; } $mpvar .= $row['_MP_PARAM']; } } elseif ($page['shortcut_mode'] == 3) { // Jumps to the parent page $page = $GLOBALS['TSFE']->sys_page->getPage($page['pid'], $disableGroupAccessCheck); $pageid = $page['uid']; if ($page && $page['doktype'] == 4) { $pageid = $this->resolveShortcut($page, $disableGroupAccessCheck, $log, $mpvar); } } return $pageid; } /** * Creates $this->sysPage if it does not exist yet * * @return void */ protected function createSysPageIfNecessary() { if (!is_object($this->sysPage)) { $this->sysPage = $this->apiWrapper->getPageRepository(); $this->sysPage->init($GLOBALS['TSFE']->showHiddenPage || $this->pObj->isBEUserLoggedIn()); } } } /** @noinspection PhpUndefinedVariableInspection */ if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/realurl/class.tx_realurl_advanced.php']) { /** @noinspection PhpUndefinedMethodInspection PhpUndefinedVariableInspection PhpIncludeInspection */ include_once ($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/realurl/class.tx_realurl_advanced.php']); }