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