PageRenderTime 57ms CodeModel.GetById 12ms RepoModel.GetById 1ms app.codeStats 0ms

/vendor/fuelphp/upload/src/FuelPHP/Upload/File.php

https://github.com/jay3/core
PHP | 611 lines | 357 code | 74 blank | 180 comment | 36 complexity | a6b149d0374bff5967d90e9be4cced16 MD5 | raw file
Possible License(s): LGPL-2.1
  1. <?php
  2. /**
  3. * Part of the Fuel framework.
  4. *
  5. * @package FuelPHP
  6. * @version 2.0
  7. * @author Fuel Development Team
  8. * @license MIT License
  9. * @copyright 2010 - 2013 Fuel Development Team
  10. * @link http://fuelphp.com
  11. */
  12. namespace FuelPHP\Upload;
  13. /**
  14. * Files is a container for a single uploaded file
  15. */
  16. class File implements \ArrayAccess, \Iterator, \Countable
  17. {
  18. /**
  19. * Our custom error code constants
  20. */
  21. const UPLOAD_ERR_MAX_SIZE = 101;
  22. const UPLOAD_ERR_EXT_BLACKLISTED = 102;
  23. const UPLOAD_ERR_EXT_NOT_WHITELISTED = 103;
  24. const UPLOAD_ERR_TYPE_BLACKLISTED = 104;
  25. const UPLOAD_ERR_TYPE_NOT_WHITELISTED = 105;
  26. const UPLOAD_ERR_MIME_BLACKLISTED = 106;
  27. const UPLOAD_ERR_MIME_NOT_WHITELISTED = 107;
  28. const UPLOAD_ERR_MAX_FILENAME_LENGTH = 108;
  29. const UPLOAD_ERR_MOVE_FAILED = 109;
  30. const UPLOAD_ERR_DUPLICATE_FILE = 110;
  31. const UPLOAD_ERR_MKDIR_FAILED = 111;
  32. const UPLOAD_ERR_EXTERNAL_MOVE_FAILED = 112;
  33. /**
  34. * @var array Container for uploaded file objects
  35. */
  36. protected $container = array();
  37. /**
  38. * @var int index pointer for Iterator
  39. */
  40. protected $index = 0;
  41. /**
  42. * @var array Container for validation errors
  43. */
  44. protected $errors = array();
  45. /**
  46. * @var array Configuration values
  47. */
  48. protected $config = array(
  49. 'langCallback' => null,
  50. 'moveCallback' => null,
  51. // validation settings
  52. 'max_size' => 0,
  53. 'max_length' => 0,
  54. 'ext_whitelist' => array(),
  55. 'ext_blacklist' => array(),
  56. 'type_whitelist' => array(),
  57. 'type_blacklist' => array(),
  58. 'mime_whitelist' => array(),
  59. 'mime_blacklist' => array(),
  60. // file settings
  61. 'prefix' => '',
  62. 'suffix' => '',
  63. 'extension' => '',
  64. 'randomize' => false,
  65. 'normalize' => false,
  66. 'normalize_separator' => '_',
  67. 'change_case' => false,
  68. // save-to-disk settings
  69. 'path' => '',
  70. 'create_path' => true,
  71. 'path_chmod' => 0777,
  72. 'file_chmod' => 0666,
  73. 'auto_rename' => true,
  74. 'overwrite' => false,
  75. );
  76. /**
  77. * @var bool Flag to indicate if validation has run on this object
  78. */
  79. protected $isValidated = false;
  80. /**
  81. * @var bool Flag to indicate the result of the validation run
  82. */
  83. protected $isValid = false;
  84. /**
  85. * @var array Container for callbacks
  86. */
  87. protected $callbacks = array();
  88. /**
  89. * Constructor
  90. *
  91. * @param array $file Array with unified information about the file uploaded
  92. * @param array|null $callbacks
  93. */
  94. public function __construct(array $file, &$callbacks = array())
  95. {
  96. // store the file data for this file
  97. $this->container = $file;
  98. // the file callbacks reference
  99. $this->callbacks =& $callbacks;
  100. }
  101. /**
  102. * Magic getter, gives read access to all elements in the file container
  103. *
  104. * @param string $name name of the container item to get
  105. *
  106. * @return mixed value of the item, or null if the item does not exist
  107. */
  108. public function __get($name)
  109. {
  110. $name = strtolower($name);
  111. return isset($this->container[$name]) ? $this->container[$name] : null;
  112. }
  113. /**
  114. * Magic setter, gives write access to all elements in the file container
  115. *
  116. * @param string $name name of the container item to set
  117. * @param mixed $value value to set it to
  118. */
  119. public function __set($name, $value)
  120. {
  121. $name = strtolower($name);
  122. array_key_exists($name, $this->container) and $this->container[$name] = $value;
  123. }
  124. /**
  125. * Return the validation state of this object
  126. *
  127. * @return bool
  128. */
  129. public function isValidated()
  130. {
  131. return $this->isValidated;
  132. }
  133. /**
  134. * Return the state of this object
  135. *
  136. * @return bool
  137. */
  138. public function isValid()
  139. {
  140. return $this->isValid;
  141. }
  142. /**
  143. * Return the error objects collected for this file upload
  144. *
  145. * @return FileError[]
  146. */
  147. public function getErrors()
  148. {
  149. return $this->isValidated ? $this->errors : array();
  150. }
  151. /**
  152. * Set the configuration for this file
  153. *
  154. * @param string|array $item name of the configuration item to set, or an array of configuration items
  155. * @param mixed $value if $name is an item name, this holds the configuration values for that item
  156. *
  157. * @return void
  158. */
  159. public function setConfig($item, $value = null)
  160. {
  161. // unify the parameters
  162. is_array($item) or $item = array($item => $value);
  163. // update the configuration
  164. foreach ($item as $name => $value)
  165. {
  166. array_key_exists($name, $this->config) and $this->config[$name] = $value;
  167. }
  168. }
  169. /**
  170. * Run validation on the uploaded file, based on the config being loaded
  171. *
  172. * @return bool
  173. */
  174. public function validate()
  175. {
  176. // reset the error container
  177. $this->errors = array();
  178. // validation starts, call the pre-validation callback
  179. $this->runCallbacks('before_validation');
  180. // was the upload of the file a success?
  181. if ($this->container['error'] == 0)
  182. {
  183. // add some filename details (pathinfo can't be trusted with utf-8 filenames!)
  184. $this->container['extension'] = ltrim(strrchr(ltrim($this->container['name'], '.'), '.'),'.');
  185. if (empty($this->container['extension']))
  186. {
  187. $this->container['basename'] = $this->container['name'];
  188. }
  189. else
  190. {
  191. $this->container['basename'] = substr($this->container['name'], 0, strlen($this->container['name'])-(strlen($this->container['extension'])+1));
  192. }
  193. // does this upload exceed the maximum size?
  194. if ( ! empty($this->config['max_size']) and is_numeric($this->config['max_size']) and $this->container['size'] > $this->config['max_size'])
  195. {
  196. $this->addError(static::UPLOAD_ERR_MAX_SIZE);
  197. }
  198. // add mimetype information
  199. try
  200. {
  201. $handle = finfo_open(FILEINFO_MIME_TYPE);
  202. $this->container['mimetype'] = finfo_file($handle, $this->container['tmp_name']);
  203. finfo_close($handle);
  204. }
  205. // this will only work if PHP errors are converted into ErrorException (like when you use FuelPHP)
  206. catch (\ErrorException $e)
  207. {
  208. $this->container['mimetype'] = false;
  209. $this->addError(UPLOAD_ERR_NO_FILE);
  210. }
  211. // always use the more specific of the mime types available
  212. if ($this->container['mimetype'] == 'application/octet-stream' and $this->container['type'] != $this->container['mimetype'])
  213. {
  214. $this->container['mimetype'] = $this->container['type'];
  215. }
  216. // make sure it contains something valid
  217. if (empty($this->container['mimetype']) or strpos($this->container['mimetype'], '/') === false)
  218. {
  219. $this->container['mimetype'] = 'application/octet-stream';
  220. }
  221. // split the mimetype info so we can run some tests
  222. preg_match('|^(.*)/(.*)|', $this->container['mimetype'], $mimeinfo);
  223. // check the file extension black- and whitelists
  224. if (in_array(strtolower($this->container['extension']), (array) $this->config['ext_blacklist']))
  225. {
  226. $this->addError(static::UPLOAD_ERR_EXT_BLACKLISTED);
  227. }
  228. elseif ( ! empty($this->config['ext_whitelist']) and ! in_array(strtolower($this->container['extension']), (array) $this->config['ext_whitelist']))
  229. {
  230. $this->addError(static::UPLOAD_ERR_EXT_NOT_WHITELISTED);
  231. }
  232. // check the file type black- and whitelists
  233. if (in_array($mimeinfo[1], (array) $this->config['type_blacklist']))
  234. {
  235. $this->addError(static::UPLOAD_ERR_TYPE_BLACKLISTED);
  236. }
  237. if ( ! empty($this->config['type_whitelist']) and ! in_array($mimeinfo[1], (array) $this->config['type_whitelist']))
  238. {
  239. $this->addError(static::UPLOAD_ERR_TYPE_NOT_WHITELISTED);
  240. }
  241. // check the file mimetype black- and whitelists
  242. if (in_array($this->container['mimetype'], (array) $this->config['mime_blacklist']))
  243. {
  244. $this->addError(static::UPLOAD_ERR_MIME_BLACKLISTED);
  245. }
  246. elseif ( ! empty($this->config['mime_whitelist']) and ! in_array($this->container['mimetype'], (array) $this->config['mime_whitelist']))
  247. {
  248. $this->addError(static::UPLOAD_ERR_MIME_NOT_WHITELISTED);
  249. }
  250. // update the status of this validation
  251. $this->isValid = empty($this->errors);
  252. // validation finished, call the post-validation callback
  253. $this->runCallbacks('after_validation');
  254. }
  255. else
  256. {
  257. // upload was already a failure, store the corresponding error
  258. $this->addError($this->container['error']);
  259. // and mark this validation a failure
  260. $this->isValid = false;
  261. }
  262. // set the flag to indicate we ran the validation
  263. $this->isValidated = true;
  264. // return the validation state
  265. return $this->isValid;
  266. }
  267. /**
  268. * Save the uploaded file
  269. *
  270. * @throws \DomainException Destination path specified does not exist
  271. *
  272. * @return bool
  273. */
  274. public function save()
  275. {
  276. // we can only save files marked as valid
  277. if ($this->isValid)
  278. {
  279. // make sure we have a valid path
  280. if (empty($this->container['path']))
  281. {
  282. $this->container['path'] = rtrim($this->config['path'], DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
  283. }
  284. if ( ! is_dir($this->container['path']) and (bool) $this->config['create_path'])
  285. {
  286. @mkdir($this->container['path'], $this->config['path_chmod'], true);
  287. if ( ! is_dir($this->container['path']))
  288. {
  289. throw new \DomainException('Can\'t save the uploaded file. Destination path specified does not exist.');
  290. }
  291. }
  292. $this->container['path'] = realpath($this->container['path']).DIRECTORY_SEPARATOR;
  293. // was a new name for the file given?
  294. if ( ! is_string($this->container['filename']) or $this->container['filename'] === '')
  295. {
  296. // do we need to generate a random filename?
  297. if ( (bool) $this->config['randomize'])
  298. {
  299. $this->container['filename'] = md5(serialize($this->container));
  300. }
  301. // do we need to normalize the filename?
  302. else
  303. {
  304. $this->container['filename'] = $this->container['basename'];
  305. (bool) $this->config['normalize'] and $this->normalize();
  306. }
  307. }
  308. // array with all filename components
  309. $filename = array(
  310. $this->config['prefix'],
  311. $this->container['filename'],
  312. $this->config['suffix'],
  313. '',
  314. '.',
  315. empty($this->config['extension']) ? $this->container['extension'] : $this->config['extension']
  316. );
  317. // remove the dot if no extension is present
  318. empty($filename[5]) and $filename[4] = '';
  319. // need to modify case?
  320. switch($this->config['change_case'])
  321. {
  322. case 'upper':
  323. $filename = array_map(function($var) { return strtoupper($var); }, $filename);
  324. break;
  325. case 'lower':
  326. $filename = array_map(function($var) { return strtolower($var); }, $filename);
  327. break;
  328. default:
  329. break;
  330. }
  331. // if we're saving the file locally
  332. if ( ! $this->config['moveCallback'])
  333. {
  334. // check if the file already exists
  335. if (file_exists($this->container['path'].implode('', $filename)))
  336. {
  337. // generate a unique filename if needed
  338. if ( (bool) $this->config['auto_rename'])
  339. {
  340. $counter = 0;
  341. do
  342. {
  343. $filename[3] = '_'.++$counter;
  344. }
  345. while (file_exists($this->container['path'].implode('', $filename)));
  346. // claim this generated filename before someone else does
  347. touch($this->container['path'].implode('', $filename));
  348. }
  349. else
  350. {
  351. // if we can't overwrite, we've got to bail out now
  352. if ( ! (bool) $this->config['overwrite'])
  353. {
  354. $this->addError(static::UPLOAD_ERR_DUPLICATE_FILE);
  355. }
  356. }
  357. }
  358. }
  359. // no need to store it as an array anymore
  360. $this->container['filename'] = implode('', $filename);
  361. // does the filename exceed the maximum length?
  362. if ( ! empty($this->config['max_length']) and strlen($this->container['filename']) > $this->config['max_length'])
  363. {
  364. $this->addError(static::UPLOAD_ERR_MAX_FILENAME_LENGTH);
  365. }
  366. // update the status of this validation
  367. $this->isValid = empty($this->errors);
  368. // if the file is still valid, run the before save callbacks
  369. if ($this->isValid)
  370. {
  371. // validation starts, call the pre-save callbacks
  372. $this->runCallbacks('before_save');
  373. // recheck the path, it might have been altered by a callback
  374. if ($this->isValid and ! is_dir($this->container['path']) and (bool) $this->config['create_path'])
  375. {
  376. @mkdir($this->container['path'], $this->config['path_chmod'], true);
  377. if ( ! is_dir($this->container['path']))
  378. {
  379. $this->addError(static::UPLOAD_ERR_MKDIR_FAILED);
  380. }
  381. }
  382. // update the status of this validation
  383. $this->isValid = empty($this->errors);
  384. }
  385. // if the file is still valid, move it
  386. if ($this->isValid)
  387. {
  388. // check if file should be moved to an ftp server
  389. if ($this->config['moveCallback'])
  390. {
  391. $moved = call_user_func($this->config['moveCallback'], $this->container['tmp_name'], $this->container['path'].$this->container['filename']);
  392. if ( ! $moved)
  393. {
  394. $this->addError(static::UPLOAD_ERR_EXTERNAL_MOVE_FAILED);
  395. }
  396. }
  397. else
  398. {
  399. if( ! @move_uploaded_file($this->container['tmp_name'], $this->container['path'].$this->container['filename']))
  400. {
  401. $this->addError(static::UPLOAD_ERR_MOVE_FAILED);
  402. }
  403. else
  404. {
  405. @chmod($this->container['path'].$this->container['filename'], $this->config['file_chmod']);
  406. }
  407. }
  408. }
  409. else
  410. {
  411. // remove the temporary file we've created, make sure it exists first though!
  412. if (file_exists($this->container['path'].$this->container['filename']))
  413. {
  414. unlink($this->container['path'].$this->container['filename']);
  415. }
  416. }
  417. // validation starts, call the post-save callback
  418. if ($this->isValid)
  419. {
  420. $this->runCallbacks('after_save');
  421. }
  422. }
  423. // return the status of this operation
  424. return empty($this->errors);
  425. }
  426. /**
  427. * Run callbacks of he defined type
  428. *
  429. * @param callable $type Valid callable callback
  430. *
  431. * @return void
  432. */
  433. protected function runCallbacks($type)
  434. {
  435. // make sure we have callbacks of this type
  436. if (array_key_exists($type, $this->callbacks))
  437. {
  438. // run the defined callbacks
  439. foreach ($this->callbacks[$type] as $callback)
  440. {
  441. // check if the callback is valid
  442. if (is_callable($callback))
  443. {
  444. // call the defined callback
  445. $result = call_user_func($callback, $this);
  446. // and process the results. we need FileError instances only
  447. foreach ((array) $result as $entry)
  448. {
  449. if (is_object($entry) and $entry instanceOf FileError)
  450. {
  451. $this->errors[] = $entry;
  452. }
  453. }
  454. // update the status of this validation
  455. $this->isValid = empty($this->errors);
  456. }
  457. }
  458. }
  459. }
  460. /**
  461. * Convert a filename into a normalized name. only outputs 7 bit ASCII characters.
  462. *
  463. * @return void
  464. */
  465. protected function normalize()
  466. {
  467. // Decode all entities to their simpler forms
  468. $this->container['filename'] = html_entity_decode($this->container['filename'], ENT_QUOTES, 'UTF-8');
  469. // Remove all quotes
  470. $this->container['filename'] = preg_replace("#[\"\']#", '', $this->container['filename']);
  471. // Strip unwanted characters
  472. $this->container['filename'] = preg_replace("#[^a-z0-9]#i", $this->config['normalize_separator'], $this->container['filename']);
  473. $this->container['filename'] = preg_replace("#[/_|+ -]+#u", $this->config['normalize_separator'], $this->container['filename']);
  474. $this->container['filename'] = trim($this->container['filename'], $this->config['normalize_separator']);
  475. }
  476. /**
  477. * Add a new error object to the list
  478. *
  479. * @param int $error uploaded file number
  480. *
  481. * @return void
  482. */
  483. protected function addError($error)
  484. {
  485. $this->errors[] = new FileError($error, $this->config['langCallback']);
  486. }
  487. //------------------------------------------------------------------------------------------------------------------
  488. /**
  489. * Countable methods
  490. */
  491. public function count()
  492. {
  493. return count($this->container);
  494. }
  495. /**
  496. * ArrayAccess methods
  497. */
  498. public function offsetExists($offset)
  499. {
  500. return isset($this->container[$offset]);
  501. }
  502. public function offsetGet($offset)
  503. {
  504. return $this->container[$offset];
  505. }
  506. public function offsetSet($offset, $value)
  507. {
  508. $this->container[$offset] = $value;
  509. }
  510. public function offsetUnset($offset)
  511. {
  512. throw new \OutOfBoundsException('You can not unset a data element of an Upload File instance');
  513. }
  514. /**
  515. * Iterator methods
  516. */
  517. function rewind()
  518. {
  519. return reset($this->container);
  520. }
  521. function current()
  522. {
  523. return current($this->container);
  524. }
  525. function key()
  526. {
  527. return key($this->container);
  528. }
  529. function next()
  530. {
  531. return next($this->container);
  532. }
  533. function valid()
  534. {
  535. return key($this->container) !== null;
  536. }
  537. }