[TASK] Do not store files directly into typo3temp
[Packages/TYPO3.CMS.git] / typo3 / sysext / documentation / Classes / Service / DocumentationService.php
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
17 use TYPO3\CMS\Core\Utility\GeneralUtility;
18
19 /**
20 * Service class to connect to docs.typo3.org.
21 */
22 class DocumentationService {
23
24 /**
25 * Returns the list of official documents on docs.typo3.org.
26 *
27 * @return array
28 */
29 public function getOfficialDocuments() {
30 $documents = array();
31
32 $json = GeneralUtility::getUrl('https://docs.typo3.org/typo3cms/documents.json');
33 if ($json) {
34 $documents = json_decode($json, TRUE);
35 foreach ($documents as &$document) {
36 $document['icon'] = \TYPO3\CMS\Documentation\Utility\MiscUtility::getIcon($document['key']);
37 }
38
39 // Cache file locally to be able to create a composer.json file when fetching a document
40 $absoluteCacheFilename = GeneralUtility::getFileAbsFileName('typo3temp/Documentation/documents.json');
41 GeneralUtility::writeFileToTypo3tempDir($absoluteCacheFilename, $json);
42 }
43 return $documents;
44 }
45
46 /**
47 * Returns the list of local extensions.
48 *
49 * @return array
50 */
51 public function getLocalExtensions() {
52 $documents = array();
53
54 foreach ($GLOBALS['TYPO3_LOADED_EXT'] as $extensionKey => $extensionData) {
55 $absoluteExtensionPath = GeneralUtility::getFileAbsFileName($extensionData['siteRelPath']);
56 if (is_file($absoluteExtensionPath . 'README.rst') || is_file($absoluteExtensionPath . 'Documentation' . DIRECTORY_SEPARATOR . 'Index.rst')) {
57 $metadata = \TYPO3\CMS\Documentation\Utility\MiscUtility::getExtensionMetaData($extensionKey);
58 if ($extensionData['type'] === 'S') {
59 $version = TYPO3_branch;
60 } else {
61 $version = substr($metadata['release'], -4) === '-dev' ? 'latest' : $metadata['release'];
62 }
63
64 $documentKey = 'typo3cms.extensions.' . $extensionKey;
65 $documents[] = array(
66 'title' => $metadata['title'],
67 'icon' => \TYPO3\CMS\Documentation\Utility\MiscUtility::getIcon($documentKey),
68 'type' => 'Extension',
69 'key' => $documentKey,
70 'shortcut' => $extensionKey,
71 'url' => 'https://docs.typo3.org/typo3cms/extensions/' . $extensionKey . '/',
72 'version' => $version,
73 );
74 }
75 }
76
77 return $documents;
78 }
79
80 /**
81 * Fetches the nearest version of a document from docs.typo3.org.
82 *
83 * Algorithm is as follows:
84 *
85 * 1) If exact version/language pair exists, fetch it
86 * 2) If document with version trimmed down to 2 digits and given language exists, fetch it
87 * 3) If document with version 'latest' and given language exists, fetch it
88 * 4) Restart at step 1) with language 'default'
89 *
90 * @param string $url
91 * @param string $key
92 * @param string $version
93 * @param string $language
94 * @return bool TRUE if fetch succeeded, otherwise FALSE
95 */
96 public function fetchNearestDocument($url, $key, $version = 'latest', $language = 'default') {
97 // In case we could not find a working combination
98 $success = FALSE;
99
100 $packages = $this->getAvailablePackages($url);
101 if (empty($packages)) {
102 return $success;
103 }
104
105 $languages = array($language);
106 if ($language !== 'default') {
107 $languages[] = 'default';
108 }
109 foreach ($languages as $language) {
110 // Step 1)
111 if (isset($packages[$version][$language])) {
112 $success |= $this->fetchDocument($url, $key, $version, $language);
113 // Fetch next language
114 continue;
115 } else {
116 if (isset($packages[$version])) {
117 foreach ($packages[$version] as $locale => $_) {
118 if (GeneralUtility::isFirstPartOfStr($locale, $language)) {
119 $success |= $this->fetchDocument($url, $key, $version, $locale);
120 // Fetch next language (jump current foreach up to the loop of $languages)
121 continue 2;
122 }
123 }
124 }
125 }
126 // Step 2)
127 if (preg_match('/^(\d+\.\d+)\.\d+$/', $version, $matches)) {
128 // Instead of a 3-digit version, try to get it on 2 digits
129 $shortVersion = $matches[1];
130 if (isset($packages[$shortVersion][$language])) {
131 $success |= $this->fetchDocument($url, $key, $shortVersion, $language);
132 // Fetch next language
133 continue;
134 }
135 }
136 // Step 3)
137 if ($version !== 'latest' && isset($packages['latest'][$language])) {
138 $success |= $this->fetchDocument($url, $key, 'latest', $language);
139 // Fetch next language
140 continue;
141 }
142 }
143
144 return $success;
145 }
146
147 /**
148 * Fetches a document from docs.typo3.org.
149 *
150 * @param string $url
151 * @param string $key
152 * @param string $version
153 * @param string $language
154 * @return bool TRUE if fetch succeeded, otherwise FALSE
155 */
156 public function fetchDocument($url, $key, $version = 'latest', $language = 'default') {
157 $result = FALSE;
158 $url = rtrim($url, '/') . '/';
159
160 $packagePrefix = substr($key, strrpos($key, '.') + 1);
161 $languageSegment = str_replace('_', '-', strtolower($language));
162 $packageName = sprintf('%s-%s-%s.zip', $packagePrefix, $version, $languageSegment);
163 $packageUrl = $url . 'packages/' . $packageName;
164 $absolutePathToZipFile = GeneralUtility::getFileAbsFileName('typo3temp/Documentation/' . $packageName);
165
166 $packages = $this->getAvailablePackages($url);
167 if (empty($packages) || !isset($packages[$version][$language])) {
168 return FALSE;
169 }
170
171 // Check if a local version of the package is already present
172 $hasArchive = FALSE;
173 if (is_file($absolutePathToZipFile)) {
174 $localMd5 = md5_file($absolutePathToZipFile);
175 $remoteMd5 = $packages[$version][$language];
176 $hasArchive = $localMd5 === $remoteMd5;
177 }
178
179 if (!$hasArchive) {
180 /** @var $http \TYPO3\CMS\Core\Http\HttpRequest */
181 $http = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Http\HttpRequest::class, $packageUrl);
182 $response = $http->send();
183 if ($response->getStatus() == 200) {
184 GeneralUtility::writeFileToTypo3tempDir($absolutePathToZipFile, $response->getBody());
185 }
186 }
187
188 if (is_file($absolutePathToZipFile)) {
189 $absoluteDocumentPath = GeneralUtility::getFileAbsFileName('typo3conf/Documentation/');
190
191 $result = $this->unzipDocumentPackage($absolutePathToZipFile, $absoluteDocumentPath);
192
193 // Create a composer.json file
194 $absoluteCacheFilename = GeneralUtility::getFileAbsFileName('typo3temp/Documentation/documents.json');
195 $documents = json_decode(file_get_contents($absoluteCacheFilename), TRUE);
196 foreach ($documents as $document) {
197 if ($document['key'] === $key) {
198 $composerData = array(
199 'name' => $document['title'],
200 'type' => 'documentation',
201 'description' => 'TYPO3 ' . $document['type'],
202 );
203 $relativeComposerFilename = $key . '/' . $language . '/composer.json';
204 $absoluteComposerFilename = GeneralUtility::getFileAbsFileName('typo3conf/Documentation/' . $relativeComposerFilename);
205 GeneralUtility::writeFile($absoluteComposerFilename, json_encode($composerData));
206 break;
207 }
208 }
209 }
210
211 return $result;
212 }
213
214 /**
215 * Returns the available packages (version + language) for a given
216 * document on docs.typo3.org.
217 *
218 * @param string $url
219 * @return array
220 */
221 protected function getAvailablePackages($url) {
222 $packages = array();
223 $url = rtrim($url, '/') . '/';
224 $indexUrl = $url . 'packages/packages.xml';
225
226 $remote = GeneralUtility::getUrl($indexUrl);
227 if ($remote) {
228 $packages = $this->parsePackagesXML($remote);
229 }
230
231 return $packages;
232 }
233
234 /**
235 * Parses content of packages.xml into a suitable array.
236 *
237 * @param string $string: XML data to parse
238 * @throws \TYPO3\CMS\Documentation\Exception\XmlParser
239 * @return array Array representation of XML data
240 */
241 protected function parsePackagesXML($string) {
242 $data = json_decode(json_encode((array)simplexml_load_string($string)), TRUE);
243 if (count($data) !== 2) {
244 throw new \TYPO3\CMS\Documentation\Exception\XmlParser('Error in XML parser while decoding packages XML file.', 1374222437);
245 }
246
247 // SimpleXML does not properly handle arrays with only 1 item
248 if ($data['languagePackIndex']['languagepack'][0] === NULL) {
249 $data['languagePackIndex']['languagepack'] = array($data['languagePackIndex']['languagepack']);
250 }
251
252 $packages = array();
253 foreach ($data['languagePackIndex']['languagepack'] as $languagePack) {
254 $language = $languagePack['@attributes']['language'];
255 $version = $languagePack['@attributes']['version'];
256 $packages[$version][$language] = $languagePack['md5'];
257 }
258
259 return $packages;
260 }
261
262 /**
263 * Unzips a document package.
264 *
265 * @param string $file path to zip file
266 * @param string $path path to extract to
267 * @throws \TYPO3\CMS\Documentation\Exception\Document
268 * @return bool
269 */
270 protected function unzipDocumentPackage($file, $path) {
271 $zip = zip_open($file);
272 if (is_resource($zip)) {
273 $result = TRUE;
274
275 if (!is_dir($path)) {
276 GeneralUtility::mkdir_deep($path);
277 }
278
279 while (($zipEntry = zip_read($zip)) !== FALSE) {
280 $zipEntryName = zip_entry_name($zipEntry);
281 if (strpos($zipEntryName, '/') !== FALSE) {
282 $zipEntryPathSegments = explode('/', $zipEntryName);
283 $fileName = array_pop($zipEntryPathSegments);
284 // It is a folder, because the last segment is empty, let's create it
285 if (empty($fileName)) {
286 GeneralUtility::mkdir_deep($path, implode('/', $zipEntryPathSegments));
287 } else {
288 $absoluteTargetPath = GeneralUtility::getFileAbsFileName($path . implode('/', $zipEntryPathSegments) . '/' . $fileName);
289 if (trim($absoluteTargetPath) !== '') {
290 $return = GeneralUtility::writeFile(
291 $absoluteTargetPath, zip_entry_read($zipEntry, zip_entry_filesize($zipEntry))
292 );
293 if ($return === FALSE) {
294 throw new \TYPO3\CMS\Documentation\Exception\Document('Could not write file ' . $zipEntryName, 1374161546);
295 }
296 } else {
297 throw new \TYPO3\CMS\Documentation\Exception\Document('Could not write file ' . $zipEntryName, 1374161532);
298 }
299 }
300 } else {
301 throw new \TYPO3\CMS\Documentation\Exception\Document('Extension directory missing in zip file!', 1374161519);
302 }
303 }
304 } else {
305 throw new \TYPO3\CMS\Documentation\Exception\Document('Unable to open zip file ' . $file, 1374161508);
306 }
307
308 return $result;
309 }
310
311 }