PageRenderTime 55ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/app/controllers/Upload.php

https://github.com/ianbogda/FileZ
PHP | 375 lines | 240 code | 47 blank | 88 comment | 39 complexity | d9d52c685400dcc6f04d4e1c9c5b9ea1 MD5 | raw file
  1. <?php
  2. define ('UPLOAD_ERR_QUOTA_EXCEEDED', 99);
  3. define('UPLOAD_ERR_VIRUS_FOUND', 100);
  4. define('UPLOAD_ERR_ANTIVIRUS', 101);
  5. define('UPLOAD_ERR_ALLOWED_EXTS', 102);
  6. /**
  7. * Controller used to upload files and monitor progression
  8. */
  9. class App_Controller_Upload extends Fz_Controller {
  10. /**
  11. * Action called when a user is uploading a file
  12. * @return string json if request is made async or html otherwise
  13. */
  14. public function startAction () {
  15. $this->secure ();
  16. fz_log ('uploading');
  17. fz_log ('uploading', FZ_LOG_DEBUG, $_FILES);
  18. $response = array (); // returned data
  19. // First of all check if file's type is allowed
  20. if ($this->extensionNotAllowed()) return $this->onFileUploadError (UPLOAD_ERR_ALLOWED_EXTS);
  21. // check if request exceed php.ini post_max_size
  22. if ($_SERVER ['CONTENT_LENGTH'] > $this->shorthandSizeToBytes (
  23. ini_get ('post_max_size'))) {
  24. fz_log ('upload error (POST request > post_max_size)', FZ_LOG_ERROR);
  25. return $this->onFileUploadError (UPLOAD_ERR_INI_SIZE);
  26. }
  27. else if ($_FILES ['file']['error'] === UPLOAD_ERR_OK) {
  28. if ($this->checkQuota ($_FILES ['file'])) // Check user quota first
  29. return $this->onFileUploadError (UPLOAD_ERR_QUOTA_EXCEEDED);
  30. $this->runAntivirus();
  31. // Still no error ? we can move the file to its final destination
  32. $file = $this->saveFile ($_POST, $_FILES ['file']);
  33. if ($file !== null) {
  34. $this->sendFileUploadedMail ($file);
  35. return $this->onFileUploadSuccess ($file);
  36. } else { // Errors happened while saving or moving the uploaded file
  37. return $this->onFileUploadError ();
  38. }
  39. } else { // Errors happened during file upload
  40. return $this->onFileUploadError ($_FILES ['file']['error']);
  41. }
  42. }
  43. /**
  44. * Action called when a * visitor * is uploading a file
  45. * @return string json if request is made async or html otherwise
  46. */
  47. public function visitorStartAction () {
  48. fz_log ('visitor uploading');
  49. fz_log ('visitor uploading', FZ_LOG_DEBUG, $_FILES);
  50. $response = array (); // returned data
  51. option('visitor',true);
  52. // First of all check if file's type is allowed
  53. if ($this->extensionNotAllowed()) return $this->onFileUploadError (UPLOAD_ERR_ALLOWED_EXTS);
  54. // check if request exceed php.ini post_max_size
  55. if ($_SERVER ['CONTENT_LENGTH'] > $this->shorthandSizeToBytes (
  56. ini_get ('post_max_size'))) {
  57. fz_log ('upload error (POST request > post_max_size)', FZ_LOG_ERROR);
  58. return $this->onFileUploadError (UPLOAD_ERR_INI_SIZE);
  59. }
  60. else if ($_FILES ['file']['error'] === UPLOAD_ERR_OK) {
  61. $this->runAntivirus();
  62. // Still no error ? we can move the file to its final destination
  63. $file = $this->saveFile ($_POST, $_FILES ['file']);
  64. if ($file !== null) {
  65. $this->sendFileUploadedMail ($file);
  66. return $this->onFileUploadSuccess ($file);
  67. } else { // Errors happened while saving or moving the uploaded file
  68. return $this->onFileUploadError ();
  69. }
  70. } else { // Errors happened during file upload
  71. return $this->onFileUploadError ($_FILES ['file']['error']);
  72. }
  73. }
  74. /**
  75. * Action called from the javascript to request file upload progress
  76. * @return string (json)
  77. */
  78. public function getProgressAction () {
  79. $this->secure ();
  80. $uploadId = params ('upload_id');
  81. if (! $uploadId)
  82. halt (HTTP_BAD_REQUEST, 'A file id must be specified');
  83. $progressMonitor = fz_config_get ('app', 'progress_monitor');
  84. $progressMonitor = new $progressMonitor ();
  85. if (! $progressMonitor->isInstalled ())
  86. halt (HTTP_NOT_IMPLEMENTED, 'Your system is not configured for'.get_class ($progressMonitor));
  87. $progress = $progressMonitor->getProgress ($uploadId);
  88. if (! is_array ($progress))
  89. halt (NOT_FOUND);
  90. return json ($progress);
  91. }
  92. /**
  93. * run the antivirus if [app] antivirus=true
  94. */
  95. private function runAntivirus () {
  96. if ( fz_config_get ('app', 'antivirus') )
  97. {
  98. // We check if the file contains a virus and must be stopped
  99. $fileFirstStep = $_FILES ['file']['tmp_name'];
  100. try {
  101. if ($this->checkVirus ($fileFirstStep))
  102. {
  103. //$fileFirstStep->delete();
  104. return $this->onFileUploadError (UPLOAD_ERR_VIRUS_FOUND);
  105. }
  106. } catch (Exception $e) {
  107. fz_log ($e, FZ_LOG_ERROR);
  108. return $this->onFileUploadError (UPLOAD_ERR_ANTIVIRUS);
  109. }
  110. }
  111. }
  112. /**
  113. * Check virus with clamscan
  114. */
  115. private function checkVirus($file) {
  116. $cmd = "clamscan -i --no-summary --remove";
  117. exec($cmd." ".$file, $output, $return_value);
  118. fz_log ('Clamscan antivirus check returns:', FZ_LOG_DEBUG,$return_value);
  119. if ($return_value === 1) {
  120. fz_log ('VIRUS FOUND file id '.$file.', antivirus message: "'.implode ($output).'"', FZ_LOG_ERROR);
  121. return 1;
  122. }
  123. if ($return_value === 2) {
  124. throw new Exception ('Antivirus reported an error.');
  125. }
  126. return 0;
  127. }
  128. /**
  129. * Create a new File object from posted values and store it into the database.
  130. *
  131. * @param array $post ~= $_POST
  132. * @param array $files ~= $_FILES
  133. * @return App_Model_File
  134. */
  135. private function saveFile ($post, $uploadedFile) {
  136. // Computing default values
  137. $comment = array_key_exists ('comment', $post) ? $post['comment'] : '';
  138. // Validating lifetime
  139. $lifetime = fz_config_get ('app', 'default_file_lifetime', 10);
  140. if (array_key_exists ('lifetime', $post) && is_numeric ($post['lifetime'])) {
  141. $lifetime = intval ($post['lifetime']);
  142. $maxLifetime = intval (fz_config_get ('app', 'max_file_lifetime', 20));
  143. if ($lifetime > $maxLifetime)
  144. $lifetime = $maxLifetime;
  145. }
  146. $availableFrom = array_key_exists ('start-from', $post) ? $post['start-from'] : null;
  147. $availableFrom = new Zend_Date ($availableFrom, Zend_Date::DATE_SHORT);
  148. $availableUntil = clone ($availableFrom);
  149. $availableUntil->add ($lifetime, Zend_Date::DAY);
  150. $user = $this->getUser ();
  151. // Storing values
  152. $file = new App_Model_File ();
  153. $file->setFileInfo ($uploadedFile);
  154. if (option('visitor')) $file->setVisitorUploader();
  155. else $file->setUploader ($user);
  156. $file->setCreatedAt (new Zend_Date ());
  157. $file->comment = substr ($comment, 0, 199);
  158. $file->setAvailableFrom ($availableFrom);
  159. $file->setAvailableUntil($availableUntil);
  160. $file->notify_uploader = isset ($post['email-notifications']);
  161. if (! empty ($post ['password']))
  162. $file->setPassword ($post ['password']);
  163. try {
  164. $file->save ();
  165. if ($file->moveUploadedFile ($uploadedFile)) {
  166. fz_log ('Saved "'.$file->file_name.'"['.$file->id.'] uploaded by '.$user);
  167. return $file;
  168. }
  169. else {
  170. $file->delete ();
  171. return null;
  172. }
  173. } catch (Exception $e) {
  174. fz_log ('Can\'t save file "'.$uploadedFile['name'].'" uploaded by '.$user, FZ_LOG_ERROR);
  175. fz_log ($e, FZ_LOG_ERROR);
  176. return null;
  177. }
  178. }
  179. /**
  180. * Notify the user by email that its file has been uploaded
  181. *
  182. * @param App_Model_File $file
  183. */
  184. private function sendFileUploadedMail (App_Model_File $file) {
  185. if (! $file->notify_uploader)
  186. return;
  187. $user = $this->getUser ();
  188. $subject = __r('[FileZ] "%file_name%" uploaded successfuly',
  189. array('file_name' => $file->file_name));
  190. $msg = __r('email_upload_success (%file_name%, %file_url%, %filez_url%, %available_from%, %available_until%)',
  191. array('file_name' => $file->file_name,
  192. 'available_from' => $file->getAvailableFrom()->toString (Zend_Date::DATE_LONG),
  193. 'available_until' => $file->getAvailableUntil()->toString (Zend_Date::DATE_LONG),
  194. 'file_url' => $file->getDownloadUrl(),
  195. 'filez_url' => fz_url_for ('/', (fz_config_get ('app', 'https') == 'always'))
  196. )
  197. );
  198. $mail = $this->createMail();
  199. $mail->setBodyText ($msg);
  200. $mail->setSubject ($subject);
  201. $mail->addTo ($user->email, $user->firstname.' '.$user->lastname);
  202. try {
  203. $mail->send ();
  204. }
  205. catch (Exception $e) {
  206. fz_log ('Can\'t send email "File Uploaded" : '.$e, FZ_LOG_ERROR);
  207. }
  208. }
  209. /**
  210. * Transform a size in the shorthand format ('K', 'M', 'G') to bytes
  211. *
  212. * @param string $size
  213. * @return integer
  214. */
  215. private function shorthandSizeToBytes ($size) {
  216. $size = str_replace (' ', '', $size);
  217. switch(strtolower($size[strlen($size)-1])) {
  218. case 'g': $size *= 1024;
  219. case 'm': $size *= 1024;
  220. case 'k': $size *= 1024;
  221. }
  222. return floatval ($size);
  223. }
  224. /**
  225. * Check if the user will exceed its quota if if he upload the file $file
  226. *
  227. * @param array $file File element from $_FILES
  228. * @return boolean true if he will exceed, false else
  229. */
  230. private function checkQuota ($file) {
  231. $fileSize = $_FILES['file']['size'];
  232. $freeSpace = Fz_Db::getTable('File')->getRemainingSpaceForUser ($this->getUser());
  233. return ($fileSize > $freeSpace);
  234. }
  235. /**
  236. * Return data to the browser with the correct response type (json or html).
  237. * If the request comes from an iframe (with the is-async GET parameter,
  238. * the response is embedded inside a textarea to prevent some browsers :
  239. * quirks (http://www.malsup.com/jquery/form/#file-upload) JQuery Form
  240. * Plugin will handle the response transparently.
  241. *
  242. * @param array $data
  243. */
  244. private function returnData ($data) {
  245. if (array_key_exists ('is-async', $_GET) && $_GET ['is-async']) {
  246. return html("<textarea>\n".json_encode ($data)."\n</textarea>",'');
  247. }
  248. else {
  249. flash ('notification', $data ['statusText']);
  250. redirect_to ('/');
  251. }
  252. }
  253. /**
  254. * Function called on file upload success, a default message is returned
  255. * to the user.
  256. *
  257. * @param App_Model_File $file
  258. */
  259. private function onFileUploadSuccess (App_Model_File $file) {
  260. $user = $this->getUser();
  261. $response ['status'] = 'success';
  262. $response ['statusText'] = __('The file was successfuly uploaded');
  263. $response ['html'] = partial ('main/_file_row.php', array ('file' => $file));
  264. $response ['disk_usage'] = bytesToShorthand (max (0,
  265. Fz_Db::getTable('File')->getTotalDiskSpaceByUser ($user)));
  266. return $this->returnData ($response);
  267. }
  268. /**
  269. * Function called to check if file's type is allowed or not to be downloaded
  270. *
  271. */
  272. private function extensionNotAllowed() {
  273. $allowed_exts = ( fz_config_get ('app', 'allowed_extensions') ) ? fz_config_get ('app', 'allowed_extensions') : '';
  274. // No extension restriction
  275. if ('' === $allowed_exts) return false;
  276. // Extension restriction
  277. // Check extension
  278. $allowed_exts = explode(',', $allowed_exts);
  279. $extension = end(explode('.', $_FILES['file']['name']));
  280. // TODO : add chack with mime-type
  281. if (in_array($extension, $allowed_exts)) return false;
  282. return true;
  283. }
  284. /**
  285. * Function called on file upload error. A message corresponding to the error
  286. * code passed as parameter is return to the user. Error codes come from
  287. * $_FILES['userfile']['error'] plus a custom error code called
  288. * 'UPLOAD_ERR_QUOTA_EXCEEDED'
  289. *
  290. * @param integer $errorCode
  291. */
  292. private function onFileUploadError ($errorCode = null) {
  293. $response ['status'] = 'error';
  294. $response ['statusText'] = __('An error occured while uploading the file.').' ';
  295. if ($errorCode === null)
  296. return $this->returnData ($response);
  297. switch ($errorCode) {
  298. case UPLOAD_ERR_NO_TMP_DIR:
  299. fz_log ('upload error (Missing a temporary folder)', FZ_LOG_ERROR);
  300. break;
  301. case UPLOAD_ERR_CANT_WRITE:
  302. fz_log ('upload error (Failed to write file to disk)', FZ_LOG_ERROR);
  303. break;
  304. // These errors come from the client side, let him know what's wrong
  305. case UPLOAD_ERR_INI_SIZE:
  306. case UPLOAD_ERR_FORM_SIZE:
  307. $response ['statusText'] .=
  308. __('The uploaded file exceeds the max file size.')
  309. .' : ('.ini_get ('upload_max_filesize').')';
  310. break;
  311. case UPLOAD_ERR_PARTIAL:
  312. $response ['statusText'] .=
  313. __('The uploaded file was only partially uploaded.');
  314. break;
  315. case UPLOAD_ERR_NO_FILE:
  316. $response ['statusText'] .=
  317. __('No file was uploaded.');
  318. break;
  319. case UPLOAD_ERR_QUOTA_EXCEEDED:
  320. $response ['statusText'] .= __r('You exceeded your disk space quota (%space%).',
  321. array ('space' => fz_config_get ('app', 'user_quota')));
  322. case UPLOAD_ERR_ALLOWED_EXTS:
  323. $response ['statusText'] .= __r('The file is not allowed to be uploaded. Note that files allowed need to be %allowed_exts%.',
  324. array ('allowed_exts' => fz_config_get ('app', 'allowed_exts')));
  325. }
  326. return $this->returnData ($response);
  327. }
  328. }