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