basePath = $this->getRepositoryBasePath(); } else { $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')); $distributionBaseUrl = $this->getDistributionBaseUrl(); // 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']))); // Use "0" for outdated extension versions as the Extension Manager handles it as insecure $reviewState = (string)((int)$extensionVersionArr['review_state'] !== -2 ? $extensionVersionArr['review_state'] : 0); $versionObj->appendChild(new \DOMElement('reviewstate', $reviewState)); $versionObj->appendChild(new \DOMElement('category', $this->xmlentities((string)$extensionVersionArr['em_category']))); if ($extensionVersionArr['em_category'] === 'distribution') { $prefixDistributionFilePath = $extensionKey[0] . '/' . $extensionKey[1] . '/' . $extensionKey . '_' . $versionNumber . '_'; $distributionImage = $prefixDistributionFilePath . 'Distribution.png'; $distributionWelcomeImage = $prefixDistributionFilePath . 'DistributionWelcome.png'; if (is_file($this->basePath . $distributionImage)) { $distributionImageUrl = $distributionBaseUrl . $distributionImage; $versionObj->appendChild( new \DOMElement( 'distributionImage', $this->xmlentities($distributionImageUrl) ) ); } if (is_file($this->basePath . $distributionWelcomeImage)) { $distributionWelcomeImageUrl = $distributionBaseUrl . $distributionWelcomeImage; $versionObj->appendChild( new \DOMElement( 'distributionImageWelcome', $this->xmlentities($distributionWelcomeImageUrl) ) ); } } $versionObj->appendChild( new \DOMElement( 'downloadcounter', $this->xmlentities((string)$extensionVersionArr['download_counter']) ) ); $versionObj->appendChild(new \DOMElement('lastuploaddate', (string)$extensionVersionArr['upload_date'])); $versionObj->appendChild( new \DOMElement( 'uploadcomment', $this->xmlentities((string)$extensionVersionArr['upload_comment']) ) ); $versionObj->appendChild(new \DOMElement('dependencies', $this->serializeDependencies($extensionVersionArr['dependencies']))); $versionObj->appendChild(new \DOMElement('composerinfo', (string)$extensionVersionArr['composer_info'])); $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['frontend_user']) ) ); $versionObj->appendChild(new \DOMElement('t3xfilemd5', (string)$extensionVersionArr['file_hash'])); $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 { return GeneralUtility::makeInstance(Configuration::class)->getRepositoryBasePath(); } protected function serializeDependencies(?string $dependencies): string { return $dependencies !== null ? (string)serialize(json_decode($dependencies, true)) : ''; } protected function getDistributionBaseUrl(): string { return implode( '', [ (string)GeneralUtility::makeInstance(SiteFinder::class)->getSiteByIdentifier('extensions')->getBase() . '/', GeneralUtility::makeInstance(ResourceFactory::class)->getDefaultStorage()->getFolder('ter')->getPublicUrl() ] ); } }