getRepositoryBasePath(); } $this->basePath = rtrim($basePath, '/') . '/'; } /** * Creates the extensions.xml.gz file in a safe manner * * @param \DOMDocument $dom * @return string the newly created filename * @throws InternalServerErrorException */ public function writeXmlToFile(\DOMDocument $dom): string { // Write XML data to disk: $fh = fopen($this->basePath . 'new-extensions.xml.gz', 'wb'); if (!$fh) { throw new InternalServerErrorException( 'Write error while writing extensions index file: ' . $this->basePath . 'extensions.xml' ); } fwrite($fh, gzencode($dom->saveXML(), 9)); fclose($fh); if (!@filesize($this->basePath . 'new-extensions.xml.gz') > 0) { $this->logger->error('Newly created extension index is zero bytes!'); throw new InternalServerErrorException( 'Write error while writing extensions index file (zero bytes): ' . $this->basePath . 'extensions.xml' ); } @unlink($this->basePath . 'extensions.xml.gz'); rename($this->basePath . 'new-extensions.xml.gz', $this->basePath . 'extensions.xml.gz'); GeneralUtility::writeFile($this->basePath . 'extensions.md5', md5_file($this->basePath . 'extensions.xml.gz')); return $this->basePath . 'extensions.xml.gz'; } /** * Write serialized array file to disk. Question is: WHY should we do this? * * @param array $extensionsAndVersionsArr * @return string the absolute file that was created. * @throws InternalServerErrorException */ public function writeSerializedExtensionInformationToFile(array $extensionsAndVersionsArr): string { $fh = fopen($this->basePath . 'new-extensions.bin', 'wb'); if (!$fh) { throw new InternalServerErrorException( 'Write error while writing extensions index file: ' . $this->basePath . 'extensions.bin' ); } fwrite($fh, serialize($extensionsAndVersionsArr)); fclose($fh); if (!@filesize($this->basePath . 'new-extensions.bin') > 0) { $this->logger->debug('Newly created extension index is zero bytes!'); throw new InternalServerErrorException( 'Write error while writing extensions index file (zero bytes): ' . $this->basePath . 'extensions.bin' ); } @unlink($this->basePath . 'extensions.bin'); rename($this->basePath . 'new-extensions.bin', $this->basePath . 'extensions.bin'); return $this->basePath . 'extensions.bin'; } /** * Build DOM structure for the XML to output. * * @param array $extensionsAndVersionsArr * @param string $processStarted * @return \DOMDocument */ public function compileXmlStructure(array $extensionsAndVersionsArr, string $processStarted): \DOMDocument { // 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((string)($extensionVersionsArr['downloads'] ?? '0')) ) ); foreach ($extensionVersionsArr['versions'] as $versionNumber => $extensionVersionArr) { $versionObj = $extensionObj->appendChild(new \DOMElement('version')); $versionObj->appendChild(new \DOMAttr('version', (string)$versionNumber)); $versionObj->appendChild(new \DOMElement('title', $this->xmlentities((string)$extensionVersionArr['title']))); $versionObj->appendChild(new \DOMElement('description', $this->xmlentities((string)$extensionVersionArr['description']))); $versionObj->appendChild(new \DOMElement('state', $this->xmlentities((string)$extensionVersionArr['state']))); $versionObj->appendChild(new \DOMElement('reviewstate', (string)$extensionVersionArr['reviewstate'])); $versionObj->appendChild(new \DOMElement('category', $this->xmlentities((string)$extensionVersionArr['category']))); if ($extensionVersionArr['category'] === 'distribution') { $prefixDistributionFilePath = $extensionKey[0] . '/' . $extensionKey[1] . '/' . $extensionKey . '_' . $versionNumber . '_'; $distributionImage = $prefixDistributionFilePath . 'Distribution.png'; $distributionWelcomeImage = $prefixDistributionFilePath . 'DistributionWelcome.png'; if (is_file($this->basePath . $distributionImage)) { $versionObj->appendChild(new \DOMElement('distributionImage', $this->xmlentities($distributionImage))); } if (is_file($this->basePath . $distributionWelcomeImage)) { $versionObj->appendChild(new \DOMElement('distributionImageWelcome', $this->xmlentities($distributionWelcomeImage))); } } $versionObj->appendChild( new \DOMElement( 'downloadcounter', $this->xmlentities((string)$extensionVersionArr['downloadcounter']) ) ); $versionObj->appendChild(new \DOMElement('lastuploaddate', (string)$extensionVersionArr['lastuploaddate'])); $versionObj->appendChild( new \DOMElement( 'uploadcomment', $this->xmlentities((string)$extensionVersionArr['uploadcomment']) ) ); $versionObj->appendChild(new \DOMElement('dependencies', (string)$extensionVersionArr['dependencies'])); $versionObj->appendChild(new \DOMElement('composerinfo', (string)$extensionVersionArr['composerinfo'])); $versionObj->appendChild(new \DOMElement('authorname', $this->xmlentities((string)$extensionVersionArr['authorname']))); $versionObj->appendChild(new \DOMElement('authoremail', $this->xmlentities((string)$extensionVersionArr['authoremail']))); $versionObj->appendChild( new \DOMElement( 'authorcompany', $this->xmlentities((string)$extensionVersionArr['authorcompany']) ) ); $versionObj->appendChild( new \DOMElement( 'ownerusername', $this->xmlentities((string)$extensionVersionsArr['ownerusername']) ) ); $versionObj->appendChild(new \DOMElement('t3xfilemd5', (string)$extensionVersionArr['t3xfilemd5'])); $versionObj->appendChild(new \DOMElement('documentation_link', (string)$extensionVersionArr['external_manual'] ?: '')); } } $extensionsObj->appendChild(new \DOMComment('Index created at ' . date('D M j G:i:s T Y'))); $extensionsObj->appendChild(new \DOMComment('Index created in ' . (microtime() - $processStarted) . ' ms')); return $dom; } /** * 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. */ public function requestUpdate() { GeneralUtility::writeFile( $this->basePath . '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' ); } public function isOldUpdateRequested(): bool { // Check if update of files requested $updateRequestedFile = $this->basePath . 'extensions.xml.gz.needsupdate'; if (file_exists($updateRequestedFile) && @filemtime($updateRequestedFile) <= @filemtime($this->basePath . 'extensions.xml.gz')) { return true; } return false; } /** * Equivalent to htmlentities but for XML content * * @param string $string : String to encode * @return string &,",',< and > replaced by entities */ protected function xmlentities(string $string): string { // Until I have found a better solution for guaranteeing valid characters, I use this regex: $string = (preg_replace('/[^\w\s"%&\[\]\(\)\.\,\;\:\/\?\{\}!\$\-\/\@]/u', '', $string)); return str_replace(['&', '"', "'", '<', '>'], ['&', '"', ''', '<', '>'], $string); } protected function getRepositoryBasePath(): string { // Check extension repository path $extensionConfig = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['ter'] ?? []; if (empty($extensionConfig['repositoryDir'])) { throw new \Exception('No repository path found in extension configuration', 1303220917); } return $extensionConfig['repositoryDir']; } }