TYPO3 CMS  TYPO3_8-7
DocumentationService.php
Go to the documentation of this file.
1 <?php
3 
4 /*
5  * This file is part of the TYPO3 CMS project.
6  *
7  * It is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU General Public License, either version 2
9  * of the License, or any later version.
10  *
11  * For the full copyright and license information, please read the
12  * LICENSE.txt file that was distributed with this source code.
13  *
14  * The TYPO3 project - inspiring people to share!
15  */
16 
27 
31 class DocumentationService implements LoggerAwareInterface
32 {
33  use LoggerAwareTrait;
34 
40  public function getOfficialDocuments()
41  {
42  $documents = [];
43 
44  $json = GeneralUtility::getUrl('https://docs.typo3.org/typo3cms/documents.json');
45  if ($json) {
46  $documents = json_decode($json, true);
47  foreach ($documents as &$document) {
48  $document['icon'] = MiscUtility::getIcon($document['key']);
49  }
50 
51  // Cache file locally to be able to create a composer.json file when fetching a document
52  $absoluteCacheFilename = GeneralUtility::getFileAbsFileName('typo3temp/var/transient/documents.json');
53  GeneralUtility::writeFileToTypo3tempDir($absoluteCacheFilename, $json);
54  }
55  return $documents;
56  }
57 
63  public function getLocalExtensions()
64  {
65  $documents = [];
66 
67  foreach ($GLOBALS['TYPO3_LOADED_EXT'] as $extensionKey => $extensionData) {
68  $absoluteExtensionPath = ExtensionManagementUtility::extPath($extensionKey);
69  if (is_file($absoluteExtensionPath . 'README.rst') || is_file($absoluteExtensionPath . 'Documentation' . DIRECTORY_SEPARATOR . 'Index.rst')) {
70  $metadata = MiscUtility::getExtensionMetaData($extensionKey);
71  if ($extensionData['type'] === 'S') {
72  $version = TYPO3_branch;
73  } else {
74  $version = substr($metadata['release'], -4) === '-dev' ? 'latest' : $metadata['release'];
75  }
76 
77  $documentKey = 'typo3cms.extensions.' . $extensionKey;
78  $documents[] = [
79  'title' => $metadata['title'],
80  'icon' => MiscUtility::getIcon($documentKey),
81  'type' => 'Extension',
82  'key' => $documentKey,
83  'shortcut' => $extensionKey,
84  'url' => 'https://docs.typo3.org/typo3cms/extensions/' . $extensionKey . '/',
85  'version' => $version,
86  ];
87  }
88  }
89 
90  return $documents;
91  }
92 
109  public function fetchNearestDocument($url, $key, $version = 'latest', $language = 'default')
110  {
111  // In case we could not find a working combination
112  $success = false;
113 
114  $packages = $this->getAvailablePackages($url);
115  if (empty($packages)) {
116  return $success;
117  }
118 
119  $languages = [$language];
120  if ($language !== 'default') {
121  $languages[] = 'default';
122  }
123  foreach ($languages as $language) {
124  // Step 1)
125  if (isset($packages[$version][$language])) {
126  $success |= $this->fetchDocument($url, $key, $version, $language);
127  // Fetch next language
128  continue;
129  }
130  if (isset($packages[$version])) {
131  foreach ($packages[$version] as $locale => $_) {
132  if (GeneralUtility::isFirstPartOfStr($locale, $language)) {
133  $success |= $this->fetchDocument($url, $key, $version, $locale);
134  // Fetch next language (jump current foreach up to the loop of $languages)
135  continue 2;
136  }
137  }
138  }
139 
140  // Step 2)
141  if (preg_match('/^(\d+\.\d+)\.\d+$/', $version, $matches)) {
142  // Instead of a 3-digit version, try to get it on 2 digits
143  $shortVersion = $matches[1];
144  if (isset($packages[$shortVersion][$language])) {
145  $success |= $this->fetchDocument($url, $key, $shortVersion, $language);
146  // Fetch next language
147  continue;
148  }
149  }
150  // Step 3)
151  if ($version !== 'latest' && isset($packages['latest'][$language])) {
152  $success |= $this->fetchDocument($url, $key, 'latest', $language);
153  // Fetch next language
154  continue;
155  }
156  }
157 
158  return $success;
159  }
160 
170  public function fetchDocument($url, $key, $version = 'latest', $language = 'default')
171  {
172  $result = false;
173  $url = rtrim($url, '/') . '/';
174 
175  $packagePrefix = substr($key, strrpos($key, '.') + 1);
176  $languageSegment = str_replace('_', '-', strtolower($language));
177  $packageName = sprintf('%s-%s-%s.zip', $packagePrefix, $version, $languageSegment);
178  $packageUrl = $url . 'packages/' . $packageName;
179  $absolutePathToZipFile = GeneralUtility::getFileAbsFileName('typo3temp/var/transient/' . $packageName);
180 
181  $packages = $this->getAvailablePackages($url);
182  if (empty($packages) || !isset($packages[$version][$language])) {
183  return false;
184  }
185 
186  // Check if a local version of the package is already present
187  $hasArchive = false;
188  if (is_file($absolutePathToZipFile)) {
189  $localMd5 = md5_file($absolutePathToZipFile);
190  $remoteMd5 = $packages[$version][$language];
191  $hasArchive = $localMd5 === $remoteMd5;
192  }
193 
194  if (!$hasArchive) {
196  $requestFactory = GeneralUtility::makeInstance(RequestFactory::class);
197  $response = $requestFactory->request($packageUrl, 'GET');
198  if ($response->getStatusCode() === 200) {
199  GeneralUtility::writeFileToTypo3tempDir($absolutePathToZipFile, $response->getBody()->getContents());
200  }
201  }
202 
203  if (is_file($absolutePathToZipFile)) {
204  $absoluteDocumentPath = GeneralUtility::getFileAbsFileName('typo3conf/Documentation/');
205 
206  $result = $this->unzipDocumentPackage($absolutePathToZipFile, $absoluteDocumentPath);
207 
208  // Create a composer.json file
209  $absoluteCacheFilename = GeneralUtility::getFileAbsFileName('typo3temp/var/transient/documents.json');
210  $documents = json_decode(file_get_contents($absoluteCacheFilename), true);
211  foreach ($documents as $document) {
212  if ($document['key'] === $key) {
213  $composerData = [
214  'name' => $document['title'],
215  'type' => 'documentation',
216  'description' => 'TYPO3 ' . $document['type'],
217  ];
218  $relativeComposerFilename = $key . '/' . $language . '/composer.json';
219  $absoluteComposerFilename = GeneralUtility::getFileAbsFileName('typo3conf/Documentation/' . $relativeComposerFilename);
220  GeneralUtility::writeFile($absoluteComposerFilename, json_encode($composerData));
221  break;
222  }
223  }
224  }
225 
226  return $result;
227  }
228 
236  protected function getAvailablePackages($url)
237  {
238  $packages = [];
239  $url = rtrim($url, '/') . '/';
240  $indexUrl = $url . 'packages/packages.xml';
241 
242  $remote = GeneralUtility::getUrl($indexUrl);
243  if ($remote) {
244  $packages = $this->parsePackagesXML($remote);
245  }
246 
247  return $packages;
248  }
249 
257  protected function parsePackagesXML($string)
258  {
259  // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
260  $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
261  $data = json_decode(json_encode((array)simplexml_load_string($string)), true);
262  libxml_disable_entity_loader($previousValueOfEntityLoader);
263  if (count($data) !== 2) {
264  throw new XmlParser('Error in XML parser while decoding packages XML file.', 1374222437);
265  }
266 
267  // SimpleXML does not properly handle arrays with only 1 item
268  if ($data['languagePackIndex']['languagepack'][0] === null) {
269  $data['languagePackIndex']['languagepack'] = [$data['languagePackIndex']['languagepack']];
270  }
271 
272  $packages = [];
273  foreach ($data['languagePackIndex']['languagepack'] as $languagePack) {
274  $language = $languagePack['@attributes']['language'];
275  $version = $languagePack['@attributes']['version'];
276  $packages[$version][$language] = $languagePack['md5'];
277  }
278 
279  return $packages;
280  }
281 
290  protected function unzipDocumentPackage($file, $path)
291  {
292  if (!is_dir($path)) {
294  }
295 
296  try {
297  $zipService = GeneralUtility::makeInstance(ZipService::class);
298  if ($zipService->verify($file)) {
299  $zipService->extract($file, $path);
300  }
301  } catch (ExtractException $e) {
302  $this->logger->error('Extracting the documentation archive failed', ['exception' => $e]);
303  throw new Document('Extracting the documentation archive failed: ' . $e->getMessage(), 1576247962, $e);
304  }
305  GeneralUtility::fixPermissions($path, true);
306 
307  return true;
308  }
309 }
static mkdir_deep($directory, $deepDirectory='')
static isFirstPartOfStr($str, $partStr)
static writeFileToTypo3tempDir($filepath, $content)
static getFileAbsFileName($filename, $_=null, $_2=null)
static makeInstance($className,... $constructorArguments)
static fixPermissions($path, $recursive=false)
fetchNearestDocument($url, $key, $version='latest', $language='default')
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
static writeFile($file, $content, $changePermissions=false)