6f11491ffb27b5c093ad181376eb1e7804d03269
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Utility / File / BasicFileUtility.php
1 <?php
2 namespace TYPO3\CMS\Core\Utility\File;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Utility\GeneralUtility;
18 use TYPO3\CMS\Core\Utility\PathUtility;
19
20 /**
21 * Contains class with basic file management functions
22 *
23 * Contains functions for management, validation etc of files in TYPO3,
24 * using the concepts of web- and ftp-space. Please see the comment for the
25 * init() function
26 *
27 * Note: All methods in this class should not be used anymore since TYPO3 6.0.
28 * Please use corresponding TYPO3\\CMS\\Core\\Resource\\ResourceStorage
29 * (fetched via BE_USERS->getFileStorages()), as all functions should be
30 * found there (in a cleaner manner).
31 */
32 class BasicFileUtility
33 {
34 /**
35 * @var string
36 */
37 const UNSAFE_FILENAME_CHARACTER_EXPRESSION = '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF';
38
39 /**
40 * Prefix which will be prepended the file when using the getUniqueName-function
41 *
42 * @var string
43 */
44 public $getUniqueNamePrefix = '';
45
46 /**
47 * This number decides the highest allowed appended number used on a filename before we use naming with unique strings
48 *
49 * @var int
50 */
51 public $maxNumber = 99;
52
53 /**
54 * This number decides how many characters out of a unique MD5-hash that is appended to a filename if getUniqueName is asked to find an available filename.
55 *
56 * @var int
57 */
58 public $uniquePrecision = 6;
59
60 /**
61 * Temp-foldername. A folder in the root of one of the mounts with this name is regarded a TEMP-folder (used for upload from clipboard)
62 *
63 * @var string
64 */
65 public $tempFN = '_temp_';
66
67 /**
68 * @var array
69 */
70 public $f_ext = array();
71
72 /**
73 * See comment in header
74 *
75 * @var array
76 */
77 public $mounts = array();
78
79 /**
80 * See comment in header
81 *
82 * @var string
83 */
84 public $webPath = '';
85
86 /**
87 * Set to DOCUMENT_ROOT.
88 *
89 * @var bool
90 */
91 public $isInit = 0;
92
93 /**
94 * @var \TYPO3\CMS\Core\Charset\CharsetConverter
95 */
96 public $csConvObj;
97
98 /**********************************
99 *
100 * Checking functions
101 *
102 **********************************/
103 /**
104 * Constructor
105 * This function should be called to initialise the internal arrays $this->mounts and $this->f_ext
106 *
107 * A typical example of the array $mounts is this:
108 * $mounts[xx][path] = (..a mounted path..)
109 * the 'xx'-keys is just numerical from zero. There are also a [name] and [type] value that just denotes the mountname and type. Not used for athentication here.
110 * $this->mounts is traversed in the function checkPathAgainstMounts($thePath), and it is checked that $thePath is actually below one of the mount-paths
111 * The mountpaths are with a trailing '/'. $thePath must be with a trailing '/' also!
112 * As you can see, $this->mounts is very critical! This is the array that decides where the user will be allowed to copy files!!
113 *
114 * A typical example of the array $f_ext is this:
115 * $f_ext['webspace']['allow']='';
116 * $f_ext['webspace']['deny']= PHP_EXTENSIONS_DEFAULT;
117 * $f_ext['ftpspace']['allow']='*';
118 * $f_ext['ftpspace']['deny']='';
119 * The control of fileextensions goes in two catagories. Webspace and Ftpspace. Webspace is folders accessible from a webbrowser (below TYPO3_DOCUMENT_ROOT) and ftpspace is everything else.
120 * The control is done like this: If an extension matches 'allow' then the check returns TRUE. If not and an extension matches 'deny' then the check return FALSE. If no match at all, returns TRUE.
121 * You list extensions comma-separated. If the value is a '*' every extension is allowed
122 * The list is case-insensitive when used in this class (see init())
123 * Typically TYPO3_CONF_VARS['BE']['fileExtensions'] would be passed along as $f_ext.
124 *
125 * Example:
126 * $basicff->init(array(), $GLOBALS['TYPO3_CONF_VARS']['BE']['fileExtensions']);
127 *
128 * @param array Not in use anymore
129 * @param array Array with information about allowed and denied file extensions. Typically passed: $GLOBALS['TYPO3_CONF_VARS']['BE']['fileExtensions']
130 * @return void
131 */
132 public function init($mounts, $f_ext)
133 {
134 $this->f_ext['webspace']['allow'] = GeneralUtility::uniqueList(strtolower($f_ext['webspace']['allow']));
135 $this->f_ext['webspace']['deny'] = GeneralUtility::uniqueList(strtolower($f_ext['webspace']['deny']));
136 $this->f_ext['ftpspace']['allow'] = GeneralUtility::uniqueList(strtolower($f_ext['ftpspace']['allow']));
137 $this->f_ext['ftpspace']['deny'] = GeneralUtility::uniqueList(strtolower($f_ext['ftpspace']['deny']));
138
139 $this->mounts = (!empty($mounts) ? $mounts : array());
140 $this->webPath = GeneralUtility::getIndpEnv('TYPO3_DOCUMENT_ROOT');
141 $this->isInit = 1;
142 }
143
144 /**
145 * Checks if a $iconkey (fileextension) is allowed according to $this->f_ext.
146 *
147 * @param string The extension to check, eg. "php" or "html" etc.
148 * @param string Either "webspage" or "ftpspace" - points to a key in $this->f_ext
149 * @return bool TRUE if file extension is allowed.
150 * @todo Deprecate, but still in use by checkIfAllowed()
151 * @deprecated but still in use in the Core. Don't use in your extensions!
152 */
153 public function is_allowed($iconkey, $type)
154 {
155 if (isset($this->f_ext[$type])) {
156 $ik = strtolower($iconkey);
157 if ($ik) {
158 // If the extension is found amongst the allowed types, we return TRUE immediately
159 if ($this->f_ext[$type]['allow'] == '*' || GeneralUtility::inList($this->f_ext[$type]['allow'], $ik)) {
160 return true;
161 }
162 // If the extension is found amongst the denied types, we return FALSE immediately
163 if ($this->f_ext[$type]['deny'] == '*' || GeneralUtility::inList($this->f_ext[$type]['deny'], $ik)) {
164 return false;
165 }
166 // If no match we return TRUE
167 return true;
168 } else {
169 // If no extension:
170 if ($this->f_ext[$type]['allow'] == '*') {
171 return true;
172 }
173 if ($this->f_ext[$type]['deny'] == '*') {
174 return false;
175 }
176 return true;
177 }
178 }
179 return false;
180 }
181
182 /**
183 * Returns TRUE if you can operate of ANY file ('*') in the space $theDest is in ('webspace' / 'ftpspace')
184 *
185 * @param string Absolute path
186 * @return bool
187 * @todo Deprecate: but still in use by through func_unzip in ExtendedFileUtility
188 * @deprecated but still in use in the Core. Don't use in your extensions!
189 */
190 public function checkIfFullAccess($theDest)
191 {
192 $type = $this->is_webpath($theDest) ? 'webspace' : 'ftpspace';
193 if (isset($this->f_ext[$type])) {
194 if ((string)$this->f_ext[$type]['deny'] == '' || $this->f_ext[$type]['allow'] == '*') {
195 return true;
196 }
197 }
198 }
199
200 /**
201 * Checks if $this->webPath (should be TYPO3_DOCUMENT_ROOT) is in the first part of $path
202 * Returns TRUE also if $this->init is not set or if $path is empty...
203 *
204 * @param string Absolute path to check
205 * @return bool
206 * @todo Deprecate, but still in use by DataHandler
207 * @deprecated but still in use in the Core. Don't use in your extensions!
208 */
209 public function is_webpath($path)
210 {
211 if ($this->isInit) {
212 $testPath = $this->slashPath($path);
213 $testPathWeb = $this->slashPath($this->webPath);
214 if ($testPathWeb && $testPath) {
215 return GeneralUtility::isFirstPartOfStr($testPath, $testPathWeb);
216 }
217 }
218 return true;
219 }
220
221 /**
222 * If the filename is given, check it against the TYPO3_CONF_VARS[BE][fileDenyPattern] +
223 * Checks if the $ext fileextension is allowed in the path $theDest (this is based on whether $theDest is below the $this->webPath)
224 *
225 * @param string File extension, eg. "php" or "html
226 * @param string Absolute path for which to test
227 * @param string Filename to check against TYPO3_CONF_VARS[BE][fileDenyPattern]
228 * @return bool TRUE if extension/filename is allowed
229 * @todo Deprecate, but still in use by DataHandler
230 * @deprecated but still in use in the Core. Don't use in your extensions!
231 */
232 public function checkIfAllowed($ext, $theDest, $filename = '')
233 {
234 return GeneralUtility::verifyFilenameAgainstDenyPattern($filename) && $this->is_allowed($ext, ($this->is_webpath($theDest) ? 'webspace' : 'ftpspace'));
235 }
236
237 /**
238 * Cleans $theDir for slashes in the end of the string and returns the new path, if it exists on the server.
239 *
240 * @param string Directory path to check
241 * @return string Returns the cleaned up directory name if OK, otherwise FALSE.
242 * @todo Deprecate: but still in use by getUniqueName (used by DataHandler)
243 * @deprecated but still in use in the Core. Don't use in your extensions!
244 */
245 public function is_directory($theDir)
246 {
247 // @todo: should go into the LocalDriver in a protected way (not important to the outside world)
248 if (GeneralUtility::validPathStr($theDir)) {
249 $theDir = PathUtility::getCanonicalPath($theDir);
250 if (@is_dir($theDir)) {
251 return $theDir;
252 }
253 }
254 return false;
255 }
256
257 /**
258 * Returns the destination path/filename of a unique filename/foldername in that path.
259 * If $theFile exists in $theDest (directory) the file have numbers appended up to $this->maxNumber. Hereafter a unique string will be appended.
260 * This function is used by fx. TCEmain when files are attached to records and needs to be uniquely named in the uploads/* folders
261 *
262 * @param string The input filename to check
263 * @param string The directory for which to return a unique filename for $theFile. $theDest MUST be a valid directory. Should be absolute.
264 * @param bool If set the filename is returned with the path prepended without checking whether it already existed!
265 * @return string The destination absolute filepath (not just the name!) of a unique filename/foldername in that path.
266 * @see \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue()
267 * @todo Deprecate, but still in use by the Core (DataHandler...)
268 * @deprecated but still in use in the Core. Don't use in your extensions!
269 */
270 public function getUniqueName($theFile, $theDest, $dontCheckForUnique = 0)
271 {
272 // @todo: should go into the LocalDriver in a protected way (not important to the outside world)
273 $theDest = $this->is_directory($theDest);
274 // $theDest is cleaned up
275 $origFileInfo = GeneralUtility::split_fileref($theFile);
276 // Fetches info about path, name, extension of $theFile
277 if ($theDest) {
278 if ($this->getUniqueNamePrefix) {
279 // Adds prefix
280 $origFileInfo['file'] = $this->getUniqueNamePrefix . $origFileInfo['file'];
281 $origFileInfo['filebody'] = $this->getUniqueNamePrefix . $origFileInfo['filebody'];
282 }
283 // Check if the file exists and if not - return the filename...
284 $fileInfo = $origFileInfo;
285 $theDestFile = $theDest . '/' . $fileInfo['file'];
286 // The destinations file
287 if (!file_exists($theDestFile) || $dontCheckForUnique) {
288 // If the file does NOT exist we return this filename
289 return $theDestFile;
290 }
291 // Well the filename in its pure form existed. Now we try to append numbers / unique-strings and see if we can find an available filename...
292 $theTempFileBody = preg_replace('/_[0-9][0-9]$/', '', $origFileInfo['filebody']);
293 // This removes _xx if appended to the file
294 $theOrigExt = $origFileInfo['realFileext'] ? '.' . $origFileInfo['realFileext'] : '';
295 for ($a = 1; $a <= $this->maxNumber + 1; $a++) {
296 if ($a <= $this->maxNumber) {
297 // First we try to append numbers
298 $insert = '_' . sprintf('%02d', $a);
299 } else {
300 // .. then we try unique-strings...
301 $insert = '_' . substr(md5(uniqid('', true)), 0, $this->uniquePrecision);
302 }
303 $theTestFile = $theTempFileBody . $insert . $theOrigExt;
304 $theDestFile = $theDest . '/' . $theTestFile;
305 // The destinations file
306 if (!file_exists($theDestFile)) {
307 // If the file does NOT exist we return this filename
308 return $theDestFile;
309 }
310 }
311 }
312 }
313
314 /**
315 * Checks if $thePath is a path under one of the paths in $this->mounts
316 * See comment in the header of this class.
317 *
318 * @param string $thePath MUST HAVE a trailing '/' in order to match correctly with the mounts
319 * @return string The key to the first mount found, otherwise nothing is returned.
320 * @see init()
321 * @todo: deprecate this function, now done in the Storage object. But still in use by impexp and ExtendedFileUtility
322 * @deprecated but still in use in the Core. Don't use in your extensions!
323 */
324 public function checkPathAgainstMounts($thePath)
325 {
326 if ($thePath && GeneralUtility::validPathStr($thePath) && is_array($this->mounts)) {
327 foreach ($this->mounts as $k => $val) {
328 if (GeneralUtility::isFirstPartOfStr($thePath, $val['path'])) {
329 return $k;
330 }
331 }
332 }
333 }
334
335 /**
336 * Find first web folder (relative to PATH_site.'fileadmin') in filemounts array
337 *
338 * @return string The key to the first mount inside PATH_site."fileadmin" found, otherwise nothing is returned.
339 * @todo: deprecate this function. But still in use by impexp
340 * @deprecated but still in use in the Core. Don't use in your extensions!
341 */
342 public function findFirstWebFolder()
343 {
344 // @todo: where and when to use this function?
345 if (is_array($this->mounts)) {
346 foreach ($this->mounts as $k => $val) {
347 if (GeneralUtility::isFirstPartOfStr($val['path'], PATH_site . $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'])) {
348 return $k;
349 }
350 }
351 }
352 }
353
354 /*********************
355 *
356 * Cleaning functions
357 *
358 *********************/
359 /**
360 * Returns a string which has a slash '/' appended if it doesn't already have that slash
361 *
362 * @param string Input string
363 * @return string Output string with a slash in the end (if not already there)
364 * @todo Deprecate, but still in use by is_webpath, used by DataHandler
365 * @deprecated but still in use in the Core. Don't use in your extensions!
366 */
367 public function slashPath($path)
368 {
369 // @todo: should go into the LocalDriver in a protected way (not important to the outside world)
370 // @todo: should be done with rtrim($path, '/') . '/';
371 if (substr($path, -1) != '/') {
372 return $path . '/';
373 }
374 return $path;
375 }
376
377 /**
378 * Returns a string where any character not matching [.a-zA-Z0-9_-] is substituted by '_'
379 * Trailing dots are removed
380 *
381 * @param string $fileName Input string, typically the body of a filename
382 * @param string $charset Charset of the a filename (defaults to current charset; depending on context)
383 * @return string Output string with any characters not matching [.a-zA-Z0-9_-] is substituted by '_' and trailing dots removed
384 * @todo Deprecate, but still in use by the core
385 * @deprecated but still in use in the Core. Don't use in your extensions!
386 */
387 public function cleanFileName($fileName, $charset = '')
388 {
389 // Handle UTF-8 characters
390 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']) {
391 // allow ".", "-", 0-9, a-z, A-Z and everything beyond U+C0 (latin capital letter a with grave)
392 $cleanFileName = preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . ']/u', '_', trim($fileName));
393 } else {
394 // Get conversion object or initialize if needed
395 if (!is_object($this->csConvObj)) {
396 $this->csConvObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Charset\CharsetConverter::class);
397 }
398 // Define character set
399 if (!$charset) {
400 if (TYPO3_MODE == 'FE') {
401 $charset = $GLOBALS['TSFE']->renderCharset;
402 } else {
403 // Backend
404 $charset = 'utf-8';
405 }
406 }
407 // If a charset was found, convert filename
408 if ($charset) {
409 $fileName = $this->csConvObj->specCharsToASCII($charset, $fileName);
410 }
411 // Replace unwanted characters by underscores
412 $cleanFileName = preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . '\\xC0-\\xFF]/', '_', trim($fileName));
413 }
414 // Strip trailing dots and return
415 return rtrim($cleanFileName, '.');
416 }
417 }