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