PageRenderTime 67ms CodeModel.GetById 34ms RepoModel.GetById 0ms app.codeStats 0ms

/fuel/core/classes/upload.php

https://bitbucket.org/arkross/venus
PHP | 710 lines | 464 code | 80 blank | 166 comment | 58 complexity | 34afe0ba9284677e1a128b9735af4f92 MD5 | raw file
Possible License(s): MIT, BSD-3-Clause
  1. <?php
  2. /**
  3. * Part of the Fuel framework.
  4. *
  5. * @package Fuel
  6. * @version 1.0
  7. * @author Fuel Development Team
  8. * @license MIT License
  9. * @copyright 2010 - 2011 Fuel Development Team
  10. * @link http://fuelphp.com
  11. */
  12. namespace Fuel\Core;
  13. /**
  14. * Upload Class
  15. *
  16. * @package Fuel
  17. * @category Core
  18. * @author Harro "WanWizard" Verton
  19. * @link http://docs.fuelphp.com/classes/upload.html
  20. */
  21. class Upload
  22. {
  23. /* ---------------------------------------------------------------------------
  24. * ERROR CODE CONSTANTS
  25. * --------------------------------------------------------------------------- */
  26. // duplicate the PHP standard error codes for consistency
  27. const UPLOAD_ERR_OK = UPLOAD_ERR_OK;
  28. const UPLOAD_ERR_INI_SIZE = UPLOAD_ERR_INI_SIZE;
  29. const UPLOAD_ERR_FORM_SIZE = UPLOAD_ERR_FORM_SIZE;
  30. const UPLOAD_ERR_PARTIAL = UPLOAD_ERR_PARTIAL;
  31. const UPLOAD_ERR_NO_FILE = UPLOAD_ERR_NO_FILE;
  32. const UPLOAD_ERR_NO_TMP_DIR = UPLOAD_ERR_NO_TMP_DIR;
  33. const UPLOAD_ERR_CANT_WRITE = UPLOAD_ERR_CANT_WRITE;
  34. const UPLOAD_ERR_EXTENSION = UPLOAD_ERR_EXTENSION;
  35. // and add our own error codes
  36. const UPLOAD_ERR_MAX_SIZE = 101;
  37. const UPLOAD_ERR_EXT_BLACKLISTED = 102;
  38. const UPLOAD_ERR_EXT_NOT_WHITELISTED = 103;
  39. const UPLOAD_ERR_TYPE_BLACKLISTED = 104;
  40. const UPLOAD_ERR_TYPE_NOT_WHITELISTED = 105;
  41. const UPLOAD_ERR_MIME_BLACKLISTED = 106;
  42. const UPLOAD_ERR_MIME_NOT_WHITELISTED = 107;
  43. const UPLOAD_ERR_MAX_FILENAME_LENGTH = 108;
  44. const UPLOAD_ERR_MOVE_FAILED = 109;
  45. const UPLOAD_ERR_DUPLICATE_FILE = 110;
  46. const UPLOAD_ERR_MKDIR_FAILED = 111;
  47. const UPLOAD_ERR_FTP_FAILED = 112;
  48. /* ---------------------------------------------------------------------------
  49. * STATIC PROPERTIES
  50. * --------------------------------------------------------------------------- */
  51. /**
  52. * @var array default configuration values
  53. */
  54. protected static $_defaults = array(
  55. 'auto_process' => false,
  56. // validation settings
  57. 'max_size' => 0,
  58. 'max_length' => 0,
  59. 'ext_whitelist' => array(),
  60. 'ext_blacklist' => array(),
  61. 'type_whitelist' => array(),
  62. 'type_blacklist' => array(),
  63. 'mime_whitelist' => array(),
  64. 'mime_blacklist' => array(),
  65. // save settings
  66. 'path' => '',
  67. 'prefix' => '',
  68. 'suffix' => '',
  69. 'extension' => '',
  70. 'create_path' => true,
  71. 'path_chmod' => 0777,
  72. 'file_chmod' => 0666,
  73. 'auto_rename' => true,
  74. 'overwrite' => false,
  75. 'randomize' => false,
  76. 'normalize' => false,
  77. 'change_case' => false,
  78. 'ftp_mode' => 'auto',
  79. 'ftp_permissions' => null
  80. );
  81. /**
  82. * @var array defined callbacks
  83. */
  84. protected static $callbacks = array(
  85. 'validate' => null,
  86. 'before' => null,
  87. 'after' => null
  88. );
  89. /**
  90. * @var array configuration of this instance
  91. */
  92. protected static $config = array();
  93. /**
  94. * @var array normalized $_FILES array
  95. */
  96. protected static $files = array();
  97. /**
  98. * @var bool indicator of valid uploads
  99. */
  100. protected static $valid = false;
  101. /**
  102. * @var object Ftp object
  103. */
  104. protected static $with_ftp = false;
  105. /* ---------------------------------------------------------------------------
  106. * STATIC METHODS
  107. * --------------------------------------------------------------------------- */
  108. /**
  109. * class initialisation, load the config and process $_FILES if needed
  110. *
  111. * @return void
  112. */
  113. public static function _init()
  114. {
  115. // get the config for this upload
  116. \Config::load('upload', true);
  117. // get the language file for this upload
  118. \Lang::load('upload', true);
  119. // make sure we have defaults for those not defined
  120. static::$config = array_merge(static::$_defaults, \Config::get('upload', array()));
  121. static::$config['auto_process'] and static::process();
  122. }
  123. // ---------------------------------------------------------------------------
  124. /**
  125. * Check if we have valid files
  126. *
  127. * @return bool true if static:$files contains uploaded files that are valid
  128. */
  129. public static function is_valid()
  130. {
  131. return static::$valid;
  132. }
  133. // ---------------------------------------------------------------------------
  134. /**
  135. * Get the list of validated files
  136. *
  137. * @return array list of uploaded files that are validated
  138. */
  139. public static function get_files($index = null)
  140. {
  141. if (func_num_args() == 0)
  142. {
  143. return array_filter(static::$files, function($file) { return $file['error'] === false; } );
  144. }
  145. elseif (isset(static::$files[$index]) and static::$files[$index]['error'] === false)
  146. {
  147. return static::$files[$index];
  148. }
  149. else
  150. {
  151. throw new \FuelException('No valid uploaded file exists with index "'.$index.'"');
  152. }
  153. }
  154. // ---------------------------------------------------------------------------
  155. /**
  156. * Get the list of non-validated files
  157. *
  158. * @return array list of uploaded files that failed to validate
  159. */
  160. public static function get_errors($index = null)
  161. {
  162. if (func_num_args() == 0)
  163. {
  164. return array_filter(static::$files, function($file) { return $file['error'] === true; } );
  165. }
  166. elseif (isset(static::$files[$index]) and static::$files[$index]['error'] === true)
  167. {
  168. return static::$files[$index];
  169. }
  170. else
  171. {
  172. throw new \FuelException('No invalid uploaded file exists with index "'.$index.'"');
  173. }
  174. }
  175. // --------------------------------------------------------------------
  176. /**
  177. * Register
  178. *
  179. * Registers a Callback for a given event
  180. *
  181. * @access public
  182. * @param string The name of the event
  183. * @param mixed callback information
  184. * @return void
  185. */
  186. public static function register()
  187. {
  188. // get any arguments passed
  189. $callback = func_get_args();
  190. // if the arguments are valid, register the callback
  191. if (isset($callback[0]) and is_string($callback[0]) and isset($callback[1]) and is_callable($callback[1]))
  192. {
  193. // make sure we have an entry for this callback
  194. if (array_key_exists($callback[0], static::$callbacks))
  195. {
  196. static::$callbacks[array_shift($callback)] = $callback;
  197. // report success
  198. return true;
  199. }
  200. }
  201. // can't register the callback
  202. return false;
  203. }
  204. // ---------------------------------------------------------------------------
  205. /**
  206. * Normalize the $_FILES array and store the result in $files
  207. *
  208. * @return void
  209. */
  210. public static function process($config = array())
  211. {
  212. // process runtime config
  213. if (is_array($config))
  214. {
  215. static::$config = array_merge(static::$config, $config);
  216. }
  217. // processed files array
  218. static::$files = $files = array();
  219. // normalize the $_FILES array
  220. foreach($_FILES as $name => $value)
  221. {
  222. // if the variable is an array, flatten it
  223. if (is_array($value['name']))
  224. {
  225. $keys = array_keys($value['name']);
  226. foreach ($keys as $key)
  227. {
  228. // store the file data
  229. $file = array('field' => $name, 'key' => $key);
  230. $file['name'] = $value['name'][$key];
  231. $file['type'] = $value['type'][$key];
  232. $file['file'] = $value['tmp_name'][$key];
  233. if ($value['error'][$key])
  234. {
  235. $file['error'] = true;
  236. $file['errors'][] = array('error' => $value['error'][$key]);
  237. }
  238. else
  239. {
  240. $file['error'] = false;
  241. $file['errors'] = array();
  242. }
  243. $file['size'] = $value['size'][$key];
  244. $files[] = $file;
  245. }
  246. }
  247. else
  248. {
  249. // store the file data
  250. $file = array('field' => $name, 'key' => false, 'file' => $value['tmp_name']);
  251. if ($value['error'])
  252. {
  253. $file['error'] = true;
  254. $file['errors'][] = array('error' => $value['error']);
  255. }
  256. else
  257. {
  258. $file['error'] = false;
  259. $file['errors'] = array();
  260. }
  261. unset($value['tmp_name']);
  262. $files[] = array_merge($value, $file);
  263. }
  264. }
  265. // verify and augment the files data
  266. foreach($files as $key => $value)
  267. {
  268. // add some filename details (pathinfo can't be trusted with utf-8 filenames!)
  269. $files[$key]['extension'] = ltrim(strrchr(ltrim($files[$key]['name'], '.'), '.'),'.');
  270. if (empty($files[$key]['extension']))
  271. {
  272. $files[$key]['filename'] = $files[$key]['name'];
  273. }
  274. else
  275. {
  276. $files[$key]['filename'] = substr($files[$key]['name'], 0, strlen($files[$key]['name'])-(strlen($files[$key]['extension'])+1));
  277. }
  278. // does this upload exceed the maximum size?
  279. if (! empty(static::$config['max_size']) and $files[$key]['size'] > static::$config['max_size'])
  280. {
  281. $files[$key]['error'] = true;
  282. $files[$key]['errors'][] = array('error' => static::UPLOAD_ERR_MAX_SIZE);
  283. }
  284. // add mimetype information
  285. if ( ! $files[$key]['error'])
  286. {
  287. $handle = finfo_open(FILEINFO_MIME_TYPE);
  288. $files[$key]['mimetype'] = finfo_file($handle, $value['file']);
  289. finfo_close($handle);
  290. if ($files[$key]['mimetype'] == 'application/octet-stream' and $files[$key]['type'] != $files[$key]['mimetype'])
  291. {
  292. $files[$key]['mimetype'] = $files[$key]['type'];
  293. }
  294. // make sure it contains something valid
  295. if (empty($files[$key]['mimetype']))
  296. {
  297. $files[$key]['mimetype'] = 'application/octet-stream';
  298. }
  299. }
  300. // check the file extension black- and whitelists
  301. if ( ! $files[$key]['error'])
  302. {
  303. if (in_array(strtolower($files[$key]['extension']), (array) static::$config['ext_blacklist']))
  304. {
  305. $files[$key]['error'] = true;
  306. $files[$key]['errors'][] = array('error' => static::UPLOAD_ERR_EXT_BLACKLISTED);
  307. }
  308. elseif ( ! empty(static::$config['ext_whitelist']) and ! in_array(strtolower($files[$key]['extension']), (array) static::$config['ext_whitelist']))
  309. {
  310. $files[$key]['error'] = true;
  311. $files[$key]['errors'][] = array('error' => static::UPLOAD_ERR_EXT_NOT_WHITELISTED);
  312. }
  313. }
  314. // check the file type black- and whitelists
  315. if ( ! $files[$key]['error'])
  316. {
  317. // split the mimetype info so we can run some tests
  318. preg_match('|^(.*)/(.*)|', $files[$key]['mimetype'], $mimeinfo);
  319. if (in_array($mimeinfo[1], (array) static::$config['type_blacklist']))
  320. {
  321. $files[$key]['error'] = true;
  322. $files[$key]['errors'][] = array('error' => static::UPLOAD_ERR_TYPE_BLACKLISTED);
  323. }
  324. if ( ! empty(static::$config['type_whitelist']) and ! in_array($mimeinfo[1], (array) static::$config['type_whitelist']))
  325. {
  326. $files[$key]['error'] = true;
  327. $files[$key]['errors'][] = array('error' => static::UPLOAD_ERR_TYPE_NOT_WHITELISTED);
  328. }
  329. }
  330. // check the file mimetype black- and whitelists
  331. if ( ! $files[$key]['error'])
  332. {
  333. if (in_array($files[$key]['mimetype'], (array) static::$config['mime_blacklist']))
  334. {
  335. $files[$key]['error'] = true;
  336. $files[$key]['errors'][] = array('error' => static::UPLOAD_ERR_MIME_BLACKLISTED);
  337. }
  338. elseif ( ! empty(static::$config['mime_whitelist']) and ! in_array($files[$key]['mimetype'], (array) static::$config['mime_whitelist']))
  339. {
  340. $files[$key]['error'] = true;
  341. $files[$key]['errors'][] = array('error' => static::UPLOAD_ERR_MIME_NOT_WHITELISTED);
  342. }
  343. }
  344. // store the normalized and validated result
  345. static::$files[$key] = $files[$key];
  346. // validation callback defined?
  347. if (array_key_exists('validate', static::$callbacks) and ! is_null(static::$callbacks['validate']))
  348. {
  349. // get the callback method
  350. $callback = static::$callbacks['validate'][0];
  351. // call the callback
  352. if (is_callable($callback))
  353. {
  354. $result = call_user_func_array($callback, array(&static::$files[$key]));
  355. if (is_numeric($result) and $result)
  356. {
  357. static::$files[$key]['error'] = true;
  358. static::$files[$key]['errors'][] = array('error' => $result);
  359. }
  360. }
  361. }
  362. // and add the message texts
  363. foreach (static::$files[$key]['errors'] as $e => $error)
  364. {
  365. static::$files[$key]['errors'][$e]['message'] = \Lang::get('upload.'.$error['error']);
  366. }
  367. }
  368. // determine the validate status of at least one uploaded file
  369. $valid = false;
  370. foreach(static::$files as $key => $value)
  371. {
  372. if ($value['error'] === false)
  373. {
  374. $valid = true;
  375. break;
  376. }
  377. }
  378. static::$valid = $valid;
  379. }
  380. // ---------------------------------------------------------------------------
  381. /**
  382. * Upload files with Ftp
  383. *
  384. * @param string|array The name of the config group to use, or a configuration array.
  385. * @param bool Automatically connect to this server.
  386. */
  387. public static function with_ftp($config = 'default', $connect = true)
  388. {
  389. static::$with_ftp = \Ftp::forge($config, $connect);
  390. }
  391. // ---------------------------------------------------------------------------
  392. /**
  393. * save uploaded file(s)
  394. *
  395. * @param mixed if int, $files element to move. if array, list of elements to move, if none, move all elements
  396. * @param string path to move to
  397. * @return void
  398. */
  399. public static function save()
  400. {
  401. // path to save the files to
  402. $path = static::$config['path'];
  403. // files to save
  404. $files = array();
  405. // check for parameters
  406. if (func_num_args())
  407. {
  408. foreach(func_get_args() as $param)
  409. {
  410. // string => new path to save to
  411. if (is_string($param))
  412. {
  413. $path = $param;
  414. }
  415. // array => list of $files indexes to save
  416. elseif(is_array($param))
  417. {
  418. $files = array();
  419. foreach($param as $key)
  420. {
  421. if (isset(static::$files[(int) $key]))
  422. {
  423. $files[(int) $key] = static::$files[(int) $key];
  424. }
  425. }
  426. }
  427. // integer => files index to save
  428. elseif(is_numeric($param))
  429. {
  430. if (isset(static::$files[$param]))
  431. {
  432. $files[$param] = static::$files[$param];
  433. }
  434. }
  435. }
  436. }
  437. else
  438. {
  439. // save all files
  440. $files = static::$files;
  441. }
  442. // anything to save?
  443. if (empty($files))
  444. {
  445. throw new \FuelException('No uploaded files are selected.');
  446. }
  447. // supplied new name and not auto renaming?
  448. if (array_key_exists('new_name', static::$config) and ! static::$config['auto_rename'] and count($files) > 1)
  449. {
  450. throw new \FuelException('Can\'t rename multiple files without auto renaming.');
  451. }
  452. // make sure we have a valid path
  453. $path = rtrim($path, DS).DS;
  454. if ( ! is_dir($path) and (bool) static::$config['create_path'])
  455. {
  456. $oldumask = umask(0);
  457. @mkdir($path, static::$config['path_chmod'], true);
  458. umask($oldumask);
  459. }
  460. if ( ! is_dir($path))
  461. {
  462. throw new \FuelException('Can\'t move the uploaded file. Destination path specified does not exist.');
  463. }
  464. // save the old umask
  465. $oldumask = umask(0);
  466. // now that we have a path, let's save the files
  467. foreach($files as $key => $file)
  468. {
  469. // skip all files in error
  470. if ($file['error'] === true)
  471. {
  472. continue;
  473. }
  474. // do we need to generate a random filename?
  475. if ( (bool) static::$config['randomize'])
  476. {
  477. $filename = md5(serialize($file));
  478. }
  479. else
  480. {
  481. $filename = $file['filename'];
  482. if ( (bool) static::$config['normalize'])
  483. {
  484. $filename = \Inflector::friendly_title($filename, '_');
  485. }
  486. }
  487. array_key_exists('new_name', static::$config) and $filename = (string) static::$config['new_name'];
  488. // array with the final filename
  489. $save_as = array(
  490. static::$config['prefix'],
  491. $filename,
  492. static::$config['suffix'],
  493. '',
  494. '.',
  495. empty(static::$config['extension']) ? $file['extension'] : static::$config['extension']
  496. );
  497. // remove the dot if no extension is present
  498. if (empty($save_as[5]))
  499. {
  500. $save_as[4] = '';
  501. }
  502. // need to modify case?
  503. switch(static::$config['change_case'])
  504. {
  505. case 'upper':
  506. $save_as = array_map(function($var) { return strtoupper($var); }, $save_as);
  507. break;
  508. case 'lower':
  509. $save_as = array_map(function($var) { return strtolower($var); }, $save_as);
  510. break;
  511. default:
  512. break;
  513. }
  514. // check if the file already exists
  515. if (file_exists($path.implode('', $save_as)))
  516. {
  517. if ( (bool) static::$config['auto_rename'])
  518. {
  519. $counter = 0;
  520. do
  521. {
  522. $save_as[3] = '_'.++$counter;
  523. }
  524. while (file_exists($path.implode('', $save_as)));
  525. }
  526. else
  527. {
  528. if ( ! (bool) static::$config['overwrite'])
  529. {
  530. static::$files[$key]['error'] = true;
  531. static::$files[$key]['errors'][] = array('error' => static::UPLOAD_ERR_DUPLICATE_FILE);
  532. continue;
  533. }
  534. }
  535. }
  536. // no need to store it as an array anymore
  537. $save_as = implode('', $save_as);
  538. // does the filename exceed the maximum length?
  539. if ( ! empty(static::$config['max_length']) and strlen($save_as) > static::$config['max_length'])
  540. {
  541. static::$files[$key]['error'] = true;
  542. static::$files[$key]['errors'][] = array('error' => static::UPLOAD_ERR_MAX_FILENAME_LENGTH);
  543. continue;
  544. }
  545. // if no error was detected, move the file
  546. if ( ! static::$files[$key]['error'])
  547. {
  548. // save the additional information
  549. static::$files[$key]['saved_to'] = $path;
  550. static::$files[$key]['saved_as'] = $save_as;
  551. // before callback defined?
  552. if (array_key_exists('before', static::$callbacks) and ! is_null(static::$callbacks['before']))
  553. {
  554. // get the callback method
  555. $callback = static::$callbacks['before'][0];
  556. // call the callback
  557. if (is_callable($callback))
  558. {
  559. $result = call_user_func_array($callback, array(&static::$files[$key]));
  560. if (is_numeric($result))
  561. {
  562. static::$files[$key]['error'] = true;
  563. static::$files[$key]['errors'][] = array('error' => $result);
  564. }
  565. }
  566. // recheck the saved_to path, it might have been altered
  567. if ( ! is_dir(static::$files[$key]['saved_to']) and (bool) static::$config['create_path'])
  568. {
  569. @mkdir(static::$files[$key]['saved_to'], static::$config['path_chmod'], true);
  570. }
  571. if ( ! is_dir(static::$files[$key]['saved_to']))
  572. {
  573. static::$files[$key]['error'] = true;
  574. static::$files[$key]['errors'][] = array('error' => static::UPLOAD_ERR_MKDIR_FAILED);
  575. continue;
  576. }
  577. }
  578. // move the uploaded file
  579. if ( ! static::$files[$key]['error'])
  580. {
  581. // check if file should be uploaded with ftp
  582. if (static::$with_ftp)
  583. {
  584. $uploaded = static::$with_ftp->upload($file['file'], static::$config['path'].static::$files[$key]['saved_as'], static::$config['ftp_mode'], static::$config['ftp_permissions']);
  585. if ( ! $uploaded)
  586. {
  587. static::$files[$key]['error'] = true;
  588. static::$files[$key]['errors'][] = array('error' => static::UPLOAD_ERR_FTP_FAILED);
  589. }
  590. }
  591. else
  592. {
  593. if( ! @move_uploaded_file($file['file'], static::$files[$key]['saved_to'].static::$files[$key]['saved_as']) )
  594. {
  595. static::$files[$key]['error'] = true;
  596. static::$files[$key]['errors'][] = array('error' => static::UPLOAD_ERR_MOVE_FAILED);
  597. }
  598. else
  599. {
  600. @chmod(static::$files[$key]['saved_to'].static::$files[$key]['saved_as'], static::$config['file_chmod']);
  601. }
  602. // after callback defined?
  603. if (array_key_exists('after', static::$callbacks) and ! is_null(static::$callbacks['after']))
  604. {
  605. // get the callback method
  606. $callback = static::$callbacks['after'][0];
  607. // call the callback
  608. if (is_callable($callback))
  609. {
  610. $result = call_user_func_array($callback, array(&static::$files[$key]));
  611. if (is_numeric($result))
  612. {
  613. static::$files[$key]['error'] = true;
  614. static::$files[$key]['errors'][] = array('error' => $result);
  615. }
  616. }
  617. }
  618. }
  619. }
  620. }
  621. // and add the message texts
  622. foreach (static::$files[$key]['errors'] as $e => $error)
  623. {
  624. empty(static::$files[$key]['errors'][$e]['message']) and static::$files[$key]['errors'][$e]['message'] = \Lang::get('upload.'.$error['error']);
  625. }
  626. }
  627. // reset the umask
  628. umask($oldumask);
  629. }
  630. }