‪TYPO3CMS  ‪main
TerExtensionRemote.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\Http\Message\ResponseInterface;
26 
39 {
40  protected string ‪$identifier;
42  protected string ‪$remoteBase = 'https://extensions.typo3.org/fileadmin/ter/';
43 
44  public function ‪__construct(string ‪$identifier, array $options = [])
45  {
46  $this->identifier = ‪$identifier;
47  $this->localExtensionListCacheFile = ‪Environment::getVarPath() . '/extensionmanager/' . $this->identifier . '.extensions.xml.gz';
48 
49  if ($options['remoteBase'] ?? '') {
50  $this->remoteBase = $options['remoteBase'];
51  }
52  }
53 
54  public function ‪getIdentifier(): string
55  {
56  return ‪$this->identifier;
57  }
58 
62  public function ‪getAvailablePackages(bool $force = false): void
63  {
64  if ($force || $this->‪needsUpdate()) {
65  $this->‪fetchPackageList();
66  }
67  }
68 
69  public function ‪needsUpdate(): bool
70  {
71  $threshold = new \DateTimeImmutable('-7 days');
72  if ($this->‪getLastUpdate() < $threshold) {
73  return true;
74  }
75  return $this->‪isDownloadedExtensionListUpToDate() !== true;
76  }
77 
82  protected function ‪isDownloadedExtensionListUpToDate(): bool
83  {
84  if (!file_exists($this->localExtensionListCacheFile)) {
85  return false;
86  }
87  try {
88  $response = $this->‪downloadFile('extensions.md5');
89  $md5SumOfRemoteExtensionListFile = $response->getBody()->getContents();
90  return hash_equals($md5SumOfRemoteExtensionListFile, md5_file($this->localExtensionListCacheFile) ?: '');
91  } catch (‪DownloadFailedException $exception) {
92  return false;
93  }
94  }
95 
96  public function ‪getLastUpdate(): \DateTimeInterface
97  {
98  if (file_exists($this->localExtensionListCacheFile) && filesize($this->localExtensionListCacheFile) > 0) {
99  $mtime = filemtime($this->localExtensionListCacheFile);
100  return (new \DateTimeImmutable())->setTimestamp($mtime);
101  }
102  // Select a very old date (hint: easter egg)
103  return new \DateTimeImmutable('1975-04-13');
104  }
105 
109  protected function ‪fetchPackageList(): void
110  {
111  try {
112  $extensionListXml = $this->‪downloadFile('extensions.xml.gz');
113  ‪GeneralUtility::writeFileToTypo3tempDir($this->localExtensionListCacheFile, $extensionListXml->getBody()->getContents());
114  GeneralUtility::makeInstance(BulkExtensionRepositoryWriter::class)->import($this->localExtensionListCacheFile, $this->identifier);
115  } catch (‪DownloadFailedException $e) {
116  // Do not update package list
117  }
118  }
119 
124  protected function ‪downloadFile(string $remotePath): ResponseInterface
125  {
126  try {
127  $requestFactory = GeneralUtility::makeInstance(RequestFactory::class);
128  return $requestFactory->request($this->remoteBase . $remotePath);
129  } catch (\Throwable $e) {
130  throw new ‪DownloadFailedException(sprintf('The file "%s" could not be fetched. Possible reasons: network problems, allow_url_fopen is off, cURL is not available', $this->remoteBase . $remotePath), 1334426297);
131  }
132  }
133 
141  public function ‪downloadExtension(string $extensionKey, string $version, ‪FileHandlingUtility $fileHandler, string $verificationHash = null, string $pathType = 'Local'): void
142  {
143  $extensionPath = strtolower($extensionKey);
144  $remotePath = $extensionPath[0] . '/' . $extensionPath[1] . '/' . $extensionPath . '_' . $version . '.t3x';
145  try {
146  $downloadedContent = (string)$this->‪downloadFile($remotePath)->getBody()->getContents();
147  } catch (\Throwable $e) {
148  throw new ‪DownloadFailedException(sprintf('The T3X file "%s" could not be fetched. Possible reasons: network problems, allow_url_fopen is off, cURL is not available.', $this->remoteBase . $remotePath), 1334426097);
149  }
150  if ($verificationHash && !$this->‪isDownloadedPackageValid($verificationHash, $downloadedContent)) {
151  throw new ‪VerificationFailedException('MD5 hash of downloaded file not as expected: ' . md5($downloadedContent) . ' != ' . $verificationHash, 1334426098);
152  }
153  $extensionData = $this->‪decodeExchangeData($downloadedContent);
154  if (!empty($extensionData['extKey']) && is_string($extensionData['extKey'])) {
155  $fileHandler->‪unpackExtensionFromExtensionDataArray($extensionData['extKey'], $extensionData);
156  } else {
157  throw new ‪VerificationFailedException('Downloaded t3x file could not be extracted', 1334426698);
158  }
159  }
160 
164  protected function ‪isDownloadedPackageValid(string $expectedHash, string $fileContents): bool
165  {
166  return hash_equals($expectedHash, md5($fileContents));
167  }
168 
177  protected function ‪decodeExchangeData(string $stream): array
178  {
179  [$expectedHash, $compressionType, $contents] = explode(':', $stream, 3);
180  if ($compressionType === 'gzcompress') {
181  if (function_exists('gzuncompress')) {
182  $contents = gzuncompress($contents) ?: '';
183  } else {
184  throw new ‪VerificationFailedException('No decompressor available for compressed content. gzcompress()/gzuncompress() functions are not available', 1601370681);
185  }
186  }
187  if ($this->‪isDownloadedPackageValid($expectedHash, $contents)) {
188  ‪$output = unserialize($contents, ['allowed_classes' => false]);
189  if (!is_array(‪$output)) {
190  throw new ‪VerificationFailedException('Content could not be unserialized to an array. Strange (since MD5 hashes match!)', 1601370682);
191  }
192  } else {
193  throw new ‪VerificationFailedException('MD5 mismatch. Maybe the extension file was downloaded and saved as a text file by the browser and thereby corrupted.', 1601370683);
194  }
195  return ‪$output;
196  }
197 }
‪TYPO3\CMS\Extensionmanager\Domain\Repository\BulkExtensionRepositoryWriter
Definition: BulkExtensionRepositoryWriter.php:34
‪TYPO3\CMS\Extensionmanager\Remote\TerExtensionRemote\$identifier
‪string $identifier
Definition: TerExtensionRemote.php:40
‪TYPO3\CMS\Extensionmanager\Remote\TerExtensionRemote\isDownloadedExtensionListUpToDate
‪isDownloadedExtensionListUpToDate()
Definition: TerExtensionRemote.php:82
‪TYPO3\CMS\Extensionmanager\Remote\TerExtensionRemote\__construct
‪__construct(string $identifier, array $options=[])
Definition: TerExtensionRemote.php:44
‪TYPO3\CMS\Extensionmanager\Remote\TerExtensionRemote\downloadFile
‪downloadFile(string $remotePath)
Definition: TerExtensionRemote.php:124
‪TYPO3\CMS\Extensionmanager\Remote\TerExtensionRemote\getAvailablePackages
‪getAvailablePackages(bool $force=false)
Definition: TerExtensionRemote.php:62
‪TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility\unpackExtensionFromExtensionDataArray
‪unpackExtensionFromExtensionDataArray(string $extensionKey, array $extensionData)
Definition: FileHandlingUtility.php:58
‪TYPO3\CMS\Core\Core\Environment\getVarPath
‪static getVarPath()
Definition: Environment.php:197
‪TYPO3\CMS\Extensionmanager\Remote\VerificationFailedException
Definition: VerificationFailedException.php:25
‪TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility
Definition: FileHandlingUtility.php:40
‪TYPO3\CMS\Extensionmanager\Remote\TerExtensionRemote\needsUpdate
‪needsUpdate()
Definition: TerExtensionRemote.php:69
‪TYPO3\CMS\Extensionmanager\Remote\TerExtensionRemote\downloadExtension
‪downloadExtension(string $extensionKey, string $version, FileHandlingUtility $fileHandler, string $verificationHash=null, string $pathType='Local')
Definition: TerExtensionRemote.php:141
‪TYPO3\CMS\Extensionmanager\Remote\TerExtensionRemote\isDownloadedPackageValid
‪isDownloadedPackageValid(string $expectedHash, string $fileContents)
Definition: TerExtensionRemote.php:164
‪TYPO3\CMS\Core\Utility\GeneralUtility\writeFileToTypo3tempDir
‪static string null writeFileToTypo3tempDir(string $filepath, string $content)
Definition: GeneralUtility.php:1561
‪TYPO3\CMS\Extensionmanager\Remote\TerExtensionRemote\getIdentifier
‪getIdentifier()
Definition: TerExtensionRemote.php:54
‪TYPO3\CMS\Extensionmanager\Remote\TerExtensionRemote
Definition: TerExtensionRemote.php:39
‪TYPO3\CMS\Extensionmanager\Remote\TerExtensionRemote\decodeExchangeData
‪array decodeExchangeData(string $stream)
Definition: TerExtensionRemote.php:177
‪TYPO3\CMS\Extensionmanager\Remote\ExtensionDownloaderRemoteInterface
Definition: ExtensionDownloaderRemoteInterface.php:28
‪TYPO3\CMS\Extensionmanager\Remote\TerExtensionRemote\$remoteBase
‪string $remoteBase
Definition: TerExtensionRemote.php:42
‪TYPO3\CMS\Extensionmanager\Remote
Definition: DownloadFailedException.php:18
‪TYPO3\CMS\Core\Http\RequestFactory
Definition: RequestFactory.php:30
‪$output
‪$output
Definition: annotationChecker.php:114
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:41
‪TYPO3\CMS\Extensionmanager\Remote\ListableRemoteInterface
Definition: ListableRemoteInterface.php:24
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Extensionmanager\Remote\TerExtensionRemote\$localExtensionListCacheFile
‪string $localExtensionListCacheFile
Definition: TerExtensionRemote.php:41
‪TYPO3\CMS\Extensionmanager\Remote\TerExtensionRemote\getLastUpdate
‪getLastUpdate()
Definition: TerExtensionRemote.php:96
‪TYPO3\CMS\Extensionmanager\Remote\DownloadFailedException
Definition: DownloadFailedException.php:25
‪TYPO3\CMS\Extensionmanager\Remote\TerExtensionRemote\fetchPackageList
‪fetchPackageList()
Definition: TerExtensionRemote.php:109