$prefs['auth_token_access_maxtimeout'], 'maxHits' => $prefs['auth_token_access_maxhits'], ] ); } public function __construct($db, $options = [], DateTime $dt = null) { $this->db = $db; $this->table = $this->db->table('tiki_auth_tokens'); if (is_null($dt)) { $this->dt = new DateTime(); } else { $this->dt = $dt; } if (isset($options['maxTimeout'])) { $this->maxTimeout = (int) $options['maxTimeout']; } if (isset($options['maxHits'])) { $this->maxHits = (int) $options['maxHits']; } } public function getToken($token) { $data = $this->db->query( 'SELECT * FROM tiki_auth_tokens WHERE token = ? AND token = ' . self::SCHEME, [ $token ] )->fetchRow(); return $data; } public function getTokens($conditions = []) { return $this->table->fetchAll([], $conditions, -1, -1, ['creation' => 'asc']); } /** * @param int $limit The maximum number of tokens to delete * * @return mixed */ public function deleteExpired(int $limit = -1) { return $this->db->query( 'DELETE FROM tiki_auth_tokens WHERE (timeout != -1 AND UNIX_TIMESTAMP(creation) + timeout < UNIX_TIMESTAMP()) OR `hits` = 0', null, $limit, ); } public function getGroups($token, $entry, $parameters) { // Process deletion of temporary users that are created via tokens $usersToDelete = $this->db->fetchAll( 'SELECT tokenId, userPrefix FROM tiki_auth_tokens WHERE (timeout != -1 AND UNIX_TIMESTAMP(creation) + timeout < UNIX_TIMESTAMP()) OR `hits` = 0', null, 2000 ); $userlib = TikiLib::lib('user'); foreach ($usersToDelete as $del) { if (! empty($del['userPrefix'])) { $userlib->remove_temporary_user($del['userPrefix'] . $del['tokenId']); } } $this->deleteExpired(2000); $data = $this->db->query( 'SELECT tokenId, entry, parameters, `groups`, email, createUser, userPrefix FROM tiki_auth_tokens WHERE token = ? AND token = ' . self::SCHEME, [ $token ] )->fetchRow(); if (! $data) { return null; } global $prefs, $tikiroot; // $full defined in route.php if ($prefs['feature_sefurl'] === 'y' && strpos($entry, 'tiki-autologin.php') === false) { $sefurlTypeMap = $this->getSefurlTypeMap(); $keys = array_keys($_GET); $seftype = ''; for ($i = 0; $i < count($keys); $i++) { $seftype = $sefurlTypeMap[$keys[$i]]; if ($seftype) { $key = $keys[$i]; // $parameters is compared with the stored $data['parameters'] later // but that doesn't include the 'page' or 'fileId' etc param due to sefurl unset($parameters[$keys[$i]]); break; } } if (empty($key)) { // missing object type? return null; } TikiLib::lib('smarty')->loadPlugin('smarty_modifier_sefurl'); $sefurl = $tikiroot . smarty_modifier_sefurl($_GET[$key], $seftype); // add an extra conversion to prevent false positives due to url encoding // e.g. in cases of "/tikiroot/My Page" vs "/tikiroot/My+Page" $entry_no_tikiroot = substr($data['entry'], strlen($tikiroot)); $entry_encoded_no_tikiroot = urlencode($entry_no_tikiroot); $full_entry_encoded = $tikiroot . $entry_encoded_no_tikiroot; $convertedSefurl = ! empty($GLOBALS['path']) ? $tikiroot . $GLOBALS['path'] : ''; if ($data['entry'] !== $sefurl && $full_entry_encoded !== $sefurl && $convertedSefurl !== $sefurl) { return null; // entry doesn't match } } elseif (! isset($data['entry']) || $data['entry'] != $entry) { return null; // entry doesn't match } $registered = (array) json_decode($data['parameters'], true); // If sefurl is in use, do not compare page or fileId params if ($prefs['feature_sefurl'] === 'y' && ! empty($key)) { unset($registered[$key]); } if (! $this->allPresent($registered, $parameters) || ! $this->allPresent($parameters, $registered)) { return null; } $this->db->query( 'UPDATE `tiki_auth_tokens` SET `hits` = `hits` - 1 WHERE `tokenId` = ? AND hits != -1', [ $data['tokenId'] ] ); // Process autologin of temporary users if ($data['createUser'] == 'y') { $tempuser = $data['userPrefix'] . $userlib->autogenerate_login($data['tokenId'], 6); $groups = json_decode($data['groups'], true); if (! $userlib->user_exists($tempuser)) { $randompass = $userlib->genPass(); $userlib->add_user($tempuser, $randompass, $data['email'], '', false, null, null, null, $groups); } $userlib->autologin_user($tempuser); $url = ! empty($convertedSefurl) ? basename($convertedSefurl) : basename($data['entry']); if ($parameters) { $query = '?' . http_build_query($parameters, '', '&'); $url .= $query; } include_once(__DIR__ . '/../../tiki-sefurl.php'); $url = filter_out_sefurl($url); TikiLib::lib('access')->redirect($url); die; } $this->ok = true; return (array) json_decode($data['groups'], true); } private function allPresent($a, $b) { foreach ($a as $key => $value) { if (! isset($b[$key]) || $value != $b[$key]) { return false; } } return true; } /** * Provide mapping between item key and object type * TODO centralise this info in objectlib.php (?) and decide on one word or two for each * * @return array */ private function getSefurlTypeMap() { return [ 'page' => 'wiki page', 'articleId' => 'article', 'blogId' => 'blog', 'postId' => 'blog post', 'parentId' => 'category', 'fileId' => 'file', 'galleryId' => 'file gallery', 'forumId' => 'forum', 'nlId' => 'newsletter', 'trackerId' => 'tracker', 'itemId' => 'trackeritem', 'sheetId' => 'sheet', 'userId' => 'user', 'calIds' => 'calendar', ]; } public function createToken($entry, array $parameters, array $groups, array $arguments = []) { if (! empty($arguments['timeout'])) { $timeout = min($this->maxTimeout, $arguments['timeout']); } else { $timeout = $this->maxTimeout; } if (! empty($arguments['hits'])) { $hits = $arguments['hits']; } else { $hits = $this->maxHits; } if (isset($arguments['email'])) { $email = $arguments['email']; } else { $email = ''; } if (! empty($arguments['createUser']) && $arguments['createUser'] !== 'n') { $createUser = 'y'; } else { $createUser = 'n'; } if (isset($arguments['userPrefix'])) { $userPrefix = $arguments['userPrefix']; } else { $userPrefix = ''; } $this->db->query( 'INSERT INTO tiki_auth_tokens ( timeout, maxhits, hits, entry, parameters, `groups`, email, createUser, userPrefix ) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ? )', [ (int) $timeout, (int) $hits, (int) $hits, $entry, json_encode($parameters), json_encode($groups), $email, $createUser, $userPrefix, ] ); $max = $this->db->getOne('SELECT MAX(tokenId) FROM tiki_auth_tokens'); $this->db->query('UPDATE tiki_auth_tokens SET token = ' . self::SCHEME . ' WHERE tokenId = ?', [ $max ]); return $this->db->getOne('SELECT token FROM tiki_auth_tokens WHERE tokenId = ?', [ $max ]); } /** * This is a function that includes a security token into a provided URL * @param string $url The URL where the token is valid. * @param array $groups The groups from which the person using the token will have permissions for. * @param string $email The email that the token was sent to, for recording purpose. If there are multiple * emails, it currently saves as comma-separated list but exploding will need to trim spaces. * @param int $timeout Timeout to set in seconds. If not included, will use default as set in prefs. * @param int $hits Number of hits allowed before token expires. If not included, will use default as set in prefs. * @param boolean $createUser Login token user as temporary user if set to true * @param string $userPrefix Username of the created users will be a 6 digit number based on the token ID prefixed with this (default is 'guest') * @return string A URL that has the security token included. */ public function includeToken($url, array $groups = [], $email = '', $timeout = 0, $hits = 0, $createUser = false, $userPrefix = 'guest') { $data = parse_url($url); $longurl = ''; if (isset($data['query'])) { parse_str($data['query'], $args); unset($args['TOKEN']); } else { global $prefs, $sefurl_regex_out; include_once __DIR__ . '/../../tiki-sefurl.php'; if ($prefs['feature_sefurl'] === 'y' && ! empty($sefurl_regex_out)) { global $base_url; $short = substr($url, strlen($base_url)); $is_numeric = preg_match('/\d+/', $short); foreach (array_reverse($sefurl_regex_out) as $regex) { // wiki is the first one and will match anything if ($is_numeric) { $replace = '(\d+)'; // match digits } else { $replace = '(.+)'; // or anything (for wiki pages) } $pattern = str_replace('$1', $replace, $regex['right']); if (preg_match('/' . $pattern . '/', $short, $matches)) { $longurl = preg_replace('/' . preg_quote($replace) . '/', $matches[1], $regex['left']); $longurl = $base_url . stripcslashes($longurl); // add back the beginning and get rid of the \ infront of the ? break; } } if ($longurl) { $longdata = parse_url($longurl); parse_str($longdata['query'], $args); } else { $args = []; } } else { $args = []; } } $settings = ['email' => $email]; if (! empty($timeout)) { $settings['timeout'] = $timeout; } if (! empty($hits)) { $settings['hits'] = $hits; } $settings['createUser'] = $createUser; $settings['userPrefix'] = $userPrefix; $token = $this->createToken($data['path'], $args, $groups, $settings); if ($longurl) { // sefurl was used so the args should be reset now the token has been created $args = []; } $args['TOKEN'] = $token; $query = '?' . http_build_query($args, '', '&'); if (! isset($data['fragment'])) { $anchor = ''; } else { $anchor = "#{$data['fragment']}"; } return "{$data['scheme']}://{$data['host']}{$data['path']}$query$anchor"; } public function deleteToken($tokenId) { $userPrefix = $this->table->fetchOne( 'userPrefix', ['tokenId' => $tokenId, 'createUser' => 'y'] ); if ($userPrefix) { TikiLib::lib('user')->remove_temporary_user($userPrefix . $tokenId); } $this->table->delete(['tokenId' => $tokenId]); } }