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