TYPO3  7.6
DocumentationService.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Documentation\Service;
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 
18 
23 {
29  public function getOfficialDocuments()
30  {
31  $documents = array();
32 
33  $json = GeneralUtility::getUrl('https://docs.typo3.org/typo3cms/documents.json');
34  if ($json) {
35  $documents = json_decode($json, true);
36  foreach ($documents as &$document) {
37  $document['icon'] = \TYPO3\CMS\Documentation\Utility\MiscUtility::getIcon($document['key']);
38  }
39 
40  // Cache file locally to be able to create a composer.json file when fetching a document
41  $absoluteCacheFilename = GeneralUtility::getFileAbsFileName('typo3temp/Documentation/documents.json');
42  GeneralUtility::writeFileToTypo3tempDir($absoluteCacheFilename, $json);
43  }
44  return $documents;
45  }
46 
52  public function getLocalExtensions()
53  {
54  $documents = array();
55 
56  foreach ($GLOBALS['TYPO3_LOADED_EXT'] as $extensionKey => $extensionData) {
57  $absoluteExtensionPath = GeneralUtility::getFileAbsFileName($extensionData['siteRelPath']);
58  if (is_file($absoluteExtensionPath . 'README.rst') || is_file($absoluteExtensionPath . 'Documentation' . DIRECTORY_SEPARATOR . 'Index.rst')) {
59  $metadata = \TYPO3\CMS\Documentation\Utility\MiscUtility::getExtensionMetaData($extensionKey);
60  if ($extensionData['type'] === 'S') {
61  $version = TYPO3_branch;
62  } else {
63  $version = substr($metadata['release'], -4) === '-dev' ? 'latest' : $metadata['release'];
64  }
65 
66  $documentKey = 'typo3cms.extensions.' . $extensionKey;
67  $documents[] = array(
68  'title' => $metadata['title'],
69  'icon' => \TYPO3\CMS\Documentation\Utility\MiscUtility::getIcon($documentKey),
70  'type' => 'Extension',
71  'key' => $documentKey,
72  'shortcut' => $extensionKey,
73  'url' => 'https://docs.typo3.org/typo3cms/extensions/' . $extensionKey . '/',
74  'version' => $version,
75  );
76  }
77  }
78 
79  return $documents;
80  }
81 
98  public function fetchNearestDocument($url, $key, $version = 'latest', $language = 'default')
99  {
100  // In case we could not find a working combination
101  $success = false;
102 
103  $packages = $this->getAvailablePackages($url);
104  if (empty($packages)) {
105  return $success;
106  }
107 
108  $languages = array($language);
109  if ($language !== 'default') {
110  $languages[] = 'default';
111  }
112  foreach ($languages as $language) {
113  // Step 1)
114  if (isset($packages[$version][$language])) {
115  $success |= $this->fetchDocument($url, $key, $version, $language);
116  // Fetch next language
117  continue;
118  } else {
119  if (isset($packages[$version])) {
120  foreach ($packages[$version] as $locale => $_) {
121  if (GeneralUtility::isFirstPartOfStr($locale, $language)) {
122  $success |= $this->fetchDocument($url, $key, $version, $locale);
123  // Fetch next language (jump current foreach up to the loop of $languages)
124  continue 2;
125  }
126  }
127  }
128  }
129  // Step 2)
130  if (preg_match('/^(\d+\.\d+)\.\d+$/', $version, $matches)) {
131  // Instead of a 3-digit version, try to get it on 2 digits
132  $shortVersion = $matches[1];
133  if (isset($packages[$shortVersion][$language])) {
134  $success |= $this->fetchDocument($url, $key, $shortVersion, $language);
135  // Fetch next language
136  continue;
137  }
138  }
139  // Step 3)
140  if ($version !== 'latest' && isset($packages['latest'][$language])) {
141  $success |= $this->fetchDocument($url, $key, 'latest', $language);
142  // Fetch next language
143  continue;
144  }
145  }
146 
147  return $success;
148  }
149 
159  public function fetchDocument($url, $key, $version = 'latest', $language = 'default')
160  {
161  $result = false;
162  $url = rtrim($url, '/') . '/';
163 
164  $packagePrefix = substr($key, strrpos($key, '.') + 1);
165  $languageSegment = str_replace('_', '-', strtolower($language));
166  $packageName = sprintf('%s-%s-%s.zip', $packagePrefix, $version, $languageSegment);
167  $packageUrl = $url . 'packages/' . $packageName;
168  $absolutePathToZipFile = GeneralUtility::getFileAbsFileName('typo3temp/Documentation/' . $packageName);
169 
170  $packages = $this->getAvailablePackages($url);
171  if (empty($packages) || !isset($packages[$version][$language])) {
172  return false;
173  }
174 
175  // Check if a local version of the package is already present
176  $hasArchive = false;
177  if (is_file($absolutePathToZipFile)) {
178  $localMd5 = md5_file($absolutePathToZipFile);
179  $remoteMd5 = $packages[$version][$language];
180  $hasArchive = $localMd5 === $remoteMd5;
181  }
182 
183  if (!$hasArchive) {
185  $http = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Http\HttpRequest::class, $packageUrl);
186  $response = $http->send();
187  if ($response->getStatus() == 200) {
188  GeneralUtility::writeFileToTypo3tempDir($absolutePathToZipFile, $response->getBody());
189  }
190  }
191 
192  if (is_file($absolutePathToZipFile)) {
193  $absoluteDocumentPath = GeneralUtility::getFileAbsFileName('typo3conf/Documentation/');
194 
195  $result = $this->unzipDocumentPackage($absolutePathToZipFile, $absoluteDocumentPath);
196 
197  // Create a composer.json file
198  $absoluteCacheFilename = GeneralUtility::getFileAbsFileName('typo3temp/Documentation/documents.json');
199  $documents = json_decode(file_get_contents($absoluteCacheFilename), true);
200  foreach ($documents as $document) {
201  if ($document['key'] === $key) {
202  $composerData = array(
203  'name' => $document['title'],
204  'type' => 'documentation',
205  'description' => 'TYPO3 ' . $document['type'],
206  );
207  $relativeComposerFilename = $key . '/' . $language . '/composer.json';
208  $absoluteComposerFilename = GeneralUtility::getFileAbsFileName('typo3conf/Documentation/' . $relativeComposerFilename);
209  GeneralUtility::writeFile($absoluteComposerFilename, json_encode($composerData));
210  break;
211  }
212  }
213  }
214 
215  return $result;
216  }
217 
225  protected function getAvailablePackages($url)
226  {
227  $packages = array();
228  $url = rtrim($url, '/') . '/';
229  $indexUrl = $url . 'packages/packages.xml';
230 
231  $remote = GeneralUtility::getUrl($indexUrl);
232  if ($remote) {
233  $packages = $this->parsePackagesXML($remote);
234  }
235 
236  return $packages;
237  }
238 
246  protected function parsePackagesXML($string)
247  {
248  $data = json_decode(json_encode((array)simplexml_load_string($string)), true);
249  if (count($data) !== 2) {
250  throw new \TYPO3\CMS\Documentation\Exception\XmlParser('Error in XML parser while decoding packages XML file.', 1374222437);
251  }
252 
253  // SimpleXML does not properly handle arrays with only 1 item
254  if ($data['languagePackIndex']['languagepack'][0] === null) {
255  $data['languagePackIndex']['languagepack'] = array($data['languagePackIndex']['languagepack']);
256  }
257 
258  $packages = array();
259  foreach ($data['languagePackIndex']['languagepack'] as $languagePack) {
260  $language = $languagePack['@attributes']['language'];
261  $version = $languagePack['@attributes']['version'];
262  $packages[$version][$language] = $languagePack['md5'];
263  }
264 
265  return $packages;
266  }
267 
276  protected function unzipDocumentPackage($file, $path)
277  {
278  $zip = zip_open($file);
279  if (is_resource($zip)) {
280  $result = true;
281 
282  if (!is_dir($path)) {
284  }
285 
286  while (($zipEntry = zip_read($zip)) !== false) {
287  $zipEntryName = zip_entry_name($zipEntry);
288  if (strpos($zipEntryName, '/') !== false) {
289  $zipEntryPathSegments = explode('/', $zipEntryName);
290  $fileName = array_pop($zipEntryPathSegments);
291  // It is a folder, because the last segment is empty, let's create it
292  if (empty($fileName)) {
293  GeneralUtility::mkdir_deep($path, implode('/', $zipEntryPathSegments));
294  } else {
295  $absoluteTargetPath = GeneralUtility::getFileAbsFileName($path . implode('/', $zipEntryPathSegments) . '/' . $fileName);
296  if (trim($absoluteTargetPath) !== '') {
297  $return = GeneralUtility::writeFile(
298  $absoluteTargetPath, zip_entry_read($zipEntry, zip_entry_filesize($zipEntry))
299  );
300  if ($return === false) {
301  throw new \TYPO3\CMS\Documentation\Exception\Document('Could not write file ' . $zipEntryName, 1374161546);
302  }
303  } else {
304  throw new \TYPO3\CMS\Documentation\Exception\Document('Could not write file ' . $zipEntryName, 1374161532);
305  }
306  }
307  } else {
308  throw new \TYPO3\CMS\Documentation\Exception\Document('Extension directory missing in zip file!', 1374161519);
309  }
310  }
311  } else {
312  throw new \TYPO3\CMS\Documentation\Exception\Document('Unable to open zip file ' . $file, 1374161508);
313  }
314 
315  return $result;
316  }
317 }