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