PageRenderTime 46ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/application/libraries/Uploader/File.php

http://github.com/tcm-project/tangocms
PHP | 307 lines | 173 code | 23 blank | 111 comment | 46 complexity | 7b1bd1bf8a4ad7965930b4d568ecc8d5 MD5 | raw file
Possible License(s): LGPL-2.1
  1. <?php
  2. /**
  3. * Zula Framework Uploader_File
  4. * --- Provides an OOP interface to get details about the uploaded
  5. * file, and methods to actually upload/move the file.
  6. *
  7. * @patches submit all patches to patches@tangocms.org
  8. *
  9. * @author Alex Cartwright
  10. * @copyright Copyright (C) 2008, 2009, 2010 Alex Cartwright
  11. * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU/LGPL 2.1
  12. * @package Zula_Uploader
  13. */
  14. class Uploader_File extends Zula_LibraryBase {
  15. /**
  16. * Details about the uploaded file (very similar to
  17. * that of the $_FILES array
  18. * @var array
  19. */
  20. protected $fileDetails = array();
  21. /**
  22. * Uploader object, to get details from
  23. * @var object
  24. */
  25. protected $uploader = null;
  26. /**
  27. * Stores common errors that are used
  28. * @var array
  29. */
  30. protected $errorMsg = array(
  31. 'partial' => 'requested file "%s" was only partially uploaded',
  32. 'no_tmp_dir' => 'no temp directory for file "%s" to be uploaded to',
  33. 'cant_write' => 'failed to write file "%s" to disk',
  34. 'extension' => 'PHP extension blocked file upload for "%s"',
  35. 'mime' => 'requested file "%s" has invalid mime of "%s"',
  36. 'file_ext' => 'requested file "%s" has an invalid file extension',
  37. );
  38. /**
  39. * Constructor
  40. * Takes the file details and main Uploader config
  41. *
  42. * @param array $fileDetails
  43. * @param object $uploader
  44. * @return object
  45. */
  46. public function __construct( array $fileDetails, Uploader $uploader ) {
  47. $this->fileDetails[] = $fileDetails;
  48. $this->uploader = $uploader;
  49. }
  50. /**
  51. * Quick access to details of the first file, such as file size
  52. * mime type, path etc.
  53. *
  54. * @param string $name
  55. * @return mixed
  56. */
  57. public function __get( $name ) {
  58. if ( $name == 'type' || $name == 'mime_type' ) {
  59. $name = 'mime';
  60. } else if ( $name == 'tmpName' ) {
  61. $name = 'tmp_name';
  62. }
  63. return isset($this->fileDetails[0][$name]) ? $this->fileDetails[0][ $name ] : parent::__get( $name );
  64. }
  65. /**
  66. * Returns details about uploaded files. Multiple details will
  67. * only exist if an archive was uploaded and was able to extract.
  68. *
  69. * bool false will only be returned if multiple details and
  70. * we have reached the end of all extracted files, or if the
  71. * provided index does not exist.
  72. *
  73. * @param int $index
  74. * @return array|bool
  75. */
  76. public function getDetails( $index=null ) {
  77. static $pointer = 0;
  78. if ( $index === null ) {
  79. $index = $pointer++;
  80. }
  81. return isset($this->fileDetails[$index]) ? $this->fileDetails[ $index ] : false;
  82. }
  83. /**
  84. * Main method for uploading/moving the file. Various exceptions are
  85. * thrown in this method to indicate the errors that could occur
  86. * (such as the main PHP error codes).
  87. *
  88. * bool false is returned if there was no file uploaded,
  89. *
  90. * @param string $filename
  91. * @return bool
  92. */
  93. public function upload( $filename=null ) {
  94. switch( $this->error ) {
  95. case UPLOAD_ERR_INI_SIZE:
  96. throw new Uploader_MaxFileSize( zula_byte_value( ini_get('upload_max_filesize') ) );
  97. case UPLOAD_ERR_FORM_SIZE:
  98. throw new Uploader_MaxFileSize( abs( $this->_input->post('MAX_FILE_SIZE') ) );
  99. case UPLOAD_ERR_PARTIAL:
  100. throw new Uploader_PartialUpload( sprintf( $this->errorMsg['partial'], $this->name ) );
  101. case UPLOAD_ERR_NO_FILE:
  102. return false;
  103. case UPLOAD_ERR_NO_TMP_DIR:
  104. throw new Uploader_NoTmpDir( sprintf( $this->errorMsg['no_tmp_dir'], $this->name ) );
  105. case UPLOAD_ERR_CANT_WRITE:
  106. throw new Uploader_NoWrite( sprintf( $this->errorMsg['cant_write'], $this->name ) );
  107. case UPLOAD_ERR_EXTENSION:
  108. throw new Uploader_FileBlocked( sprintf( $this->errorMsg['extension'], $this->name ) );
  109. }
  110. if ( !is_uploaded_file( $this->tmpName ) ) {
  111. $this->_log->message( 'file was not uploaded, possible malicious attack', Log::L_WARNING );
  112. throw new Uploader_Exception( 'requested file was not actually uploaded, possible malicious attack' );
  113. }
  114. /**
  115. * Find out the mime type + category of the file, then run some checks
  116. * to ensure filesize/mime type etc are correct.
  117. */
  118. $this->fileDetails[0]['mime'] = zula_get_file_mime( $this->tmpName );
  119. $this->fileDetails[0]['category'] = substr( $this->mime, 0, strpos($this->mime, '/') );
  120. if ( $this->mime == false ) {
  121. throw new Uploader_Exception( 'unable to find mime type for uploaded file' );
  122. } else if ( $this->checkFileSize( $this->size ) === false ) {
  123. throw new Uploader_MaxFileSize( $this->uploader->maxFileSize );
  124. } else if ( $this->checkMime( $this->mime ) === false ) {
  125. throw new Uploader_InvalidMime( sprintf( $this->errorMsg['mime'], $this->name, $this->mime ) );
  126. } else if ( $this->checkExtension( $this->name ) === false ) {
  127. throw new Uploader_InvalidExtension( sprintf( $this->errorMsg['file_ext'], $this->name ) );
  128. }
  129. // All is ok, attempt to move/extract the uploaded file
  130. if ( $this->uploader->extractArchives && $this->mime =='application/zip' ) {
  131. $uploaded = $this->handleZip( $filename );
  132. unlink( $this->tmpName );
  133. if ( $uploaded !== true ) {
  134. throw new Uploader_Exception( 'failed to extract files from archive' );
  135. }
  136. } else if ( $this->handleNormal( $filename ) === false ) {
  137. throw new Uploader_Exception( 'failed to move file "'.$this->name.'"' );
  138. }
  139. return true;
  140. }
  141. /**
  142. * Handles the uploading/moving of a file in the standard way (as in
  143. * not from extracting files in an archive
  144. *
  145. * @param string $filename
  146. * @return bool
  147. */
  148. protected function handleNormal( $filename ) {
  149. if ( ($uploadDir = $this->makeDirectory($this->category)) === false ) {
  150. throw new Uploader_Exception( 'unable to create upload directory, or not writable' );
  151. }
  152. $extension = pathinfo( $this->name, PATHINFO_EXTENSION );
  153. if ( $filename ) {
  154. $i = null;
  155. do {
  156. $path = $uploadDir.'/'.$filename.$i;
  157. if ( $extension ) {
  158. $path .= '.'.$extension;
  159. }
  160. ++$i;
  161. } while ( $this->uploader->overwrite === false && file_exists( $path ) );
  162. } else {
  163. // Generate a unique name for this file
  164. $path = $uploadDir.'/'.zula_make_unique_file( $uploadDir, $extension, false );
  165. }
  166. $oldUmask = umask( 022 );
  167. if ( move_uploaded_file( $this->tmpName, $path ) ) {
  168. $this->fileDetails[0]['path'] = $path;
  169. $this->fileDetails[0] = array_merge( $this->fileDetails[0], pathinfo($path) );
  170. $this->fileDetails[0]['fromArchive'] = false;
  171. umask( $oldUmask );
  172. return true;
  173. } else {
  174. return false;
  175. }
  176. }
  177. /**
  178. * Handles the extraction of files in a ZIP archive
  179. *
  180. * @param string $filename
  181. * @return bool
  182. */
  183. protected function handleZip( $filename ) {
  184. $za = new ZipArchive;
  185. $za->open( $this->tmpName );
  186. $i = 0;
  187. $extractDir = $this->_zula->getDir( 'tmp' ).'/uploader/'.uniqid();
  188. while( $file = $za->statIndex($i) ) {
  189. $this->fileDetails[ ($i+1) ] = array(
  190. 'name' => $file['name'],
  191. 'size' => $file['size'],
  192. 'mime' => null,
  193. 'category' => null,
  194. );
  195. if ( $this->checkFileSize( $file['size'] ) && $this->checkExtension( $file['name'] ) ) {
  196. // Extract file to get the mime type, will be removed if it is not valid
  197. $za->extractTo( $extractDir, $file['name'] );
  198. $mime = zula_get_file_mime( $extractDir.'/'.$file['name'] );
  199. $this->fileDetails[ ($i+1) ]['mime'] = $mime;
  200. $this->fileDetails[ ($i+1) ]['category'] = substr( $mime, 0, strpos($mime, '/') );
  201. if ( $mime !== false && $this->checkMime( $mime ) ) {
  202. // Move the file to a uniquely named file
  203. $category = $this->fileDetails[ ($i+1) ]['category'];
  204. if ( ($uploadDir = $this->makeDirectory( $category )) === false ) {
  205. throw new Uploader_Exception( 'unable to create upload directory, or not writable' );
  206. }
  207. $this->uploader->subDirectoryName( null ); # Stop the same sub directory being used!
  208. $path = $uploadDir.'/'.zula_make_unique_file($uploadDir,
  209. pathinfo($file['name'], PATHINFO_EXTENSION),
  210. false);
  211. rename( $extractDir.'/'.$file['name'], $path );
  212. $this->fileDetails[ ($i+1) ]['path'] = $path;
  213. $this->fileDetails[ ($i+1) ] = array_merge( $this->fileDetails[($i+1)], pathinfo($path) );
  214. } else {
  215. unlink( $uploadDir.'/'.$file['name'] );
  216. }
  217. }
  218. $this->fileDetails[ ($i+1) ]['fromArchive'] = true;
  219. ++$i;
  220. }
  221. zula_full_rmdir( $extractDir );
  222. return true;
  223. }
  224. /**
  225. * Makes the correct directory for the files to be uploaded
  226. * to, a category can be provided as a token to replace.
  227. *
  228. * @param string $category
  229. * @return string|bool
  230. */
  231. protected function makeDirectory( $category=null ) {
  232. $uploadDir = str_replace( '{CATEGORY}', $category, $this->uploader->uploadDir );
  233. if ( $this->uploader->subDir === true ) {
  234. if ( !$this->uploader->subDirName ) {
  235. $this->uploader->subDirectoryName( pathinfo(zula_make_unique_dir($uploadDir), PATHINFO_BASENAME) );
  236. }
  237. $uploadDir .= '/'.$this->uploader->subDirName;
  238. }
  239. return (zula_make_dir( $uploadDir ) && is_writable( $uploadDir )) ? $uploadDir : false;
  240. }
  241. /**
  242. * Checks the file size of the file with that
  243. * of the maximum allowed
  244. *
  245. * @param int $size
  246. * @return bool
  247. */
  248. protected function checkFileSize( $size ) {
  249. return $this->uploader->maxFileSize == 0 || abs($size) <= $this->uploader->maxFileSize;
  250. }
  251. /**
  252. * Checks if the files mime type is of an allowed mime
  253. * type listed. If no allowed mimes are set, then it
  254. * will just return true.
  255. *
  256. * @param string $mime
  257. * @return bool
  258. */
  259. protected function checkMime( $mime ) {
  260. $mime = trim( $mime );
  261. $allowedMime = array_unique( $this->uploader->allowedMime );
  262. if ( empty( $mime ) || empty( $allowedMime ) ) {
  263. return true;
  264. } else {
  265. foreach( $allowedMime as $allowed ) {
  266. if ( $allowed == $mime ) {
  267. return true;
  268. }
  269. }
  270. return false;
  271. }
  272. }
  273. /**
  274. * Checks the extension of a file to make sure the uplaoded
  275. * file is not a PHP, Perl etc script.
  276. *
  277. * @param string $name
  278. * @return bool
  279. */
  280. protected function checkExtension( $name ) {
  281. return !(bool) preg_match('#(?:php[0-9]?|pl|rb|aspx?|x?html?|exe|cgi)$#i', pathinfo($name, PATHINFO_EXTENSION));
  282. }
  283. }
  284. ?>