*/ /** * [CLASS/FUNCTION INDEX of SCRIPT] * * * * 103: class tx_ter_helper * 114: public function __construct($pluginObj) * 127: public function getValidUser ($accountData) * 162: public function extensionKeyIsAvailable($extensionKey) * 188: public function getExtensionKeyRecord ($extKey) * 215: public function getLatestVersionNumberOfExtension ($extensionKey) * 245: public function requestUpdateOfExtensionIndexFile() * 260: public function writeExtensionIndexfile() * 358: public function xmlentities ($string) * * TOTAL FUNCTIONS: 8 * (This index is automatically created/updated by the extension "extdeveval") * */ use TYPO3\CMS\Core\Utility\GeneralUtility; // Make sure that we are executed only in TYPO3 context if (!defined('TYPO3_MODE')) { die('Access denied.'); } // Error codes: define('TX_TER_ERROR_GENERAL_EXTREPDIRDOESNTEXIST', '100'); define('TX_TER_ERROR_GENERAL_NOUSERORPASSWORD', '101'); define('TX_TER_ERROR_GENERAL_USERNOTFOUND', '102'); define('TX_TER_ERROR_GENERAL_WRONGPASSWORD', '103'); define('TX_TER_ERROR_GENERAL_DATABASEERROR', '104'); define('TX_TER_ERROR_GENERAL_EXTENSIONCONTAINSNOFILES', '105'); define('TX_TER_ERROR_UPLOADEXTENSION_EXTENSIONDOESNTEXIST', '202'); define('TX_TER_ERROR_UPLOADEXTENSION_EXTENSIONCONTAINSNOFILES', '203'); define('TX_TER_ERROR_UPLOADEXTENSION_WRITEERRORWHILEWRITINGFILES', '204'); define('TX_TER_ERROR_UPLOADEXTENSION_EXTENSIONTOOBIG', '205'); define('TX_TER_ERROR_UPLOADEXTENSION_EXISTINGEXTENSIONRECORDNOTFOUND', '206'); define('TX_TER_ERROR_UPLOADEXTENSION_FILEMD5DOESNOTMATCH', '207'); define('TX_TER_ERROR_UPLOADEXTENSION_ACCESSDENIED', '208'); define('TX_TER_ERROR_UPLOADEXTENSION_TYPO3DEPENDENCYINCORRECT', '209'); define('TX_TER_ERROR_UPLOADEXTENSION_TYPO3DEPENDENCYCHECKFAILED', '210'); define('TX_TER_ERROR_UPLOADEXTENSION_EXTENSIONVERSIONEXISTS', '211'); define('TX_TER_ERROR_REGISTEREXTENSIONKEY_DBERRORWHILEINSERTINGKEY', '300'); define('TX_TER_ERROR_DELETEEXTENSIONKEY_ACCESSDENIED', '500'); define('TX_TER_ERROR_DELETEEXTENSIONKEY_KEYDOESNOTEXIST', '501'); define('TX_TER_ERROR_DELETEEXTENSIONKEY_CANTDELETEBECAUSEVERSIONSEXIST', '502'); define('TX_TER_ERROR_MODIFYEXTENSIONKEY_ACCESSDENIED', '600'); define('TX_TER_ERROR_MODIFYEXTENSIONKEY_SETTINGTOTHISOWNERISNOTPOSSIBLE', '601'); define('TX_TER_ERROR_MODIFYEXTENSIONKEY_KEYDOESNOTEXIST', '602'); define('TX_TER_ERROR_SETREVIEWSTATE_NOUSERGROUPDEFINED', '700'); define('TX_TER_ERROR_SETREVIEWSTATE_ACCESSDENIED', '701'); define('TX_TER_ERROR_SETREVIEWSTATE_EXTENSIONVERSIONDOESNOTEXIST', '702'); define('TX_TER_ERROR_INCREASEEXTENSIONDOWNLOADCOUNTER_NOUSERGROUPDEFINED', '800'); define('TX_TER_ERROR_INCREASEEXTENSIONDOWNLOADCOUNTER_ACCESSDENIED', '801'); define('TX_TER_ERROR_INCREASEEXTENSIONDOWNLOADCOUNTER_EXTENSIONVERSIONDOESNOTEXIST', '802'); define('TX_TER_ERROR_INCREASEEXTENSIONDOWNLOADCOUNTER_INCREMENTORNOTPOSITIVEINTEGER', '803'); define('TX_TER_ERROR_INCREASEEXTENSIONDOWNLOADCOUNTER_EXTENSIONKEYDOESNOTEXIST', '804'); define('TX_TER_ERROR_DELETEEXTENSION_ACCESS_DENIED', '900'); define('TX_TER_ERROR_DELETEEXTENSION_EXTENSIONDOESNTEXIST', '901'); // Result codes: define('TX_TER_RESULT_GENERAL_OK', '10000'); define('TX_TER_RESULT_ERRORS_OCCURRED', '10001'); define('TX_TER_RESULT_EXTENSIONKEYALREADYEXISTS', '10500'); define('TX_TER_RESULT_EXTENSIONKEYDOESNOTEXIST', '10501'); define('TX_TER_RESULT_EXTENSIONKEYNOTVALID', '10502'); define('TX_TER_RESULT_EXTENSIONKEYSUCCESSFULLYREGISTERED', '10503'); define('TX_TER_RESULT_EXTENSIONSUCCESSFULLYUPLOADED', '10504'); define('TX_TER_RESULT_EXTENSIONSUCCESSFULLYDELETED', '10505'); /** * TYPO3 Extension Repository, helper functions * * @author Robert Lemke * @package TYPO3 * @subpackage tx_ter_helper */ class tx_ter_helper { protected $pluginObj; /** * Constructor * * @param object $pluginObj : Reference to parent object * @return void * @access public */ public function __construct($pluginObj) { $this->pluginObj = $pluginObj; } /** * This verifies the given fe_users username/password. * Either the fe_user row is returned or an exception is thrown. * * @param object $accountData : Account data information with username, password and upload password * @return mixed If success, returns array of fe_users, otherwise error string. * @access public */ public function getValidUser($accountData) { if (!strlen($accountData->username) || (!strlen($accountData->password))) { throw new tx_ter_exception_unauthorized('No user or no password submitted.', TX_TER_ERROR_GENERAL_NOUSERORPASSWORD); } $res = $this->getDatabaseConnection()->exec_SELECTquery( '*', 'fe_users', 'username=' . $this->getDatabaseConnection()->fullQuoteStr($accountData->username, 'fe_users') . $this->getTyposcriptFrontendObject()->sys_page->enableFields('fe_users') ); if ($row = $this->getDatabaseConnection()->sql_fetch_assoc($res)) { if (!$this->userIsAlreadyLoggedIn( $accountData ) && !$this->ldapValidationSucceeded($accountData) ) { throw new tx_ter_exception_unauthorized ('Wrong password.', TX_TER_ERROR_GENERAL_WRONGPASSWORD); } } else { throw new tx_ter_exception_unauthorized('The specified user does not exist. You need to login first on extensions.typo3.org.', TX_TER_ERROR_GENERAL_USERNOTFOUND); } $row['admin'] = (intval($this->pluginObj->conf['adminFrontendUsergroupUid']) && GeneralUtility::inList( $row['usergroup'], $this->pluginObj->conf['adminFrontendUsergroupUid'] )); return $row; } /** * We check whether a user is logged in by TYPO3 * because of a sent session cookie * * @param $accountData * @return bool */ private function userIsAlreadyLoggedIn($accountData) { /** @var \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController $tsfe */ $tsfe = $GLOBALS['TSFE']; if (!empty($tsfe->fe_user->user['username']) && $accountData->username === $tsfe->fe_user->user['username']) { return true; } return false; } /** * Check if LDAP authenticates the credentials * * @param stdClass $accountData * @return bool */ private function ldapValidationSucceeded(stdClass $accountData): bool { if (!\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('ig_ldap_sso_auth')) { return false; } /** @var \Causal\IgLdapSsoAuth\Domain\Repository\ConfigurationRepository $configurationRepository */ $configurationRepository = GeneralUtility::makeInstance(\Causal\IgLdapSsoAuth\Domain\Repository\ConfigurationRepository::class); $configurationRecords = $configurationRepository->findAll(); \Causal\IgLdapSsoAuth\Library\Configuration::initialize('fe', $configurationRecords[0]); return (bool)\Causal\IgLdapSsoAuth\Library\Authentication::ldapAuthenticate($accountData->username, $accountData->password); } /** * Checks for correct account data without throwing an exception. * It just returns TRUE / FALSE * * @param object $accountData * @return boolean */ public function checkValidUser($accountData) { if (!strlen($accountData->username) || (!strlen($accountData->password))) { $success = false; } else { $success = false; $res = $this->getDatabaseConnection()->exec_SELECTquery( '*', 'fe_users', 'username=' . $this->getDatabaseConnection()->fullQuoteStr( $accountData->username, 'fe_users' ) . $GLOBALS['TSFE']->sys_page->enableFields('fe_users') ); if ($row = $this->getDatabaseConnection()->sql_fetch_assoc($res)) { if ($this->ldapValidationSucceeded( $accountData) ) { $success = true; } } $this->getDatabaseConnection()->sql_free_result($res); } return $success; } /** * Checks if the given extension key is unique and not registered yet. * Takes underscores into account, so the key "ter_ter" can't be registered * if "te_rt_er" or "terter" already exist. * * @param string $extensionKey : The extension key to check * @return boolean Returns TRUE if the extension key is unique and not used yet, otherwise FALSE * @access public * @author Elmar Hinz */ public function extensionKeyIsAvailable($extensionKey) { $cleanedExtensionKey = str_replace('_', '', $extensionKey); $isAvailable = true; $res = $this->getDatabaseConnection()->exec_SELECTquery( 'extensionkey', 'tx_ter_extensionkeys', 'pid=' . intval($this->pluginObj->extensionsPID) ); while ($row = $this->getDatabaseConnection()->sql_fetch_row($res)) { if ($cleanedExtensionKey === str_replace('_', '', $row[0])) { $isAvailable = false; } } return $isAvailable; } /** * Based on $extKey this returns the extension-key record. * * @param string $extKey : Extension key * @return mixed The extension key row or FALSE * @access public */ public function getExtensionKeyRecord($extKey) { $row = $this->getDatabaseConnection()->exec_SELECTgetSingleRow( '*', 'tx_ter_extensionkeys', 'extensionkey=' . $this->getDatabaseConnection()->fullQuoteStr($extKey, 'tx_ter_extensionkeys') . 'AND pid=' . intval($this->pluginObj->extensionsPID) . $this->getTyposcriptFrontendObject()->sys_page->enableFields('tx_ter_extensionkeys') ); return $row; } /** * Searches the repository for the highest version number of an upload of the * extension specified by $extensionKey. If no upload was found at all, FALSE * will be returned. If at least one upload was found, the highest version number * following the format major.minor.dev (eg. 4.2.1) will be returned. * * @param string $extKey : Extension key * @return mixed The version number as a string or FALSE * @access public */ public function getLatestVersionNumberOfExtension($extensionKey) { $res = $this->getDatabaseConnection()->exec_SELECTquery( 'version', 'tx_ter_extensions', 'extensionkey=' . $this->getDatabaseConnection()->fullQuoteStr($extensionKey, 'tx_ter_extensions') . ' AND pid=' . intval($this->pluginObj->extensionsPID) ); $latestVersion = false; while ($row = $this->getDatabaseConnection()->sql_fetch_assoc($res)) { if (version_compare($row['version'], $latestVersion, '>')) { $latestVersion = $row['version']; } } return $latestVersion; } /** * Sets a flag so the cron job knows that the extensions.xml.gz file has to be * regenerated. Call this whenever data has changed which also exists in * extensions.xml.gz * * Note: Depending on the cron job it might take a while until the index file really * has been updated. See "cli/build-extension-index.php" for more information * * @return void * @access public */ public function requestUpdateOfExtensionIndexFile() { GeneralUtility::writeFile( $this->pluginObj->repositoryDir . 'extensions.xml.gz.needsupdate', 'Dear cron-job. The extensions.xml.gz file needs to be regenerated, please do so as soon as you find the time for it.' . chr( 10 ) . 'Thanks, your TER helper class' ); } /** * Updates the "extensions.xml" file which contains an index of all uploaded * extensions in the TER. * * @return void * @access public */ public function writeExtensionIndexfile() { GeneralUtility::devLog('writing extension index!', 'tx_ter_helper', 0); if (!@is_dir($this->pluginObj->repositoryDir)) { throw new tx_ter_exception_internalServerError( 'Extension repository directory does not exist.', TX_TER_ERROR_GENERAL_EXTREPDIRDOESNTEXIST ); } $trackTime = microtime(); $res = $this->getDatabaseConnection()->exec_SELECTquery( 'uid,tstamp,extensionkey,version,title,description,state,reviewstate,category,downloadcounter,t3xfilemd5', 'tx_ter_extensions', '1' ); // Read the extension records from the DB: $extensionsAndVersionsArr = []; $extensionsTotalDownloadsArr = []; while ($row = $this->getDatabaseConnection()->sql_fetch_assoc($res)) { $res2 = $this->getDatabaseConnection()->exec_SELECTquery( 'ownerusername,downloadcounter', 'tx_ter_extensionkeys', 'extensionkey=' . $this->getDatabaseConnection()->fullQuoteStr($row['extensionkey'], 'tx_ter_extensionkeys') ); $extensionKeyRow = $this->getDatabaseConnection()->sql_fetch_assoc($res2); $row['ownerusername'] = $extensionKeyRow['ownerusername']; $extensionsTotalDownloadsArr[$row['extensionkey']] = $extensionKeyRow['downloadcounter']; $res2 = $this->getDatabaseConnection()->exec_SELECTquery( 'lastuploaddate,uploadcomment,dependencies,composerinfo,authorname,authoremail,authorcompany', 'tx_ter_extensiondetails', 'extensionuid=' . (int)$row['uid'] ); $detailsRow = $this->getDatabaseConnection()->sql_fetch_assoc($res2); if (is_array($detailsRow)) { $row = $row + $detailsRow; } $extensionsAndVersionsArr [$row['extensionkey']]['versions'][$row['version']] = $row; } // Prepare the DOM object: $dom = new DOMDocument('1.0', 'utf-8'); $dom->formatOutput = true; $extensionsObj = $dom->appendChild(new DOMElement('extensions')); // Create the nested XML structure: foreach ($extensionsAndVersionsArr as $extensionKey => $extensionVersionsArr) { $extensionObj = $extensionsObj->appendChild(new DOMElement('extension')); $extensionObj->appendChild(new DOMAttr('extensionkey', $extensionKey)); $extensionObj->appendChild( new DOMElement( 'downloadcounter', $this->xmlentities($extensionsTotalDownloadsArr[$extensionKey]) ) ); foreach ($extensionVersionsArr['versions'] as $versionNumber => $extensionVersionArr) { $versionObj = $extensionObj->appendChild(new DOMElement('version')); $versionObj->appendChild(new DOMAttr('version', $versionNumber)); $versionObj->appendChild(new DOMElement('title', $this->xmlentities($extensionVersionArr['title']))); $versionObj->appendChild(new DOMElement('description', $this->xmlentities($extensionVersionArr['description']))); $versionObj->appendChild(new DOMElement('state', $this->xmlentities($extensionVersionArr['state']))); $versionObj->appendChild(new DOMElement('reviewstate', intval($extensionVersionArr['reviewstate']))); $versionObj->appendChild(new DOMElement('category', $this->xmlentities($extensionVersionArr['category']))); $versionObj->appendChild( new DOMElement( 'downloadcounter', $this->xmlentities($extensionVersionArr['downloadcounter']) ) ); $versionObj->appendChild(new DOMElement('lastuploaddate', $extensionVersionArr['lastuploaddate'])); $versionObj->appendChild( new DOMElement( 'uploadcomment', $this->xmlentities($extensionVersionArr['uploadcomment']) ) ); $versionObj->appendChild(new DOMElement('dependencies', $extensionVersionArr['dependencies'])); $versionObj->appendChild(new DOMElement('composerinfo', $extensionVersionArr['composerinfo'])); $versionObj->appendChild(new DOMElement('authorname', $this->xmlentities($extensionVersionArr['authorname']))); $versionObj->appendChild(new DOMElement('authoremail', $this->xmlentities($extensionVersionArr['authoremail']))); $versionObj->appendChild( new DOMElement( 'authorcompany', $this->xmlentities($extensionVersionArr['authorcompany']) ) ); $versionObj->appendChild( new DOMElement( 'ownerusername', $this->xmlentities($extensionVersionArr['ownerusername']) ) ); $versionObj->appendChild(new DOMElement('t3xfilemd5', $extensionVersionArr['t3xfilemd5'])); } } $extensionsObj->appendChild(new DOMComment('Index created at ' . date("D M j G:i:s T Y"))); $extensionsObj->appendChild(new DOMComment('Index created in ' . (microtime() - $trackTime) . ' ms')); // Write XML data to disk: $fh = fopen($this->pluginObj->repositoryDir . 'new-extensions.xml.gz', 'wb'); if (!$fh) { throw new tx_ter_exception_internalServerError( 'Write error while writing extensions index file: ' . $this->pluginObj->repositoryDir . 'extensions.xml', TX_TER_ERROR_UPLOADEXTENSION_WRITEERRORWHILEWRITINGEXTENSIONSINDEX ); } fwrite($fh, gzencode($dom->saveXML(), 9)); fclose($fh); if (!@filesize($this->pluginObj->repositoryDir . 'new-extensions.xml.gz') > 0) { GeneralUtility::devLog('Newly created extension index is zero bytes!', 'tx_ter_helper', 0); throw new tx_ter_exception_internalServerError( 'Write error while writing extensions index file (zero bytes): ' . $this->pluginObj->repositoryDir . 'extensions.xml', TX_TER_ERROR_UPLOADEXTENSION_WRITEERRORWHILEWRITINGEXTENSIONSINDEX ); } @unlink($this->pluginObj->repositoryDir . 'extensions.xml.gz'); rename($this->pluginObj->repositoryDir . 'new-extensions.xml.gz', $this->pluginObj->repositoryDir . 'extensions.xml.gz'); GeneralUtility::writeFile( $this->pluginObj->repositoryDir . 'extensions.md5', md5_file($this->pluginObj->repositoryDir . 'extensions.xml.gz') ); // Write serialized array file to disk: $fh = fopen($this->pluginObj->repositoryDir . 'new-extensions.bin', 'wb'); if (!$fh) { throw new tx_ter_exception_internalServerError( 'Write error while writing extensions index file: ' . $this->pluginObj->repositoryDir . 'extensions.bin', TX_TER_ERROR_UPLOADEXTENSION_WRITEERRORWHILEWRITINGEXTENSIONSINDEX ); } fwrite($fh, serialize($extensionsAndVersionsArr)); fclose($fh); if (!@filesize($this->pluginObj->repositoryDir . 'new-extensions.bin') > 0) { GeneralUtility::devLog('Newly created extension index is zero bytes!', 'tx_ter_helper', 0); throw new tx_ter_exception_internalServerError( 'Write error while writing extensions index file (zero bytes): ' . $this->pluginObj->repositoryDir . 'extensions.bin', TX_TER_ERROR_UPLOADEXTENSION_WRITEERRORWHILEWRITINGEXTENSIONSINDEX ); } @unlink($this->pluginObj->repositoryDir . 'extensions.bin'); rename($this->pluginObj->repositoryDir . 'new-extensions.bin', $this->pluginObj->repositoryDir . 'extensions.bin'); } /** * Equivalent to htmlentities but for XML content * * @param string $string : String to encode * @return string &,",',< and > replaced by entities * @access public */ public function xmlentities($string) { // Until I have found a better solution for guaranteeing valid characters, I use this regex: $string = (preg_replace('/[^\w\s"%&\[\]\(\)\.\,\;\:\/\?\{\}!\$\-\/\@]/', '', $string)); return str_replace(['&', '"', "'", '<', '>'], ['&', '"', ''', '<', '>'], $string); } /*** * Load an instance of the BE_USER to use with TCEFORM * * @param integer $uid UID of the virtual user * @param string $username Username * @param boolean $isAdmin Set admin rights * @return void */ public function loadBackendUser($uid, $username, $isAdmin = false) { if (!empty($GLOBALS['BE_USER'])) { return; } $GLOBALS['BE_USER'] = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\FrontendBackendUserAuthentication::class); $GLOBALS['BE_USER']->user = [ 'uid' => 0, 'username' => $username, 'admin' => (int)$isAdmin, ]; } /** * Load an instance of the LANG object * * @param string $language Used language ident * @return void */ public function loadLang($lang = 'default') { if (!empty($GLOBALS['LANG'])) { return; } $GLOBALS['LANG'] = GeneralUtility::makeInstance(\TYPO3\CMS\Lang\LanguageService::class); $GLOBALS['LANG']->init($lang); } /** * @return \TYPO3\CMS\Core\Database\DatabaseConnection */ private function getDatabaseConnection() { return $GLOBALS['TYPO3_DB']; } /** * @return \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController */ private function getTyposcriptFrontendObject() { return $GLOBALS['TSFE']; } }