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