59a873afe07830a145477c06f160f607c6c377e7
[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 $extensionPathWithoutTrailingSlash = rtrim($extDirPath, DIRECTORY_SEPARATOR);
268 if (is_link($extensionPathWithoutTrailingSlash)) {
269 $result = unlink($extensionPathWithoutTrailingSlash);
270 } else {
271 $result = GeneralUtility::rmdir($extDirPath, TRUE);
272 }
273 if ($result === FALSE) {
274 throw new ExtensionManagerException(
275 sprintf($this->languageService->getLL('fileHandling.couldNotRemoveDirectory'), $this->getRelativePath($extDirPath)),
276 1337280415
277 );
278 }
279 }
280
281 /**
282 * Constructs emConf and writes it to corresponding file
283 *
284 * @param array $extensionData
285 * @param string $rootPath
286 * @param Extension $extension
287 * @return void
288 */
289 protected function writeEmConfToFile(array $extensionData, $rootPath, Extension $extension = NULL) {
290 $emConfContent = $this->emConfUtility->constructEmConf($extensionData, $extension);
291 GeneralUtility::writeFile($rootPath . 'ext_emconf.php', $emConfContent);
292 }
293
294 /**
295 * Is the given path a valid path for extension installation
296 *
297 * @param string $path the absolute (!) path in question
298 * @return bool
299 */
300 public function isValidExtensionPath($path) {
301 $allowedPaths = Extension::returnAllowedInstallPaths();
302 foreach ($allowedPaths as $allowedPath) {
303 if (GeneralUtility::isFirstPartOfStr($path, $allowedPath)) {
304 return TRUE;
305 }
306 }
307 return FALSE;
308 }
309
310 /**
311 * Returns absolute path
312 *
313 * @param string $relativePath
314 * @throws ExtensionManagerException
315 * @return string
316 */
317 protected function getAbsolutePath($relativePath) {
318 $absolutePath = GeneralUtility::getFileAbsFileName(GeneralUtility::resolveBackPath(PATH_site . $relativePath));
319 if (empty($absolutePath)) {
320 throw new ExtensionManagerException('Illegal relative path given', 1350742864);
321 }
322 return $absolutePath;
323 }
324
325 /**
326 * Returns relative path
327 *
328 * @param string $absolutePath
329 * @return string
330 */
331 protected function getRelativePath($absolutePath) {
332 return PathUtility::stripPathSitePrefix($absolutePath);
333 }
334
335 /**
336 * Get extension path for an available or installed extension
337 *
338 * @param string $extension
339 * @return string
340 */
341 public function getAbsoluteExtensionPath($extension) {
342 $extension = $this->installUtility->enrichExtensionWithDetails($extension);
343 $absolutePath = $this->getAbsolutePath($extension['siteRelPath']);
344 return $absolutePath;
345 }
346
347 /**
348 * Get version of an available or installed extension
349 *
350 * @param string $extension
351 * @return string
352 */
353 public function getExtensionVersion($extension) {
354 $extensionData = $this->installUtility->enrichExtensionWithDetails($extension);
355 $version = $extensionData['version'];
356 return $version;
357 }
358
359 /**
360 * Create a zip file from an extension
361 *
362 * @param array $extension
363 * @return string Name and path of create zip file
364 */
365 public function createZipFileFromExtension($extension) {
366
367 $extensionPath = $this->getAbsoluteExtensionPath($extension);
368
369 // Add trailing slash to the extension path, getAllFilesAndFoldersInPath explicitly requires that.
370 $extensionPath = PathUtility::sanitizeTrailingSeparator($extensionPath);
371
372 $version = $this->getExtensionVersion($extension);
373 if (empty($version)) {
374 $version = '0.0.0';
375 }
376
377 $fileName = $this->getAbsolutePath('typo3temp/' . $extension . '_' . $version . '_' . date('YmdHi') . '.zip');
378
379 $zip = new \ZipArchive();
380 $zip->open($fileName, \ZipArchive::CREATE);
381
382 $excludePattern = $GLOBALS['TYPO3_CONF_VARS']['EXT']['excludeForPackaging'];
383
384 // Get all the files of the extension, but exclude the ones specified in the excludePattern
385 $files = GeneralUtility::getAllFilesAndFoldersInPath(
386 array(), // No files pre-added
387 $extensionPath, // Start from here
388 '', // Do not filter files by extension
389 TRUE, // Include subdirectories
390 PHP_INT_MAX, // Recursion level
391 $excludePattern // Files and directories to exclude.
392 );
393
394 // Make paths relative to extension root directory.
395 $files = GeneralUtility::removePrefixPathFromList($files, $extensionPath);
396
397 // Remove the one empty path that is the extension dir itself.
398 $files = array_filter($files);
399
400 foreach ($files as $file) {
401 $fullPath = $extensionPath . $file;
402 // Distinguish between files and directories, as creation of the archive
403 // fails on Windows when trying to add a directory with "addFile".
404 if (is_dir($fullPath)) {
405 $zip->addEmptyDir($file);
406 } else {
407 $zip->addFile($fullPath, $file);
408 }
409 }
410
411 $zip->close();
412 return $fileName;
413 }
414
415 /**
416 * Unzip an extension.zip.
417 *
418 * @param string $file path to zip file
419 * @param string $fileName file name
420 * @param string $pathType path type (Local, Global, System)
421 * @throws ExtensionManagerException
422 * @return void
423 */
424 public function unzipExtensionFromFile($file, $fileName, $pathType = 'Local') {
425 $extensionDir = $this->makeAndClearExtensionDir($fileName, $pathType);
426 $zip = zip_open($file);
427 if (is_resource($zip)) {
428 while (($zipEntry = zip_read($zip)) !== FALSE) {
429 if (strpos(zip_entry_name($zipEntry), '/') !== FALSE) {
430 $last = strrpos(zip_entry_name($zipEntry), '/');
431 $dir = substr(zip_entry_name($zipEntry), 0, $last);
432 $file = substr(zip_entry_name($zipEntry), strrpos(zip_entry_name($zipEntry), '/') + 1);
433 if (!is_dir($dir)) {
434 GeneralUtility::mkdir_deep($extensionDir . $dir);
435 }
436 if (strlen(trim($file)) > 0) {
437 $return = GeneralUtility::writeFile($extensionDir . $dir . '/' . $file, zip_entry_read($zipEntry, zip_entry_filesize($zipEntry)));
438 if ($return === FALSE) {
439 throw new ExtensionManagerException('Could not write file ' . $this->getRelativePath($file), 1344691048);
440 }
441 }
442 } else {
443 GeneralUtility::writeFile($extensionDir . zip_entry_name($zipEntry), zip_entry_read($zipEntry, zip_entry_filesize($zipEntry)));
444 }
445 }
446 } else {
447 throw new ExtensionManagerException('Unable to open zip file ' . $this->getRelativePath($file), 1344691049);
448 }
449 }
450
451 /**
452 * Sends a zip file to the browser and deletes it afterwards
453 *
454 * @param string $fileName
455 * @param string $downloadName
456 * @return void
457 */
458 public function sendZipFileToBrowserAndDelete($fileName, $downloadName = '') {
459 if ($downloadName === '') {
460 $downloadName = basename($fileName, '.zip');
461 }
462 header('Content-Type: application/zip');
463 header('Content-Length: ' . filesize($fileName));
464 header('Content-Disposition: attachment; filename="' . $downloadName . '.zip"');
465 readfile($fileName);
466 unlink($fileName);
467 die;
468 }
469
470 /**
471 * Sends the sql dump file to the browser and deletes it afterwards
472 *
473 * @param string $fileName
474 * @param string $downloadName
475 * @return void
476 */
477 public function sendSqlDumpFileToBrowserAndDelete($fileName, $downloadName = '') {
478 if ($downloadName === '') {
479 $downloadName = basename($fileName, '.sql');
480 } else {
481 $downloadName = basename($downloadName, '.sql');
482 }
483 header('Content-Type: text');
484 header('Content-Length: ' . filesize($fileName));
485 header('Content-Disposition: attachment; filename="' . $downloadName . '.sql"');
486 readfile($fileName);
487 unlink($fileName);
488 die;
489 }
490
491 }