PageRenderTime 90ms CodeModel.GetById 42ms RepoModel.GetById 8ms app.codeStats 0ms

/e107_handlers/upload_handler.php

https://github.com/CasperGemini/e107
PHP | 868 lines | 543 code | 93 blank | 232 comment | 110 complexity | 49f96c2ab19c0aecd9141010327c5bbb MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. /*
  3. * e107 website system
  4. *
  5. * Copyright (C) 2008-2013 e107 Inc (e107.org)
  6. * Released under the terms and conditions of the
  7. * GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
  8. *
  9. * File Upload Handler
  10. *
  11. * $Source: /cvs_backup/e107_0.8/e107_handlers/upload_handler.php,v $
  12. * $Revision$
  13. * $Date$
  14. * $Author$
  15. */
  16. /**
  17. * File upload handler
  18. *
  19. * @package e107
  20. * @subpackage e107_handlers
  21. * @version $Id$;
  22. *
  23. * @todo - option to restrict by total size irrespective of number of uploads
  24. */
  25. if (!defined('e107_INIT'))
  26. {
  27. exit;
  28. }
  29. include_lan(e_LANGUAGEDIR.e_LANGUAGE.'/lan_upload_handler.php');
  30. //define("UH_DEBUG",TRUE);
  31. define("UH_DEBUG", FALSE);
  32. //FIXME need another name
  33. // define('e_UPLOAD_TEMP_DIR', e_MEDIA.'temp/');
  34. define('e_UPLOAD_TEMP_DIR', e_TEMP); // Temporary directory - used if PHP's OPEN_BASEDIR active
  35. define('e_READ_FILETYPES', 'filetypes.xml'); // Upload permissions
  36. define('e_SAVE_FILETYPES', 'filetypes_.xml');
  37. /**
  38. * File upload handler - this is the preferred interface for new code
  39. *
  40. * Routine processes the array of uploaded files according to both specific options set by the caller, and
  41. * system options configured by the main admin.
  42. *
  43. * function process_uploaded_files($uploaddir, $fileinfo = FALSE, $options = array())
  44. * Parameters:
  45. * @param string $uploaddir - target directory (checked that it exists, but path not otherwise changed)
  46. *
  47. * @param string $fileinfo - determines any special handling of file name (combines previous $fileinfo and $avatar parameters):
  48. * FALSE - default option; no processing
  49. * "attachment+extra_text" - indicates an attachment (related to forum post or PM), and specifies some optional text which is
  50. * incorporated into the final file name (the original $fileinfo parameter).
  51. * "prefix+extra_text" - indicates an attachment or file, and specifies some optional text which is prefixed to the file name
  52. * "unique"
  53. * - if the proposed destination file doesn't exist, saved under given name
  54. * - if the proposed destination file does exist, prepends time() to the file name to make it unique
  55. * 'avatar'
  56. * - indicates an avatar is being uploaded (not used - options must be set elsewhere)
  57. *
  58. * @param array $options - an array of supplementary options, all of which will be given appropriate defaults if not defined:
  59. * 'filetypes' - name of file containing list of valid file types
  60. * - Always looks in the admin directory
  61. * - defaults to e_ADMIN.filetypes.xml, else e_ADMIN.admin_filetypes.php for admins (if file exists), otherwise e_ADMIN.filetypes.php for users.
  62. * - FALSE disables this option (which implies that 'extra_file_types' is used)
  63. * 'file_mask' - comma-separated list of file types which if defined limits the allowed file types to those which are in both this list and the
  64. * file specified by the 'filetypes' option. Enables restriction to, for example, image files.
  65. * 'extra_file_types' - if is FALSE or undefined, rejects totally unknown file extensions (even if in $options['filetypes'] file).
  66. * if TRUE, accepts totally unknown file extensions which are in $options['filetypes'] file.
  67. * otherwise specifies a comma-separated list of additional permitted file extensions
  68. * 'final_chmod' - chmod() to be applied to uploaded files (0644 default) (This routine expects an integer value, so watch formatting/decoding - its normally
  69. * specified in octal. Typically use intval($permissions,8) to convert)
  70. * 'max_upload_size' - maximum size of uploaded files in bytes, or as a string with a 'multiplier' letter (e.g. 16M) at the end.
  71. * - otherwise uses $pref['upload_maxfilesize'] if set
  72. * - overriding limit of the smaller of 'post_max_size' and 'upload_max_size' if set in php.ini
  73. * (Note: other parts of E107 don't understand strings with a multiplier letter yet)
  74. * 'file_array_name' - the name of the 'input' array - defaults to file_userfile[] - otherwise as set.
  75. * 'max_file_count' - maximum number of files which can be uploaded - default is 'unlimited' if this is zero or not set.
  76. * 'overwrite' - if TRUE, existing file of the same name is overwritten; otherwise returns 'duplicate file' error (default FALSE)
  77. * 'save_to_db' - [obsolete] storage type - if set and TRUE, uploaded files were saved in the database (rather than as flat files)
  78. *
  79. * @return boolean|array
  80. * Returns FALSE if the upload directory doesn't exist, or various other errors occurred which restrict the amount of meaningful information.
  81. * Returns an array, with one set of entries per uploaded file, regardless of whether saved or
  82. * discarded (not all fields always present) - $c is array index:
  83. * $uploaded[$c]['name'] - file name - as saved to disc
  84. * $uploaded[$c]['rawname'] - original file name, prior to any addition of identifiers etc (useful for display purposes)
  85. * $uploaded[$c]['type'] - mime type (if set - as sent by browser)
  86. * $uploaded[$c]['size'] - size in bytes (should be zero if error)
  87. * $uploaded[$c]['error'] - numeric error code (zero = OK)
  88. * $uploaded[$c]['index'] - if upload successful, the index position from the file_userfile[] array - usually numeric, but may be alphanumeric if coded
  89. * $uploaded[$c]['message'] - text of displayed message relating to file
  90. * $uploaded[$c]['line'] - only if an error occurred, has line number (from __LINE__)
  91. * $uploaded[$c]['file'] - only if an error occurred, has file name (from __FILE__)
  92. *
  93. * On exit, uploaded files should all have been removed from the temporary directory.
  94. * No messages displayed - its caller's responsibility to handle errors and display info to
  95. * user (or can use handle_upload_messages() from this module)
  96. *
  97. * Details of uploaded files are in $_FILES['file_userfile'] (or other array name as set) on entry.
  98. * Elements passed (from PHP) relating to each file:
  99. * ['name'] - the original name
  100. * ['type'] - mime type (if provided - not checked by PHP)
  101. * ['size'] - file size in bytes
  102. * ['tmp_name'] - temporary file name on server
  103. * ['error'] - error code. 0 = 'good'. 1..4 main others, although up to 8 defined for later PHP versions
  104. * Files stored in server's temporary directory, unless another set
  105. */
  106. function process_uploaded_files($uploaddir, $fileinfo = FALSE, $options = NULL)
  107. {
  108. $admin_log = e107::getAdminLog();
  109. $ul_temp_dir = '';
  110. if (ini_get('open_basedir') != '')
  111. { // Need to move file to intermediate directory before we can read its contents to check it.
  112. $ul_temp_dir = e_UPLOAD_TEMP_DIR;
  113. }
  114. if (UH_DEBUG)
  115. $admin_log->
  116. e_log_event(10, debug_backtrace(), "DEBUG", "Upload Handler test", "Process uploads to {$uploaddir}, fileinfo ".$fileinfo, FALSE, LOG_TO_ROLLING);
  117. // $admin_log->e_log_event(10,__FILE__."|".__FUNCTION__."@".__LINE__,"DEBUG","Upload Handler test","Intermediate directory: {$ul_temp_dir} ",FALSE,LOG_TO_ROLLING);
  118. $overwrite = varset($options['overwrite'], FALSE);
  119. $uploaddir = realpath($uploaddir); // Mostly to get rid of the grot that might be passed in from legacy code. Also strips any trailing '/'
  120. if (!is_dir($uploaddir))
  121. {
  122. if (UH_DEBUG)
  123. $admin_log->
  124. e_log_event(10, __FILE__."|".__FUNCTION__."@".__LINE__, "DEBUG", "Upload Handler test", "Invalid directory: ".$uploaddir, FALSE, FALSE);
  125. return FALSE; // Need a valid directory
  126. }
  127. if (UH_DEBUG)
  128. $admin_log->
  129. e_log_event(10, __FILE__."|".__FUNCTION__."@".__LINE__, "DEBUG", "Upload Handler test", "Destination directory: ".$uploaddir, FALSE, FALSE);
  130. $final_chmod = varset($options['final_chmod'], 0644);
  131. if (isset($options['file_array_name']))
  132. {
  133. $files = $_FILES[$options['file_array_name']];
  134. }
  135. else
  136. {
  137. $files = $_FILES['file_userfile'];
  138. }
  139. $max_file_count = varset($options['max_file_count'], 0);
  140. if (!is_array($files))
  141. {
  142. if (UH_DEBUG)
  143. $admin_log->
  144. e_log_event(10, __FILE__."|".__FUNCTION__."@".__LINE__, "DEBUG", "Upload Handler test", "No files uploaded", FALSE, FALSE);
  145. return FALSE;
  146. }
  147. $uploaded = array(
  148. );
  149. $max_upload_size = calc_max_upload_size(varset($options['max_upload_size'], -1)); // Find overriding maximum upload size
  150. $allowed_filetypes = get_filetypes(varset($options['file_mask'], ''), varset($options['filetypes'], ''));
  151. $max_upload_size = set_max_size($allowed_filetypes, $max_upload_size);
  152. // That's the basics set up - we can start processing files now
  153. if (UH_DEBUG)
  154. $admin_log->
  155. e_log_event(10, __FILE__."|".__FUNCTION__."@".__LINE__, "DEBUG", "Upload Handler test", "Start individual files: ".count($files['name'])." Max upload: ".$max_upload_size, FALSE, FALSE);
  156. $c = 0;
  157. $tp = e107::getParser();
  158. foreach ($files['name'] as $key=>$name)
  159. {
  160. $first_error = FALSE; // Clear error flag
  161. if (($name != '') || $files['size'][$key]) // Need this check for things like file manager which allow multiple possible uploads
  162. {
  163. $origname = $name;
  164. //$name = preg_replace("/[^a-z0-9._-]/", '', str_replace(' ', '_', str_replace('%20', '_', strtolower($name))));
  165. // FIX handle non-latin file names
  166. $name = preg_replace("/[^\w\pL.-]/u", '', str_replace(' ', '_', str_replace('%20', '_', $tp->ustrtolower($name))));
  167. $raw_name = $name; // Save 'proper' file name - useful for display
  168. $file_ext = trim(strtolower(substr(strrchr($name, "."), 1))); // File extension - forced to lower case internally
  169. if (!trim($files['type'][$key]))
  170. $files['type'][$key] = 'Unknowm mime-type';
  171. if (UH_DEBUG)
  172. $admin_log->
  173. e_log_event(10, __FILE__."|".__FUNCTION__."@".__LINE__, "DEBUG", "Upload Handler test", "Process file {$name}, size ".$files['size'][$key], FALSE, FALSE);
  174. if ($max_file_count && ($c >= $max_file_count))
  175. {
  176. $first_error = 249; // 'Too many files uploaded' error
  177. }
  178. else
  179. {
  180. $first_error = $files['error'][$key]; // Start with whatever error PHP gives us for the file
  181. }
  182. if (!$first_error)
  183. { // Check file size early on
  184. if ($files['size'][$key] == 0)
  185. {
  186. $first_error = 4; // Standard error code for zero size file
  187. }
  188. elseif ($files['size'][$key] > $max_upload_size)
  189. {
  190. $first_error = 254;
  191. }
  192. elseif (isset($allowed_filetypes[$file_ext]) && ($allowed_filetypes[$file_ext] > 0) && ($files['size'][$key] > $allowed_filetypes[$file_ext]))
  193. { // XML file set limits per extension
  194. $first_error = 254;
  195. }
  196. }
  197. if (!$first_error)
  198. {
  199. $uploadfile = $files['tmp_name'][$key]; // Name in temporary directory
  200. if (!$uploadfile)
  201. $first_error = 253;
  202. }
  203. if (!$first_error)
  204. {
  205. // Need to support multiple files with the same 'real' name in some cases
  206. if (strpos($fileinfo, "attachment") === 0)
  207. { // For attachments, add in a prefix plus time and date to give a unique file name
  208. $addbit = explode('+', $fileinfo, 2);
  209. $name = time()."_".USERID."_".trim($addbit[1]).$name;
  210. }
  211. elseif (strpos($fileinfo, "prefix") === 0)
  212. { // For attachments, avatars, photos etc alternatively just add a prefix we've been passed
  213. $addbit = explode('+', $fileinfo, 2);
  214. $name = trim($addbit[1]).$name;
  215. }
  216. $destination_file = $uploaddir."/".$name;
  217. if ($fileinfo == "unique" && file_exists($destination_file))
  218. { // Modify destination name to make it unique - but only if target file name exists
  219. $name = time()."_".$name;
  220. $destination_file = $uploaddir."/".$name;
  221. }
  222. if (file_exists($destination_file) && !$overwrite)
  223. $first_error = 250; // Invent our own error number - duplicate file
  224. }
  225. if (!$first_error)
  226. {
  227. $tpos = FALSE;
  228. if ($file_ext != '') // Require any uploaded file to have an extension
  229. {
  230. if ($ul_temp_dir)
  231. { // Need to move file to our own temporary directory
  232. $tempfilename = $uploadfile;
  233. $uploadfile = $ul_temp_dir.basename($uploadfile);
  234. if (UH_DEBUG)
  235. $admin_log->
  236. e_log_event(10, __FILE__."|".__FUNCTION__."@".__LINE__, "DEBUG", "Upload Handler test", "Move {$tempfilename} to {$uploadfile} ", FALSE, LOG_TO_ROLLING);
  237. @move_uploaded_file($tempfilename, $uploadfile); // This should work on all hosts
  238. }
  239. $tpos = (($file_status = vet_file($uploadfile, $name, $allowed_filetypes, varset($options['extra_file_types'], FALSE))) === TRUE);
  240. }
  241. if ($tpos === FALSE)
  242. {
  243. // File type upload not permitted - error message and abort
  244. $first_error = 251; // Invent our own error number - file type not permitted
  245. }
  246. }
  247. if (!$first_error)
  248. { // All tests passed - can store it somewhere
  249. $uploaded[$c]['name'] = $name;
  250. $uploaded[$c]['rawname'] = $raw_name;
  251. $uploaded[$c]['origname'] = $origname;
  252. $uploaded[$c]['type'] = $files['type'][$key];
  253. $uploaded[$c]['size'] = 0;
  254. $uploaded[$c]['index'] = $key; // Store the actual index from the file_userfile array
  255. // Store as flat file
  256. if ((!$ul_temp_dir && @move_uploaded_file($uploadfile, $destination_file)) || ($ul_temp_dir && @rename($uploadfile, $destination_file))) // This should work on all hosts
  257. {
  258. @chmod($destination_file, $final_chmod);
  259. if (UH_DEBUG)
  260. $admin_log->
  261. e_log_event(10, __FILE__."|".__FUNCTION__."@".__LINE__, "DEBUG", "Upload Handler test", "Final chmod() file {$destination_file} to {$final_chmod} ", FALSE, FALSE);
  262. $uploaded[$c]['size'] = $files['size'][$key];
  263. if (UH_DEBUG)
  264. $admin_log->
  265. e_log_event(10, __FILE__."|".__FUNCTION__."@".__LINE__, "DEBUG", "Upload Handler test", "Saved file {$c} OK: ".$uploaded[$c]['name'], FALSE, FALSE);
  266. }
  267. else
  268. {
  269. $first_error = 252; // Error - "couldn't save destination"
  270. }
  271. }
  272. if (!$first_error)
  273. { // This file succeeded
  274. $uploaded[$c]['message'] = LANUPLOAD_3." '".$raw_name."'";
  275. $uploaded[$c]['error'] = 0;
  276. }
  277. else
  278. {
  279. $uploaded[$c]['error'] = $first_error;
  280. $uploaded[$c]['size'] = 0;
  281. switch ($first_error)
  282. {
  283. case 1: // Exceeds upload_max_filesize in php.ini
  284. $error = LANUPLOAD_5;
  285. break;
  286. case 2: // Exceeds MAX_FILE_SIZE in form
  287. $error = LANUPLOAD_6;
  288. break;
  289. case 3: // Partial upload
  290. $error = LANUPLOAD_7;
  291. break;
  292. case 4: // No file uploaded
  293. $error = LANUPLOAD_8;
  294. break;
  295. case 5: // Undocumented code (zero file size)
  296. $error = LANUPLOAD_9;
  297. break;
  298. case 6: // Missing temporary folder
  299. $error = LANUPLOAD_13;
  300. break;
  301. case 7: // File write failed
  302. $error = LANUPLOAD_14;
  303. break;
  304. case 8: // Upload stopped by extension
  305. $error = LANUPLOAD_15;
  306. break;
  307. case 249: // Too many files (our error code)
  308. $error = LANUPLOAD_19;
  309. break;
  310. case 250: // Duplicate File (our error code)
  311. $error = LANUPLOAD_10;
  312. break;
  313. case 251: // File type not allowed (our error code)
  314. $error = LANUPLOAD_1." ".$files['type'][$key]." ".LANUPLOAD_2." ({$file_status})";
  315. break;
  316. case 252: // File uploaded OK, but couldn't save it
  317. $error = LANUPLOAD_4." [".str_replace("../", "", $uploaddir)."]";
  318. break;
  319. case 253: // Bad name for uploaded file (our error code)
  320. $error = LANUPLOAD_17;
  321. break;
  322. case 254: // file size exceeds allowable limits (our error code)
  323. $error = LANUPLOAD_18;
  324. break;
  325. default: // Shouldn't happen - but at least try and make it obvious if it does!
  326. $error = LANUPLOAD_16;
  327. }
  328. $uploaded[$c]['message'] = LANUPLOAD_11." '".$name."' <br />".LANUPLOAD_12.": ".$error;
  329. $uploaded[$c]['line'] = __LINE__;
  330. $uploaded[$c]['file'] = __FILE__;
  331. if (UH_DEBUG)
  332. $admin_log->
  333. e_log_event(10, __FILE__."|".__FUNCTION__."@".__LINE__, "DEBUG", "Upload Handler test", "Main routine error {$first_error} file {$c}: ".$uploaded[$c]['message'], FALSE, FALSE);
  334. // If we need to abort on first error, do so here - could check for specific error codes
  335. }
  336. if (is_file($uploadfile))
  337. @unlink($uploadfile); // Don't leave the file on the server if error (although should be auto-deleted)
  338. $c++;
  339. }
  340. }
  341. return $uploaded;
  342. }
  343. /**
  344. * Utility routine to handle the messages returned by process_uploaded_files().
  345. * @param array $upload_array is the list of uploaded files (as returned by process_uploaded_files())
  346. * @param boolean $errors_only - if TRUE, no message is shown for a successful upload.
  347. * @param boolean $use_handler - if TRUE, message_handler is used to display the message.
  348. * @return string - a list of all accumulated messages. (Non-destructive call, so can be called several times with different options).
  349. */
  350. function handle_upload_messages(&$upload_array, $errors_only = TRUE, $use_handler = FALSE)
  351. {
  352. // Display error messages, accumulate FMESSAGE
  353. // Write as a separate routine - returns all messages displayed. Option to only display failures.
  354. $f_message = '';
  355. foreach ($upload_array as $k=>$r)
  356. {
  357. if (!$errors_only || $r['error'])
  358. {
  359. if ($use_handler)
  360. {
  361. require_once (e_HANDLER."message_handler.php");
  362. message_handler("MESSAGE", $r['message'], $r['line'], $r['file']);
  363. }
  364. $f_message[] = $r['message'];
  365. }
  366. }
  367. return implode("<br />", $f_message);
  368. }
  369. /*
  370. //====================================================================
  371. // LEGACY FILE UPLOAD HANDLER
  372. //====================================================================
  373. /**
  374. * This is the 'legacy' interface, which handles various special cases etc.
  375. * It was the only option in E107 0.7.8 and earlier, and is still used in some places in core.
  376. * It also attempts to return in the same way as the original, especially when any errors occur
  377. *
  378. * @param string $uploaddir - target directory for file. Defaults to e_FILE/public
  379. * @param boolean|string $avatar - sets the 'type' or destination of the file:
  380. * FALSE - its a 'general' file
  381. * 'attachment' - indicates an attachment (related to forum post or PM)
  382. * 'unique' - indicates that file name must be unique - new name given (prefixed with time()_ )
  383. * 'avatar' - indicates an avatar is being uploaded
  384. * @param string $fileinfo - included within the name of the saved file with attachments - can be an identifier of some sort
  385. * (Forum adds 'FT{$tid}_' - where $tid is the thread ID.
  386. * @param boolean $overwrite - if true, an uploaded file can overwrite an existing file of the same name (not used in 0.7 core)
  387. *
  388. * Preference used:
  389. * $pref['upload_storagetype'] - now ignored - used to be 1 for files, 2 for database storage
  390. *
  391. * @return boolean|array - For backward compatibility, returns FALSE if only one file uploaded and an error;
  392. * otherwise returns an array with per-file error codes as appropriate.
  393. * On exit, F_MESSAGE is defined with the success/failure message(s) that have been displayed - one file per line
  394. */
  395. function file_upload($uploaddir, $avatar = FALSE, $fileinfo = "", $overwrite = "")
  396. {
  397. $admin_log = e107::getAdminLog();
  398. $options = array(
  399. 'extra_file_types'=>TRUE
  400. ); // As default, allow any filetype enabled in filetypes.php
  401. if (!$uploaddir)
  402. {
  403. $uploaddir = e_UPLOAD;
  404. }
  405. if (strpos($avatar, '=') !== FALSE)
  406. {
  407. list($avatar, $param) = explode('=', $avatar, 2);
  408. }
  409. else
  410. {
  411. $param = USERID;
  412. }
  413. switch ($avatar)
  414. {
  415. case 'attachment':
  416. $avatar = "attachment+".$fileinfo;
  417. break;
  418. case 'avatar':
  419. $avatar = 'prefix+ap_'.$param.'_'; // Prefix unique to user
  420. $options['overwrite'] = TRUE; // Allow update of avatar with same file name
  421. break;
  422. }
  423. if (UH_DEBUG)
  424. $admin_log->
  425. e_log_event(10, __FILE__."|".__FUNCTION__."@".__LINE__, "DEBUG", "Upload Handler test", "Legacy call, directory ".$uploaddir, FALSE, FALSE);
  426. $ret = process_uploaded_files(getcwd()."/".$uploaddir, $avatar, $options); // Well, that's the way it was done before
  427. if ($ret === FALSE)
  428. {
  429. if (UH_DEBUG)
  430. $admin_log->
  431. e_log_event(10, __FILE__."|".__FUNCTION__."@".__LINE__, "DEBUG", "Upload Handler test", "Legacy return FALSE", FALSE, FALSE);
  432. return FALSE;
  433. }
  434. if (UH_DEBUG)
  435. $admin_log->
  436. e_log_event(10, __FILE__."|".__FUNCTION__."@".__LINE__, "DEBUG", "Upload Handler test", "Legacy return with ".count($ret)." files", FALSE, FALSE);
  437. $messages = handle_upload_messages($ret, FALSE, TRUE); // Show all the error and acknowledgment messages
  438. define(F_MESSAGE, $messages);
  439. if (count($ret) == 1)
  440. {
  441. if ($ret[0]['error'] != 0)
  442. return FALSE; // Special case if errors
  443. }
  444. return $ret;
  445. }
  446. //====================================================================
  447. // VETTING AND UTILITY ROUTINES
  448. //====================================================================
  449. /**
  450. * Check uploaded file to try and identify dodgy content.
  451. * @param string $filename is the full path+name to the uploaded file on the server
  452. * @param string $target_name is the intended name of the file once transferred
  453. * @param array $allowed_filetypes is an array of permitted file extensions, in lower case, no leading '.'
  454. * (usually generated from filetypes.xml/filetypes.php)
  455. * @param boolean|string $unknown - handling of file types unknown to us/define additional types
  456. * if FALSE, rejects totally unknown file extensions (even if in $allowed_filetypes).
  457. * if $unknown is TRUE, accepts totally unknown file extensions.
  458. * otherwise $unknown is a comma-separated list of additional permitted file extensions
  459. * @return boolean|int - TRUE if file acceptable, a numeric 'fail' code if unacceptable:
  460. * 1 - file type not allowed
  461. * 2 - can't read file contents
  462. * 3 - illegal file contents (usually '<?php')
  463. * 4 - not an image file
  464. * 5 - bad image parameters
  465. * 6 - not in supplementary list
  466. * 7 - suspicious file contents
  467. * 8 - unknown file type
  468. * 9 - unacceptable file type (prone to exploits)
  469. */
  470. //TODO - Move this function to file_class.php
  471. function vet_file($filename, $target_name, $allowed_filetypes = '', $unknown = FALSE)
  472. {
  473. // 1. Start by checking against filetypes - that's the easy one!
  474. $file_ext = strtolower(substr(strrchr($target_name, '.'), 1));
  475. if (!isset($allowed_filetypes[$file_ext]))
  476. {
  477. if (is_bool($unknown))
  478. return 1; // Reject out of hand if no possible alternative extensions
  479. // Otherwise, it could be in the supplementary list
  480. $tmp = explode(',', $unknown);
  481. for ($i = 0; $i < count($tmp); $i++)
  482. {
  483. $tmp[$i] = strtolower(trim(str_replace('.', '', $tmp[$i])));
  484. }
  485. if (!in_array($file_ext, $tmp))
  486. return 6;
  487. }
  488. // 2. For all files, read the first little bit to check for any flags etc
  489. $res = fopen($filename, 'rb');
  490. $tstr = fread($res, 100);
  491. fclose($res);
  492. if ($tstr === FALSE)
  493. {
  494. return 2; // If can't read file, not much use carrying on!
  495. }
  496. if (stristr($tstr, '<?php') !== FALSE)
  497. {
  498. return 3; // Pretty certain exploit
  499. }
  500. if (stristr($tstr,'<?') !== FALSE) // Bit more tricky - can sometimes be OK
  501. {
  502. if (stristr($tstr, '<?xpacket') === FALSE) // Allow the XMP header produced by CS4
  503. {
  504. return 7;
  505. }
  506. }
  507. // 3. Now do what we can based on file extension
  508. switch ($file_ext)
  509. {
  510. case 'jpg':
  511. case 'gif':
  512. case 'png':
  513. case 'jpeg':
  514. case 'pjpeg':
  515. case 'bmp':
  516. $ret = getimagesize($filename);
  517. if (!is_array($ret))
  518. return 4; // getimagesize didn't like something
  519. if (($ret[0] == 0) || ($ret[1] == 0))
  520. return 5; // Zero size picture or bad file format
  521. break;
  522. case 'zip':
  523. case 'gzip':
  524. case 'gz':
  525. case 'tar':
  526. case 'bzip':
  527. case 'pdf':
  528. case 'rar':
  529. case '7z':
  530. case 'csv':
  531. case 'wmv':
  532. case 'swf':
  533. case 'flv': //Flash stream
  534. case 'f4v': //Flash stream
  535. case 'mov': //media
  536. case 'avi': //media
  537. break; // Just accept these
  538. case 'php':
  539. case 'htm':
  540. case 'html':
  541. case 'cgi':
  542. case 'pl':
  543. return 9; // Never accept these! Whatever the user thinks!
  544. default:
  545. if (is_bool($unknown))
  546. return ($unknown ? TRUE : 8);
  547. }
  548. return TRUE; // Accepted here
  549. }
  550. /**
  551. * Get array of file types (file extensions) which are permitted - reads a definition file.
  552. * (Similar to @See{get_XML_filetypes()}, but expects an XML file)
  553. *
  554. * @param string $def_file - name of a file containing a comma-separated list of file types, which is sought in the E_ADMIN directory
  555. * @param string $file_mask - comma-separated list of allowed file types - only those specified in both $file_mask and $def_file are returned
  556. *
  557. * @return array - where key is the file type (extension); value is max upload size
  558. */
  559. function get_allowed_filetypes($def_file = FALSE, $file_mask = '')
  560. {
  561. $ret = array(
  562. );
  563. if ($def_file === FALSE)
  564. return $ret;
  565. if ($file_mask)
  566. {
  567. $file_array = explode(',', $file_mask);
  568. foreach ($file_array as $k=>$f)
  569. {
  570. $file_array[$k] = trim($f);
  571. }
  572. }
  573. if ($def_file && is_readable(e_ADMIN.$def_file))
  574. {
  575. $a_filetypes = trim(file_get_contents(e_ADMIN.$def_file));
  576. $a_filetypes = explode(',', $a_filetypes);
  577. }
  578. foreach ($a_filetypes as $ftype)
  579. {
  580. $ftype = strtolower(trim(str_replace('.', '', $ftype)));
  581. if ($ftype && (!$file_mask || in_array($ftype, $file_array)))
  582. {
  583. $ret[$ftype] = -1;
  584. }
  585. }
  586. return $ret;
  587. }
  588. /**
  589. * Parse a file size string (e.g. 16M) and compute the simple numeric value.
  590. * Proxy to e_file::file_size_decode().
  591. *
  592. * @see e_file::file_size_decode()
  593. * @param string $source - input string which may include 'multiplier' characters such as 'M' or 'G'. Converted to 'decoded value'
  594. * @param int $compare - a 'compare' value
  595. * @param string $action - values (gt|lt)
  596. *
  597. * @return int file size value.
  598. * If the decoded value evaluates to zero, returns the value of $compare
  599. * If $action == 'gt', return the larger of the decoded value and $compare
  600. * If $action == 'lt', return the smaller of the decoded value and $compare
  601. */
  602. function file_size_decode($source, $compare = 0, $action = '')
  603. {
  604. return e107::getFile(true)->file_size_decode($source, $compare, $action);
  605. }
  606. /**
  607. * Get array of file types (file extensions) which are permitted - reads an XML-formatted definition file.
  608. * (Similar to @See{get_allowed_filetypes()}, but expects an XML file)
  609. *
  610. * @param string $def_file - name of an XML-formatted file, which is sought in the E_ADMIN directory
  611. * @param string $file_mask - comma-separated list of allowed file types - only those specified in both $file_mask and $def_file are returned
  612. *
  613. * @return array - where key is the file type (extension); value is max upload size
  614. */
  615. function get_XML_filetypes($def_file = FALSE, $file_mask = '')
  616. {
  617. $ret = array(
  618. );
  619. if ($def_file === FALSE)
  620. return $ret;
  621. if ($file_mask)
  622. {
  623. $file_array = explode(',', $file_mask);
  624. foreach ($file_array as $k=>$f)
  625. {
  626. $file_array[$k] = trim($f);
  627. }
  628. }
  629. if ($def_file && is_readable(e_SYSTEM.$def_file))
  630. {
  631. $xml = e107::getXml();
  632. // class tag should be always array
  633. $xml->setOptArrayTags('class');
  634. $temp_vars = $xml->loadXMLfile(e_SYSTEM.$def_file, 'filetypes', false);
  635. if ($temp_vars === FALSE)
  636. {
  637. echo "Error reading XML file: {$def_file}<br />";
  638. return $ret;
  639. }
  640. foreach ($temp_vars['class'] as $v1)
  641. {
  642. $v = $v1['@attributes'];
  643. if (check_class($v['name']))
  644. {
  645. $current_perms[$v['name']] = array(
  646. 'type'=>$v['type'], 'maxupload'=>$v['maxupload']
  647. );
  648. $a_filetypes = explode(',', $v['type']);
  649. foreach ($a_filetypes as $ftype)
  650. {
  651. $ftype = strtolower(trim(str_replace('.', '', $ftype))); // File extension
  652. if (!$file_mask || in_array($ftype, $file_array))
  653. { // We can load this extension
  654. if (isset($ret[$ftype]))
  655. {
  656. $ret[$ftype] = file_size_decode($v['maxupload'], $ret[$ftype], 'gt'); // Use largest value
  657. }
  658. else
  659. {
  660. $ret[$ftype] = file_size_decode($v['maxupload']);
  661. }
  662. }
  663. }
  664. }
  665. }
  666. }
  667. return $ret;
  668. }
  669. /**
  670. * Calculate 'global' maximum upload size - the maximum before extension-specific restrictions taken into account
  671. *
  672. * @param int $max_up - if > 0, its a global maximum permitted. If < 0, $pref['upload_maxfilesize'] is used (if set)
  673. *
  674. * @return int maximum allowed upload size for file
  675. */
  676. function calc_max_upload_size($max_up = -1)
  677. {
  678. global $pref;
  679. $admin_log = e107::getAdminLog();
  680. // Work out maximum allowable file size
  681. if (UH_DEBUG)
  682. {
  683. $admin_log->
  684. e_log_event(10, __FILE__."|".__FUNCTION__."@".__LINE__, "DEBUG", "Upload Handler test", "File size limits - user set: ".$pref['upload_maxfilesize']." Post_max_size: ".ini_get('post_max_size')." upload_max_size: ".ini_get('upload_max_size'), FALSE, FALSE);
  685. }
  686. $max_upload_size = file_size_decode(ini_get('post_max_size'));
  687. $max_upload_size = file_size_decode(ini_get('upload_max_filesize'), $max_upload_size, 'lt');
  688. if ($max_up > 0)
  689. {
  690. $max_upload_size = file_size_decode($max_up, $max_upload_size, 'lt');
  691. }
  692. else
  693. {
  694. if (varset($pref['upload_maxfilesize'], 0) > 0)
  695. $max_upload_size = file_size_decode($pref['upload_maxfilesize'], $max_upload_size, 'lt');
  696. }
  697. if (UH_DEBUG)
  698. $admin_log->
  699. e_log_event(10, __FILE__."|".__FUNCTION__."@".__LINE__, "DEBUG", "Upload Handler test", "Final max upload size: {$max_upload_size}", FALSE, FALSE);
  700. return $max_upload_size;
  701. }
  702. /**
  703. * Get an array of permitted filetypes according to a set hierarchy.
  704. * If a specific file name given, that's used. Otherwise the default hierarchy is used
  705. *
  706. * @param string $file_mask - comma-separated list of allowed file types
  707. * @param string $filename - optional override file name - defaults ignored
  708. *
  709. * @return array of filetypes
  710. */
  711. function get_filetypes($file_mask = FALSE, $filename = '')
  712. {
  713. if ($filename != '')
  714. {
  715. if (strtolower(substr($filename, -4) == '.xml'))
  716. {
  717. return get_XML_filetypes($filename, $file_mask);
  718. }
  719. return get_allowed_filetypes($filename, $file_mask);
  720. }
  721. if (is_readable(e_SYSTEM.e_READ_FILETYPES))
  722. {
  723. return get_XML_filetypes(e_READ_FILETYPES, $file_mask);
  724. }
  725. if (ADMIN && is_readable(e_ADMIN.'admin_filetypes.php'))
  726. {
  727. return get_allowed_filetypes('admin_filetypes.php', $file_mask);
  728. }
  729. if (is_readable(e_ADMIN.'filetypes.php'))
  730. {
  731. return get_allowed_filetypes('filetypes.php', $file_mask);
  732. }
  733. return array(); // Just an empty array
  734. }
  735. /**
  736. * Scans the array of allowed file types, updates allowed max size as appropriate.
  737. * If the value is larger than the site-wide maximum, reduces it.
  738. *
  739. * @param array $allowed_filetypes - key is file type (extension), value is maximum size allowed
  740. * @param int $max_upload_size - site-wide maximum file upload size
  741. *
  742. * @return int largest allowed file size across all file types
  743. */
  744. function set_max_size(&$allowed_filetypes, $max_upload_size)
  745. {
  746. $new_max = 0;
  747. foreach ($allowed_filetypes as $t=>$s)
  748. {
  749. if ($s < 0)
  750. { // Unspecified max - use the global value
  751. $allowed_filetypes[$t] = $max_upload_size;
  752. }
  753. elseif ($allowed_filetypes[$t] > $max_upload_size)
  754. $allowed_filetypes[$t] = $max_upload_size;
  755. if ($allowed_filetypes[$t] > $new_max)
  756. $new_max = $allowed_filetypes[$t];
  757. }
  758. return $new_max;
  759. }
  760. /*
  761. * Quick routine if all we want is the size of the largest file the current user can upload
  762. *
  763. * @return int largest allowed file size across all file types
  764. */
  765. function get_user_max_upload()
  766. {
  767. $a_filetypes = get_filetypes();
  768. if (count($a_filetypes) == 0)
  769. return 0; // Return if no upload allowed
  770. $max_upload_size = calc_max_upload_size(-1); // Find overriding maximum upload size
  771. $max_upload_size = set_max_size($a_filetypes, $max_upload_size);
  772. return $max_upload_size;
  773. }
  774. ?>