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