[BUGFIX] Labels in extension manager not loaded
[Packages/TYPO3.CMS.git] / typo3 / sysext / extensionmanager / Classes / Utility / FileHandlingUtility.php
1 <?php
2 namespace TYPO3\CMS\Extensionmanager\Utility;
3 use TYPO3\CMS\Core\Utility\PathUtility;
4 use TYPO3\CMS\Core\Utility\GeneralUtility;
5 use TYPO3\CMS\Extensionmanager\Domain\Model\Extension;
6 use TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException;
7 use TYPO3\CMS\Lang\LanguageService;
8
9 /**
10 * This file is part of the TYPO3 CMS project.
11 *
12 * It is free software; you can redistribute it and/or modify it under
13 * the terms of the GNU General Public License, either version 2
14 * of the License, or any later version.
15 *
16 * For the full copyright and license information, please read the
17 * LICENSE.txt file that was distributed with this source code.
18 *
19 * The TYPO3 project - inspiring people to share!
20 */
21 /**
22 * Utility for dealing with files and folders
23 *
24 * @author Susanne Moog <susanne.moog@typo3.org>
25 */
26 class FileHandlingUtility implements \TYPO3\CMS\Core\SingletonInterface {
27
28 /**
29 * @var \TYPO3\CMS\Extensionmanager\Utility\EmConfUtility
30 * @inject
31 */
32 protected $emConfUtility;
33
34 /**
35 * @var \TYPO3\CMS\Extensionmanager\Utility\InstallUtility
36 * @inject
37 */
38 protected $installUtility;
39
40 /**
41 * @var \TYPO3\CMS\Lang\LanguageService
42 * @inject
43 */
44 protected $languageService;
45
46 /**
47 * Initialize method - loads language file
48 *
49 * @return void
50 */
51 public function initializeObject() {
52 $this->languageService->includeLLFile('EXT:extensionmanager/Resources/Private/Language/locallang.xlf');
53 }
54
55 /**
56 * Unpack an extension in t3x data format and write files
57 *
58 * @param array $extensionData
59 * @param Extension $extension
60 * @param string $pathType
61 * @return void
62 */
63 public function unpackExtensionFromExtensionDataArray(array $extensionData, Extension $extension = NULL, $pathType = 'Local') {
64 $extensionDir = $this->makeAndClearExtensionDir($extensionData['extKey'], $pathType);
65 $files = $this->extractFilesArrayFromExtensionData($extensionData);
66 $directories = $this->extractDirectoriesFromExtensionData($files);
67 $files = array_diff_key($files, array_flip($directories));
68 $this->createDirectoriesForExtensionFiles($directories, $extensionDir);
69 $this->writeExtensionFiles($files, $extensionDir);
70 $this->writeEmConfToFile($extensionData, $extensionDir, $extension);
71 }
72
73 /**
74 * Extract needed directories from given extensionDataFilesArray
75 *
76 * @param array $files
77 * @return array
78 */
79 protected function extractDirectoriesFromExtensionData(array $files) {
80 $directories = array();
81 foreach ($files as $filePath => $file) {
82 preg_match('/(.*)\\//', $filePath, $matches);
83 if (!empty($matches[0])) {
84 $directories[] = $matches[0];
85 }
86 }
87 return array_unique($directories);
88 }
89
90 /**
91 * Returns the "FILES" part from the data array
92 *
93 * @param array $extensionData
94 * @return mixed
95 */
96 protected function extractFilesArrayFromExtensionData(array $extensionData) {
97 return $extensionData['FILES'];
98 }
99
100 /**
101 * Loops over an array of directories and creates them in the given root path
102 * It also creates nested directory structures
103 *
104 * @param array $directories
105 * @param string $rootPath
106 * @return void
107 */
108 protected function createDirectoriesForExtensionFiles(array $directories, $rootPath) {
109 foreach ($directories as $directory) {
110 $this->createNestedDirectory($rootPath . $directory);
111 }
112 }
113
114 /**
115 * Wrapper for utility method to create directory recusively
116 *
117 * @param string $directory Absolute path
118 * @throws ExtensionManagerException
119 */
120 protected function createNestedDirectory($directory) {
121 try {
122 GeneralUtility::mkdir_deep($directory);
123 } catch(\RuntimeException $exception) {
124 throw new ExtensionManagerException(
125 sprintf($this->languageService->getLL('fileHandling.couldNotCreateDirectory'), $this->getRelativePath($directory)),
126 1337280416
127 );
128 }
129
130 }
131
132 /**
133 * Loops over an array of files and writes them to the given rootPath
134 *
135 * @param array $files
136 * @param string $rootPath
137 * @return void
138 */
139 protected function writeExtensionFiles(array $files, $rootPath) {
140 foreach ($files as $file) {
141 GeneralUtility::writeFile($rootPath . $file['name'], $file['content']);
142 }
143 }
144
145 /**
146 * Removes the current extension of $type and creates the base folder for
147 * the new one (which is going to be imported)
148 *
149 * @param string $extensionKey
150 * @param string $pathType Extension installation scope (Local,Global,System)
151 * @throws ExtensionManagerException
152 * @return string
153 */
154 protected function makeAndClearExtensionDir($extensionKey, $pathType = 'Local') {
155 $extDirPath = $this->getExtensionDir($extensionKey, $pathType);
156 if (is_dir($extDirPath)) {
157 $this->removeDirectory($extDirPath);
158 }
159 $this->addDirectory($extDirPath);
160
161 return $extDirPath;
162 }
163
164 /**
165 * Returns the installation directory for an extension depending on the installation scope
166 *
167 * @param string $extensionKey
168 * @param string $pathType Extension installation scope (Local,Global,System)
169 * @return string
170 * @throws ExtensionManagerException
171 */
172 public function getExtensionDir($extensionKey, $pathType = 'Local') {
173 $paths = Extension::returnInstallPaths();
174 $path = $paths[$pathType];
175 if (!$path || !is_dir($path) || !$extensionKey) {
176 throw new ExtensionManagerException(
177 sprintf($this->languageService->getLL('fileHandling.installPathWasNoDirectory'), $this->getRelativePath($path)),
178 1337280417
179 );
180 }
181
182 return $path . $extensionKey . '/';
183 }
184
185 /**
186 * Add specified directory
187 *
188 * @param string $extDirPath
189 * @throws ExtensionManagerException
190 * @return void
191 */
192 protected function addDirectory($extDirPath) {
193 GeneralUtility::mkdir($extDirPath);
194 if (!is_dir($extDirPath)) {
195 throw new ExtensionManagerException(
196 sprintf($this->languageService->getLL('fileHandling.couldNotCreateDirectory'), $this->getRelativePath($extDirPath)),
197 1337280418
198 );
199 }
200 }
201
202 /**
203 * Creates directories configured in ext_emconf.php if not already present
204 *
205 * @param array $extension
206 */
207 public function ensureConfiguredDirectoriesExist(array $extension) {
208 foreach ($this->getAbsolutePathsToConfiguredDirectories($extension) as $directory) {
209 if (!$this->directoryExists($directory)) {
210 $this->createNestedDirectory($directory);
211 }
212 }
213 }
214
215 /**
216 * Wrapper method for directory existance check
217 *
218 * @param string $directory
219 * @return boolean
220 */
221 protected function directoryExists($directory) {
222 return is_dir($directory);
223 }
224
225 /**
226 * Checks configuration and returns an array of absolute paths that should be created
227 *
228 * @param array $extension
229 * @return array
230 */
231 protected function getAbsolutePathsToConfiguredDirectories(array $extension) {
232 $requestedDirectories = array();
233 $requestUploadFolder = isset($extension['uploadfolder']) ? (boolean)$extension['uploadfolder'] : FALSE;
234 if ($requestUploadFolder) {
235 $requestedDirectories[] = $this->getAbsolutePath($this->getPathToUploadFolder($extension));
236 }
237
238 $requestCreateDirectories = empty($extension['createDirs']) ? FALSE : (string)$extension['createDirs'];
239 if ($requestCreateDirectories) {
240 foreach (GeneralUtility::trimExplode(',', $extension['createDirs']) as $directoryToCreate) {
241 $requestedDirectories[] = $this->getAbsolutePath($directoryToCreate);
242 }
243 }
244
245 return $requestedDirectories;
246 }
247
248 /**
249 * Upload folders always reside in “uploads/tx_[extKey-with-no-underscore]”
250 *
251 * @param array $extension
252 * @return string
253 */
254 protected function getPathToUploadFolder($extension) {
255 return 'uploads/tx_' . str_replace('_', '', $extension['key']) . '/';
256 }
257
258 /**
259 * Remove specified directory
260 *
261 * @param string $extDirPath
262 * @throws ExtensionManagerException
263 * @return void
264 */
265 public function removeDirectory($extDirPath) {
266 $extensionPathWithoutTrailingSlash = rtrim($extDirPath, DIRECTORY_SEPARATOR);
267 if (is_link($extensionPathWithoutTrailingSlash)) {
268 $result = unlink($extensionPathWithoutTrailingSlash);
269 } else {
270 $result = GeneralUtility::rmdir($extDirPath, TRUE);
271 }
272 if ($result === FALSE) {
273 throw new ExtensionManagerException(
274 sprintf($this->languageService->getLL('fileHandling.couldNotRemoveDirectory'), $this->getRelativePath($extDirPath)),
275 1337280415
276 );
277 }
278 }
279
280 /**
281 * Constructs emConf and writes it to corresponding file
282 *
283 * @param array $extensionData
284 * @param string $rootPath
285 * @param Extension $extension
286 * @return void
287 */
288 protected function writeEmConfToFile(array $extensionData, $rootPath, Extension $extension = NULL) {
289 $emConfContent = $this->emConfUtility->constructEmConf($extensionData, $extension);
290 GeneralUtility::writeFile($rootPath . 'ext_emconf.php', $emConfContent);
291 }
292
293 /**
294 * Is the given path a valid path for extension installation
295 *
296 * @param string $path the absolute (!) path in question
297 * @return boolean
298 */
299 public function isValidExtensionPath($path) {
300 $allowedPaths = Extension::returnAllowedInstallPaths();
301 foreach ($allowedPaths as $allowedPath) {
302 if (GeneralUtility::isFirstPartOfStr($path, $allowedPath)) {
303 return TRUE;
304 }
305 }
306 return FALSE;
307 }
308
309 /**
310 * Returns absolute path
311 *
312 * @param string $relativePath
313 * @throws ExtensionManagerException
314 * @return string
315 */
316 protected function getAbsolutePath($relativePath) {
317 $absolutePath = GeneralUtility::getFileAbsFileName(GeneralUtility::resolveBackPath(PATH_site . $relativePath));
318 if (empty($absolutePath)) {
319 throw new ExtensionManagerException('Illegal relative path given', 1350742864);
320 }
321 return $absolutePath;
322 }
323
324 /**
325 * Returns relative path
326 *
327 * @param string $absolutePath
328 * @return string
329 */
330 protected function getRelativePath($absolutePath) {
331 return PathUtility::stripPathSitePrefix($absolutePath);
332 }
333
334 /**
335 * Get extension path for an available or installed extension
336 *
337 * @param string $extension
338 * @return string
339 */
340 public function getAbsoluteExtensionPath($extension) {
341 $extension = $this->installUtility->enrichExtensionWithDetails($extension);
342 $absolutePath = $this->getAbsolutePath($extension['siteRelPath']);
343 return $absolutePath;
344 }
345
346 /**
347 * Get version of an available or installed extension
348 *
349 * @param string $extension
350 * @return string
351 */
352 public function getExtensionVersion($extension) {
353 $extensionData = $this->installUtility->enrichExtensionWithDetails($extension);
354 $version = $extensionData['version'];
355 return $version;
356 }
357
358 /**
359 * Create a zip file from an extension
360 *
361 * @param array $extension
362 * @return string Name and path of create zip file
363 */
364 public function createZipFileFromExtension($extension) {
365
366 $extensionPath = $this->getAbsoluteExtensionPath($extension);
367
368 // Add trailing slash to the extension path, getAllFilesAndFoldersInPath explicitly requires that.
369 $extensionPath = PathUtility::sanitizeTrailingSeparator($extensionPath);
370
371 $version = $this->getExtensionVersion($extension);
372 if (empty($version)) {
373 $version = '0.0.0';
374 }
375
376 $fileName = $this->getAbsolutePath('typo3temp/' . $extension . '_' . $version . '_' . date('YmdHi') . '.zip');
377
378 $zip = new \ZipArchive();
379 $zip->open($fileName, \ZipArchive::CREATE);
380
381 $excludePattern = $GLOBALS['TYPO3_CONF_VARS']['EXT']['excludeForPackaging'];
382
383 // Get all the files of the extension, but exclude the ones specified in the excludePattern
384 $files = GeneralUtility::getAllFilesAndFoldersInPath(
385 array(), // No files pre-added
386 $extensionPath, // Start from here
387 '', // Do not filter files by extension
388 TRUE, // Include subdirectories
389 PHP_INT_MAX, // Recursion level
390 $excludePattern // Files and directories to exclude.
391 );
392
393 // Make paths relative to extension root directory.
394 $files = GeneralUtility::removePrefixPathFromList($files, $extensionPath);
395
396 // Remove the one empty path that is the extension dir itself.
397 $files = array_filter($files);
398
399 foreach ($files as $file) {
400 $fullPath = $extensionPath . $file;
401 // Distinguish between files and directories, as creation of the archive
402 // fails on Windows when trying to add a directory with "addFile".
403 if (is_dir($fullPath)) {
404 $zip->addEmptyDir($file);
405 } else {
406 $zip->addFile($fullPath, $file);
407 }
408 }
409
410 $zip->close();
411 return $fileName;
412 }
413
414 /**
415 * Unzip an extension.zip.
416 *
417 * @param string $file path to zip file
418 * @param string $fileName file name
419 * @param string $pathType path type (Local, Global, System)
420 * @throws ExtensionManagerException
421 * @return void
422 */
423 public function unzipExtensionFromFile($file, $fileName, $pathType = 'Local') {
424 $extensionDir = $this->makeAndClearExtensionDir($fileName, $pathType);
425 $zip = zip_open($file);
426 if (is_resource($zip)) {
427 while (($zipEntry = zip_read($zip)) !== FALSE) {
428 if (strpos(zip_entry_name($zipEntry), '/') !== FALSE) {
429 $last = strrpos(zip_entry_name($zipEntry), '/');
430 $dir = substr(zip_entry_name($zipEntry), 0, $last);
431 $file = substr(zip_entry_name($zipEntry), strrpos(zip_entry_name($zipEntry), '/') + 1);
432 if (!is_dir($dir)) {
433 GeneralUtility::mkdir_deep($extensionDir . $dir);
434 }
435 if (strlen(trim($file)) > 0) {
436 $return = GeneralUtility::writeFile($extensionDir . $dir . '/' . $file, zip_entry_read($zipEntry, zip_entry_filesize($zipEntry)));
437 if ($return === FALSE) {
438 throw new ExtensionManagerException('Could not write file ' . $this->getRelativePath($file), 1344691048);
439 }
440 }
441 } else {
442 GeneralUtility::writeFile($extensionDir . zip_entry_name($zipEntry), zip_entry_read($zipEntry, zip_entry_filesize($zipEntry)));
443 }
444 }
445 } else {
446 throw new ExtensionManagerException('Unable to open zip file ' . $this->getRelativePath($file), 1344691049);
447 }
448 }
449
450 /**
451 * Sends a zip file to the browser and deletes it afterwards
452 *
453 * @param string $fileName
454 * @param string $downloadName
455 * @return void
456 */
457 public function sendZipFileToBrowserAndDelete($fileName, $downloadName = '') {
458 if ($downloadName === '') {
459 $downloadName = basename($fileName, '.zip');
460 }
461 header('Content-Type: application/zip');
462 header('Content-Length: ' . filesize($fileName));
463 header('Content-Disposition: attachment; filename="' . $downloadName . '.zip"');
464 readfile($fileName);
465 unlink($fileName);
466 die;
467 }
468
469 /**
470 * Sends the sql dump file to the browser and deletes it afterwards
471 *
472 * @param string $fileName
473 * @param string $downloadName
474 * @return void
475 */
476 public function sendSqlDumpFileToBrowserAndDelete($fileName, $downloadName = '') {
477 if ($downloadName === '') {
478 $downloadName = basename($fileName, '.sql');
479 } else {
480 $downloadName = basename($downloadName, '.sql');
481 }
482 header('Content-Type: text');
483 header('Content-Length: ' . filesize($fileName));
484 header('Content-Disposition: attachment; filename="' . $downloadName . '.sql"');
485 readfile($fileName);
486 unlink($fileName);
487 die;
488 }
489
490 }