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