6e9ab6061c327f0e5b7a4ef522e687d21d82642e
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Utility / File / BasicFileUtility.php
1 <?php
2 namespace TYPO3\CMS\Core\Utility\File;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 1999-2013 Kasper Skårhøj (kasperYYYY@typo3.com)
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 * A copy is found in the text file GPL.txt and important notices to the license
19 * from the author is found in LICENSE.txt distributed with these scripts.
20 *
21 *
22 * This script is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
26 *
27 * This copyright notice MUST APPEAR in all copies of the script!
28 ***************************************************************/
29
30 use TYPO3\CMS\Core\Utility\GeneralUtility;
31 use TYPO3\CMS\Core\Utility\PathUtility;
32
33 /**
34 * Contains class with basic file management functions
35 *
36 * Contains functions for management, validation etc of files in TYPO3,
37 * using the concepts of web- and ftp-space. Please see the comment for the
38 * init() function
39 *
40 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
41 */
42 class BasicFileUtility {
43
44 /**
45 * @todo Define visibility
46 */
47 public $getUniqueNamePrefix = '';
48
49 // Prefix which will be prepended the file when using the getUniqueName-function
50 /**
51 * @todo Define visibility
52 */
53 public $maxNumber = 99;
54
55 // This number decides the highest allowed appended number used on a filename before we use naming with unique strings
56 /**
57 * @todo Define visibility
58 */
59 public $uniquePrecision = 6;
60
61 // 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.
62 /**
63 * @todo Define visibility
64 */
65 public $maxInputNameLen = 60;
66
67 // This is the maximum length of names treated by cleanFileName()
68 /**
69 * @todo Define visibility
70 */
71 public $tempFN = '_temp_';
72
73 // 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)
74 // internal
75 /**
76 * @todo Define visibility
77 */
78 public $f_ext = array();
79
80 // See comment in header
81 /**
82 * @todo Define visibility
83 */
84 public $mounts = array();
85
86 // See comment in header
87 /**
88 * @todo Define visibility
89 */
90 public $webPath = '';
91
92 // Set to DOCUMENT_ROOT.
93 /**
94 * @todo Define visibility
95 */
96 public $isInit = 0;
97
98 // Set to TRUE after init()/start();
99 /**********************************
100 *
101 * Checking functions
102 *
103 **********************************/
104 /**
105 * Constructor
106 * This function should be called to initialise the internal arrays $this->mounts and $this->f_ext
107 *
108 * A typical example of the array $mounts is this:
109 * $mounts[xx][path] = (..a mounted path..)
110 * 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.
111 * $this->mounts is traversed in the function checkPathAgainstMounts($thePath), and it is checked that $thePath is actually below one of the mount-paths
112 * The mountpaths are with a trailing '/'. $thePath must be with a trailing '/' also!
113 * As you can see, $this->mounts is very critical! This is the array that decides where the user will be allowed to copy files!!
114 * Typically the global var $WEBMOUNTS would be passed along as $mounts
115 *
116 * A typical example of the array $f_ext is this:
117 * $f_ext['webspace']['allow']='';
118 * $f_ext['webspace']['deny']= PHP_EXTENSIONS_DEFAULT;
119 * $f_ext['ftpspace']['allow']='*';
120 * $f_ext['ftpspace']['deny']='';
121 * 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.
122 * 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.
123 * You list extensions comma-separated. If the value is a '*' every extension is allowed
124 * The list is case-insensitive when used in this class (see init())
125 * Typically TYPO3_CONF_VARS['BE']['fileExtensions'] would be passed along as $f_ext.
126 *
127 * Example:
128 * $basicff->init($GLOBALS['FILEMOUNTS'],$GLOBALS['TYPO3_CONF_VARS']['BE']['fileExtensions']);
129 *
130 * @param array Contains the paths of the file mounts for the current BE user. Normally $GLOBALS['FILEMOUNTS'] is passed. This variable is set during backend user initialization; $FILEMOUNTS = $GLOBALS['BE_USER']->returnFilemounts(); (see typo3/init.php)
131 * @param array Array with information about allowed and denied file extensions. Typically passed: $GLOBALS['TYPO3_CONF_VARS']['BE']['fileExtensions']
132 * @return void
133 * @see typo3/init.php, \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::returnFilemounts()
134 * @todo Define visibility
135 * @deprecated All methods in this class should not be used anymore since TYPO3 6.0. Please use corresponding TYPO3\\CMS\\Core\\Resource\\ResourceStorage (fetched via BE_USERS->getFileStorages()), as all functions should be found there (in a cleaner manner).
136 */
137 public function init($mounts, $f_ext) {
138 GeneralUtility::logDeprecatedFunction('All methods in this class should not be used anymore since TYPO3 6.0. Please use corresponding TYPO3\\CMS\\Core\\Resource\\ResourceStorage (fetched via BE_USERS->getFileStorages()), as all functions should be found there (in a cleaner manner).');
139 $this->f_ext['webspace']['allow'] = GeneralUtility::uniqueList(strtolower($f_ext['webspace']['allow']));
140 $this->f_ext['webspace']['deny'] = GeneralUtility::uniqueList(strtolower($f_ext['webspace']['deny']));
141 $this->f_ext['ftpspace']['allow'] = GeneralUtility::uniqueList(strtolower($f_ext['ftpspace']['allow']));
142 $this->f_ext['ftpspace']['deny'] = GeneralUtility::uniqueList(strtolower($f_ext['ftpspace']['deny']));
143
144 $this->mounts = $mounts;
145 $this->webPath = GeneralUtility::getIndpEnv('TYPO3_DOCUMENT_ROOT');
146 $this->isInit = 1;
147 $this->maxInputNameLen = $GLOBALS['TYPO3_CONF_VARS']['SYS']['maxFileNameLength'] ?: $this->maxInputNameLen;
148 }
149
150 /**
151 * Returns an array with a whole lot of fileinformation.
152 * Information includes:
153 * - path : path part of give file
154 * - file : filename
155 * - filebody : filename without extension
156 * - fileext : lowercase extension
157 * - realFileext : extension
158 * - tstamp : timestamp of modification
159 * - size : file size
160 * - type : file type (block/char/dir/fifo/file/link)
161 * - owner : user ID of owner of file
162 * - perms : numerical representation of file permissions
163 * - writable : is file writeable by web user (FALSE = yes; TRUE = no) *)
164 * - readable : is file readable by web user (FALSE = yes; TRUE = no) *)
165 *
166 * ) logic is reversed because of handling by functions in TYPO3\CMS\Filelist\FileList
167 *
168 * @param string Filepath to existing file. Should probably be absolute. Filefunctions are performed on this value.
169 * @return array Information about the file in the filepath
170 * @todo Define visibility
171 */
172 public function getTotalFileInfo($wholePath) {
173 // @todo: deprecate this function, and replace its use in the storage/mounts
174 $theuser = getmyuid();
175 $info = GeneralUtility::split_fileref($wholePath);
176 $info['tstamp'] = @filemtime($wholePath);
177 $info['size'] = @filesize($wholePath);
178 $info['type'] = @filetype($wholePath);
179 $info['owner'] = @fileowner($wholePath);
180 $info['perms'] = @fileperms($wholePath);
181 $info['writable'] = !@is_writable($wholePath);
182 $info['readable'] = !@is_readable($wholePath);
183 return $info;
184 }
185
186 /**
187 * Checks if a $iconkey (fileextension) is allowed according to $this->f_ext.
188 *
189 * @param string The extension to check, eg. "php" or "html" etc.
190 * @param string Either "webspage" or "ftpspace" - points to a key in $this->f_ext
191 * @return boolean TRUE if file extension is allowed.
192 * @todo Define visibility
193 */
194 public function is_allowed($iconkey, $type) {
195 if (isset($this->f_ext[$type])) {
196 $ik = strtolower($iconkey);
197 if ($ik) {
198 // If the extension is found amongst the allowed types, we return TRUE immediately
199 if ($this->f_ext[$type]['allow'] == '*' || GeneralUtility::inList($this->f_ext[$type]['allow'], $ik)) {
200 return TRUE;
201 }
202 // If the extension is found amongst the denied types, we return FALSE immediately
203 if ($this->f_ext[$type]['deny'] == '*' || GeneralUtility::inList($this->f_ext[$type]['deny'], $ik)) {
204 return FALSE;
205 }
206 // If no match we return TRUE
207 return TRUE;
208 } else {
209 // If no extension:
210 if ($this->f_ext[$type]['allow'] == '*') {
211 return TRUE;
212 }
213 if ($this->f_ext[$type]['deny'] == '*') {
214 return FALSE;
215 }
216 return TRUE;
217 }
218 }
219 return FALSE;
220 }
221
222 /**
223 * Returns TRUE if you can operate of ANY file ('*') in the space $theDest is in ('webspace' / 'ftpspace')
224 *
225 * @param string Absolute path
226 * @return boolean
227 * @todo Define visibility
228 */
229 public function checkIfFullAccess($theDest) {
230 $type = $this->is_webpath($theDest) ? 'webspace' : 'ftpspace';
231 if (isset($this->f_ext[$type])) {
232 if ((string) $this->f_ext[$type]['deny'] == '' || $this->f_ext[$type]['allow'] == '*') {
233 return TRUE;
234 }
235 }
236 }
237
238 /**
239 * Checks if $this->webPath (should be TYPO3_DOCUMENT_ROOT) is in the first part of $path
240 * Returns TRUE also if $this->init is not set or if $path is empty...
241 *
242 * @param string Absolute path to check
243 * @return boolean
244 * @todo Define visibility
245 */
246 public function is_webpath($path) {
247 if ($this->isInit) {
248 $testPath = $this->slashPath($path);
249 $testPathWeb = $this->slashPath($this->webPath);
250 if ($testPathWeb && $testPath) {
251 return GeneralUtility::isFirstPartOfStr($testPath, $testPathWeb);
252 }
253 }
254 return TRUE;
255 }
256
257 /**
258 * If the filename is given, check it against the TYPO3_CONF_VARS[BE][fileDenyPattern] +
259 * Checks if the $ext fileextension is allowed in the path $theDest (this is based on whether $theDest is below the $this->webPath)
260 *
261 * @param string File extension, eg. "php" or "html
262 * @param string Absolute path for which to test
263 * @param string Filename to check against TYPO3_CONF_VARS[BE][fileDenyPattern]
264 * @return boolean TRUE if extension/filename is allowed
265 * @todo Define visibility
266 */
267 public function checkIfAllowed($ext, $theDest, $filename = '') {
268 return GeneralUtility::verifyFilenameAgainstDenyPattern($filename) && $this->is_allowed($ext, ($this->is_webpath($theDest) ? 'webspace' : 'ftpspace'));
269 }
270
271 /**
272 * Returns TRUE if the input filename string is shorter than $this->maxInputNameLen.
273 *
274 * @param string Filename, eg "somefile.html
275 * @return boolean
276 * @todo Define visibility
277 */
278 public function checkFileNameLen($fileName) {
279 // @todo: should go into the LocalDriver in a protected way (not important to the outside world)
280 return strlen($fileName) <= $this->maxInputNameLen;
281 }
282
283 /**
284 * Cleans $theDir for slashes in the end of the string and returns the new path, if it exists on the server.
285 *
286 * @param string Directory path to check
287 * @return string Returns the cleaned up directory name if OK, otherwise FALSE.
288 * @todo Define visibility
289 */
290 public function is_directory($theDir) {
291 // @todo: should go into the LocalDriver in a protected way (not important to the outside world)
292 if ($this->isPathValid($theDir)) {
293 $theDir = PathUtility::getCanonicalPath($theDir);
294 if (@is_dir($theDir)) {
295 return $theDir;
296 }
297 }
298 return FALSE;
299 }
300
301 /**
302 * Wrapper for \TYPO3\CMS\Core\Utility\GeneralUtility::validPathStr()
303 *
304 * @param string Filepath to evaluate
305 * @return boolean TRUE, if no '//', '..' or '\' is in the $theFile
306 * @see \TYPO3\CMS\Core\Utility\GeneralUtility::validPathStr()
307 * @todo Define visibility
308 */
309 public function isPathValid($theFile) {
310 // @todo: should go into the LocalDriver in a protected way (not important to the outside world)
311 return GeneralUtility::validPathStr($theFile);
312 }
313
314 /**
315 * Returns the destination path/filename of a unique filename/foldername in that path.
316 * If $theFile exists in $theDest (directory) the file have numbers appended up to $this->maxNumber. Hereafter a unique string will be appended.
317 * This function is used by fx. TCEmain when files are attached to records and needs to be uniquely named in the uploads/* folders
318 *
319 * @param string The input filename to check
320 * @param string The directory for which to return a unique filename for $theFile. $theDest MUST be a valid directory. Should be absolute.
321 * @param boolean If set the filename is returned with the path prepended without checking whether it already existed!
322 * @return string The destination absolute filepath (not just the name!) of a unique filename/foldername in that path.
323 * @see \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue()
324 * @todo Define visibility
325 */
326 public function getUniqueName($theFile, $theDest, $dontCheckForUnique = 0) {
327 // @todo: should go into the LocalDriver in a protected way (not important to the outside world)
328 $theDest = $this->is_directory($theDest);
329 // $theDest is cleaned up
330 $origFileInfo = GeneralUtility::split_fileref($theFile);
331 // Fetches info about path, name, extension of $theFile
332 if ($theDest) {
333 if ($this->getUniqueNamePrefix) {
334 // Adds prefix
335 $origFileInfo['file'] = $this->getUniqueNamePrefix . $origFileInfo['file'];
336 $origFileInfo['filebody'] = $this->getUniqueNamePrefix . $origFileInfo['filebody'];
337 }
338 // Check if the file exists and if not - return the filename...
339 $fileInfo = $origFileInfo;
340 $theDestFile = $theDest . '/' . $fileInfo['file'];
341 // The destinations file
342 if (!file_exists($theDestFile) || $dontCheckForUnique) {
343 // If the file does NOT exist we return this filename
344 return $theDestFile;
345 }
346 // 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...
347 $theTempFileBody = preg_replace('/_[0-9][0-9]$/', '', $origFileInfo['filebody']);
348 // This removes _xx if appended to the file
349 $theOrigExt = $origFileInfo['realFileext'] ? '.' . $origFileInfo['realFileext'] : '';
350 for ($a = 1; $a <= $this->maxNumber + 1; $a++) {
351 if ($a <= $this->maxNumber) {
352 // First we try to append numbers
353 $insert = '_' . sprintf('%02d', $a);
354 } else {
355 // .. then we try unique-strings...
356 $insert = '_' . substr(md5(uniqId('')), 0, $this->uniquePrecision);
357 }
358 $theTestFile = $theTempFileBody . $insert . $theOrigExt;
359 $theDestFile = $theDest . '/' . $theTestFile;
360 // The destinations file
361 if (!file_exists($theDestFile)) {
362 // If the file does NOT exist we return this filename
363 return $theDestFile;
364 }
365 }
366 }
367 }
368
369 /**
370 * Checks if $thePath is a path under one of the paths in $this->mounts
371 * See comment in the header of this class.
372 *
373 * @param string $thePath MUST HAVE a trailing '/' in order to match correctly with the mounts
374 * @return string The key to the first mount found, otherwise nothing is returned.
375 * @see init()
376 * @todo Define visibility
377 */
378 public function checkPathAgainstMounts($thePath) {
379 // @todo: deprecate this function, now done in the Storage object
380 if ($thePath && $this->isPathValid($thePath) && is_array($this->mounts)) {
381 foreach ($this->mounts as $k => $val) {
382 if (GeneralUtility::isFirstPartOfStr($thePath, $val['path'])) {
383 return $k;
384 }
385 }
386 }
387 }
388
389 /**
390 * Find first web folder (relative to PATH_site.'fileadmin') in filemounts array
391 *
392 * @return string The key to the first mount inside PATH_site."fileadmin" found, otherwise nothing is returned.
393 * @todo Define visibility
394 */
395 public function findFirstWebFolder() {
396 // @todo: where and when to use this function?
397 if (is_array($this->mounts)) {
398 foreach ($this->mounts as $k => $val) {
399 if (GeneralUtility::isFirstPartOfStr($val['path'], PATH_site . $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'])) {
400 return $k;
401 }
402 }
403 }
404 }
405
406 /**
407 * Removes filemount part of a path, thus blinding the position.
408 * Takes a path, $thePath, and removes the part of the path which equals the filemount.
409 *
410 * @param string $thePath is a path which MUST be found within one of the internally set filemounts, $this->mounts
411 * @return string The processed input path
412 * @todo Define visibility
413 */
414 public function blindPath($thePath) {
415 // @todo: where and when to use this function?
416 $k = $this->checkPathAgainstMounts($thePath);
417 if ($k) {
418 $name = '';
419 $name .= '[' . $this->mounts[$k]['name'] . ']: ';
420 $name .= substr($thePath, strlen($this->mounts[$k]['path']));
421 return $name;
422 }
423 }
424
425 /**
426 * Find temporary folder
427 * Finds the first $this->tempFN ('_temp_' usually) -folder in the internal array of filemounts, $this->mounts
428 *
429 * @return string Returns the path if found, otherwise nothing if error.
430 * @todo Define visibility
431 */
432 public function findTempFolder() {
433 // @todo: where and when to use this function?
434 if ($this->tempFN && is_array($this->mounts)) {
435 foreach ($this->mounts as $k => $val) {
436 $tDir = $val['path'] . $this->tempFN;
437 if (@is_dir($tDir)) {
438 return $tDir;
439 }
440 }
441 }
442 }
443
444 /*********************
445 *
446 * Cleaning functions
447 *
448 *********************/
449 /**
450 * Removes all dots, slashes and spaces after a path
451 *
452 * @param string $theDir Input string
453 * @return string Output string
454 * @deprecated since 6.1, will be removed in two versions, use \TYPO3\CMS\Core\Utility\PathUtility::getCanonicalPath() instead
455 */
456 public function cleanDirectoryName($theDir) {
457 GeneralUtility::logDeprecatedFunction();
458 return PathUtility::getCanonicalPath($theDir);
459 }
460
461 /**
462 * Converts any double slashes (//) to a single slash (/)
463 *
464 * @param string Input value
465 * @return string Returns the converted string
466 * @todo Define visibility
467 */
468 public function rmDoubleSlash($string) {
469 // @todo: should go into the LocalDriver in a protected way (not important to the outside world)
470 return str_replace('//', '/', $string);
471 }
472
473 /**
474 * Returns a string which has a slash '/' appended if it doesn't already have that slash
475 *
476 * @param string Input string
477 * @return string Output string with a slash in the end (if not already there)
478 * @todo Define visibility
479 */
480 public function slashPath($path) {
481 // @todo: should go into the LocalDriver in a protected way (not important to the outside world)
482 // @todo: should be done with rtrim($path, '/') . '/';
483 if (substr($path, -1) != '/') {
484 return $path . '/';
485 }
486 return $path;
487 }
488
489 /**
490 * Returns a string where any character not matching [.a-zA-Z0-9_-] is substituted by '_'
491 * Trailing dots are removed
492 *
493 * @param string Input string, typically the body of a filename
494 * @param string Charset of the a filename (defaults to current charset; depending on context)
495 * @return string Output string with any characters not matching [.a-zA-Z0-9_-] is substituted by '_' and trailing dots removed
496 * @todo Define visibility
497 */
498 public function cleanFileName($fileName, $charset = '') {
499 // Handle UTF-8 characters
500 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']) {
501 // allow ".", "-", 0-9, a-z, A-Z and everything beyond U+C0 (latin capital letter a with grave)
502 $cleanFileName = preg_replace('/[\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF]/u', '_', trim($fileName));
503 } else {
504 // Get conversion object or initialize if needed
505 if (!is_object($this->csConvObj)) {
506 if (TYPO3_MODE == 'FE') {
507 $this->csConvObj = $GLOBALS['TSFE']->csConvObj;
508 } elseif (is_object($GLOBALS['LANG'])) {
509 // BE assumed:
510 $this->csConvObj = $GLOBALS['LANG']->csConvObj;
511 } else {
512 // The object may not exist yet, so we need to create it now. Happens in the Install Tool for example.
513 $this->csConvObj = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Charset\\CharsetConverter');
514 }
515 }
516 // Define character set
517 if (!$charset) {
518 if (TYPO3_MODE == 'FE') {
519 $charset = $GLOBALS['TSFE']->renderCharset;
520 } else {
521 // Backend
522 $charset = 'utf-8';
523 }
524 }
525 // If a charset was found, convert filename
526 if ($charset) {
527 $fileName = $this->csConvObj->specCharsToASCII($charset, $fileName);
528 }
529 // Replace unwanted characters by underscores
530 $cleanFileName = preg_replace('/[^.[:alnum:]_-]/', '_', trim($fileName));
531 }
532 // Strip trailing dots and return
533 return preg_replace('/\\.*$/', '', $cleanFileName);
534 }
535
536 }