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