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

/wire/core/WireUpload.php

https://bitbucket.org/webbear/processwire-base-installation
PHP | 409 lines | 282 code | 93 blank | 34 comment | 85 complexity | 476982a80de04f555a9653f4c5b82567 MD5 | raw file
  1. <?php
  2. /**
  3. * ProcessWire WireUpload
  4. *
  5. * Saves uploads of single or multiple files, saving them to the destination path.
  6. * If the destination path does not exist, it will be created.
  7. *
  8. * ProcessWire 2.x
  9. * Copyright (C) 2013 by Ryan Cramer
  10. * Licensed under GNU/GPL v2, see LICENSE.TXT
  11. *
  12. * http://processwire.com
  13. *
  14. */
  15. class WireUpload extends Wire {
  16. protected $name;
  17. protected $destinationPath;
  18. protected $maxFiles;
  19. protected $maxFileSize = 0;
  20. protected $completedFilenames = array();
  21. protected $overwrite;
  22. protected $overwriteFilename = ''; // if specified, only this filename may be overwritten
  23. protected $lowercase = true;
  24. protected $targetFilename = '';
  25. protected $extractArchives = false;
  26. protected $validExtensions = array();
  27. protected $badExtensions = array('php', 'php3', 'phtml', 'exe', 'cfm', 'shtml', 'asp', 'pl', 'cgi', 'sh');
  28. protected $errors = array();
  29. protected $allowAjax = false;
  30. static protected $unzipCommand = 'unzip -j -qq -n /src/ -x __MACOSX .* -d /dst/';
  31. protected $errorInfo = array();
  32. public function __construct($name) {
  33. $this->errorInfo = array(
  34. UPLOAD_ERR_OK => $this->_('Successful Upload'),
  35. UPLOAD_ERR_INI_SIZE => $this->_('The uploaded file exceeds the upload_max_filesize directive in php.ini.'),
  36. UPLOAD_ERR_FORM_SIZE => $this->_('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.'),
  37. UPLOAD_ERR_PARTIAL => $this->_('The uploaded file was only partially uploaded.'),
  38. UPLOAD_ERR_NO_FILE => $this->_('No file was uploaded.'),
  39. UPLOAD_ERR_NO_TMP_DIR => $this->_('Missing a temporary folder.'),
  40. UPLOAD_ERR_CANT_WRITE => $this->_('Failed to write file to disk.'),
  41. UPLOAD_ERR_EXTENSION => $this->_('File upload stopped by extension.')
  42. );
  43. $this->setName($name);
  44. $this->maxFiles = 0; // no limit
  45. $this->overwrite = false;
  46. $this->destinationPath = '';
  47. if($this->config->uploadBadExtensions) {
  48. $badExtensions = $this->config->uploadBadExtensions;
  49. if(is_string($badExtensions) && $badExtensions) $badExtensions = explode(' ', $badExtensions);
  50. if(is_array($badExtensions)) $this->badExtensions = $badExtensions;
  51. }
  52. if($this->config->uploadUnzipCommand) {
  53. self::setUnzipCommand($this->config->uploadUnzipCommand);
  54. }
  55. }
  56. public function execute() {
  57. // returns array of files (multi file upload)
  58. if(!$this->name) throw new WireException("You must set the name for WireUpload before executing it");
  59. if(!$this->destinationPath) throw new WireException("You must set the destination path for WireUpload before executing it");
  60. $files = array();
  61. $f = $this->getPhpFiles();
  62. if(!$f) return $files;
  63. if(is_array($f['name'])) {
  64. // multi file upload
  65. $cnt = 0;
  66. foreach($f['name'] as $key => $name) {
  67. if($this->maxFiles && ($cnt >= $this->maxFiles)) {
  68. $this->error($this->_('Max file upload limit reached'));
  69. break;
  70. }
  71. if(!$this->isValidUpload($f['name'][$key], $f['size'][$key], $f['error'][$key])) continue;
  72. if(!$this->saveUpload($f['tmp_name'][$key], $f['name'][$key])) continue;
  73. $cnt++;
  74. }
  75. $files = $this->completedFilenames;
  76. } else {
  77. // single file upload, including ajax
  78. if($this->isValidUpload($f['name'], $f['size'], $f['error'])) {
  79. $this->saveUpload($f['tmp_name'], $f['name'], !empty($f['ajax'])); // returns filename or false
  80. $files = $this->completedFilenames;
  81. }
  82. }
  83. return $files;
  84. }
  85. /**
  86. * Returns PHP's $_FILES or one constructed from an ajax upload
  87. *
  88. */
  89. protected function getPhpFiles() {
  90. if(isset($_SERVER['HTTP_X_FILENAME']) && $this->allowAjax) return $this->getPhpFilesAjax();
  91. if(empty($_FILES) || !count($_FILES)) return false;
  92. if(!isset($_FILES[$this->name]) || !is_array($_FILES[$this->name])) return false;
  93. return $_FILES[$this->name];
  94. }
  95. /**
  96. * Handles an ajax file upload and constructs a resulting $_FILES
  97. *
  98. */
  99. protected function getPhpFilesAjax() {
  100. if(!$filename = $_SERVER['HTTP_X_FILENAME']) return false;
  101. $dir = wire('config')->uploadTmpDir;
  102. if(!$dir || !is_writable($dir)) $dir = ini_get('upload_tmp_dir');
  103. if(!$dir || !is_writable($dir)) $dir = sys_get_temp_dir();
  104. if(!$dir || !is_writable($dir)) throw new WireException("Error writing to $dir. Please define \$config->uploadTmpDir and ensure it is writable.");
  105. $tmpName = tempnam($dir, get_class($this));
  106. file_put_contents($tmpName, file_get_contents('php://input'));
  107. $filesize = is_file($tmpName) ? filesize($tmpName) : 0;
  108. $error = $filesize ? UPLOAD_ERR_OK : UPLOAD_ERR_NO_FILE;
  109. $file = array(
  110. 'name' => $filename,
  111. 'tmp_name' => $tmpName,
  112. 'size' => $filesize,
  113. 'error' => $error,
  114. 'ajax' => true,
  115. );
  116. return $file;
  117. }
  118. protected function isValidExtension($name) {
  119. $pathInfo = pathinfo($name);
  120. $extension = strtolower($pathInfo['extension']);
  121. if(in_array($extension, $this->badExtensions)) return false;
  122. if(in_array($extension, $this->validExtensions)) return true;
  123. return false;
  124. }
  125. protected function isValidUpload($name, $size, $error) {
  126. $valid = false;
  127. if($error && $error != UPLOAD_ERR_NO_FILE) $this->error($this->errorInfo[$error]);
  128. else if(!$size) $valid = false; // no data
  129. else if(!$this->isValidExtension($name)) {
  130. $fname = $this->validateFilename($name);
  131. $this->error("$fname - " . $this->_('Invalid file extension, please use one of:') . ' ' . implode(', ', $this->validExtensions));
  132. } else if($this->maxFileSize > 0 && $size > $this->maxFileSize) {
  133. $fname = $this->validateFilename($name);
  134. $this->error("$fname - " . $this->_('Exceeds max allowed file size'));
  135. } else if($name[0] == '.') $valid = false;
  136. else $valid = true;
  137. return $valid;
  138. }
  139. protected function checkDestinationPath() {
  140. if(!is_dir($this->destinationPath)) {
  141. $this->error("Destination path does not exist {$this->destinationPath}");
  142. }
  143. return true;
  144. }
  145. protected function getUniqueFilename($destination) {
  146. $cnt = 0;
  147. $p = pathinfo($destination);
  148. $basename = basename($p['basename'], ".$p[extension]");
  149. while(file_exists($destination)) {
  150. $cnt++;
  151. $filename = "$basename-$cnt.$p[extension]";
  152. $destination = "$p[dirname]/$filename";
  153. }
  154. return $destination;
  155. }
  156. public function validateFilename($value, $extensions = array()) {
  157. $value = basename($value);
  158. if($this->lowercase) $value = strtolower($value);
  159. $value = preg_replace('/[^-a-zA-Z0-9_\.]/', '_', $value);
  160. $value = preg_replace('/__+/', '_', $value);
  161. $value = trim($value, "_");
  162. $p = pathinfo($value);
  163. $extension = strtolower($p['extension']);
  164. $basename = basename($p['basename'], ".$extension");
  165. // replace any dots in the basename with underscores
  166. $basename = trim(str_replace(".", "_", $basename), "_");
  167. $value = "$basename.$extension";
  168. if(count($extensions)) {
  169. if(!in_array($extension, $extensions)) $value = false;
  170. }
  171. return $value;
  172. }
  173. protected function saveUpload($tmp_name, $filename, $ajax = false) {
  174. if(!$this->checkDestinationPath()) return false;
  175. $filename = $this->getTargetFilename($filename);
  176. $filename = $this->validateFilename($filename);
  177. if($this->lowercase) $filename = strtolower($filename);
  178. $destination = $this->destinationPath . $filename;
  179. $p = pathinfo($destination);
  180. if(!$this->overwrite && $filename != $this->overwriteFilename) {
  181. // overwrite not allowed, so find a new name for it
  182. $destination = $this->getUniqueFilename($destination);
  183. $filename = basename($destination);
  184. }
  185. if($ajax) $success = @rename($tmp_name, $destination);
  186. else $success = move_uploaded_file($tmp_name, $destination);
  187. if(!$success) {
  188. $this->error("Unable to move uploaded file to: $destination");
  189. if(is_file($tmp_name)) @unlink($tmp_name);
  190. return false;
  191. }
  192. if($this->config->chmodFile) chmod($destination, octdec($this->config->chmodFile));
  193. if($p['extension'] == 'zip' && ($this->maxFiles == 0) && $this->extractArchives) {
  194. if($this->saveUploadZip($destination)) {
  195. if(count($this->completedFilenames) == 1) return $this->completedFilenames[0];
  196. }
  197. return $this->completedFilenames;
  198. } else {
  199. $this->completedFilenames[] = $filename;
  200. return $filename;
  201. }
  202. }
  203. protected function saveUploadZip($zipFile) {
  204. // unzip with command line utility
  205. $files = array();
  206. if(!self::$unzipCommand) return false;
  207. $dir = dirname($zipFile) . '/';
  208. $tmpDir = $dir . '.zip_tmp/';
  209. if(!mkdir($tmpDir)) return $files;
  210. $unzipCommand = self::$unzipCommand;
  211. $unzipCommand = str_replace('/src/', escapeshellarg($zipFile), $unzipCommand);
  212. $unzipCommand = str_replace('/dst/', $tmpDir, $unzipCommand);
  213. $str = exec($unzipCommand);
  214. $files = new DirectoryIterator($tmpDir);
  215. $cnt = 0;
  216. foreach($files as $file) {
  217. if($file->isDot() || $file->isDir()) continue;
  218. if(!$this->isValidUpload($file->getFilename(), $file->getSize(), UPLOAD_ERR_OK)) {
  219. unlink($file->getPathname());
  220. continue;
  221. }
  222. //$destination = $dir . $file->getFilename();
  223. $basename = $file->getFilename();
  224. $basename = $this->validateFilename($basename, $this->validExtensions);
  225. if($basename) $destination = $this->getUniqueFilename($dir . $basename);
  226. else $destination = '';
  227. if($destination && rename($file->getPathname(), $destination)) {
  228. $this->completedFilenames[] = basename($destination);
  229. $cnt++;
  230. } else {
  231. unlink($file->getPathname());
  232. }
  233. }
  234. rmdir($tmpDir);
  235. unlink($zipFile);
  236. if(!$cnt) return false;
  237. return true;
  238. }
  239. public function getCompletedFilenames() {
  240. return $this->completedFilenames;
  241. }
  242. public function setTargetFilename($filename) {
  243. // target filename as destination
  244. // only useful for single uploads
  245. $this->targetFilename = $filename;
  246. }
  247. protected function getTargetFilename($filename) {
  248. // given a filename, takes it's extension and combines it with that
  249. // if the targetFilename (if set). Otherwise returns the filename you gave it
  250. if(!$this->targetFilename) return $filename;
  251. $pathInfo = pathinfo($filename);
  252. $targetPathInfo = pathinfo($this->targetFilename);
  253. return rtrim(basename($this->targetFilename, $targetPathInfo['extension']), ".") . "." . $pathInfo['extension'];
  254. }
  255. public function setOverwriteFilename($filename) {
  256. // only this filename may be overwritten if specified, i.e. myphoto.jpg
  257. // only useful for single uploads
  258. $this->overwrite = false; // required
  259. if($this->lowercase) $filename = strtolower($filename);
  260. $this->overwriteFilename = $filename;
  261. return $this;
  262. }
  263. static public function setUnzipCommand($unzipCommand) {
  264. if(strpos($unzipCommand, '/src/') && strpos($unzipCommand, '/dst/'))
  265. self::$unzipCommand = $unzipCommand;
  266. }
  267. static public function getUnzipCommand() {
  268. return self::$unzipCommand;
  269. }
  270. public function setValidExtensions(array $extensions) {
  271. foreach($extensions as $ext) $this->validExtensions[] = strtolower($ext);
  272. return $this;
  273. }
  274. public function setMaxFiles($maxFiles) {
  275. $this->maxFiles = (int) $maxFiles;
  276. return $this;
  277. }
  278. public function setMaxFileSize($bytes) {
  279. $this->maxFileSize = (int) $bytes;
  280. return $this;
  281. }
  282. public function setOverwrite($overwrite) {
  283. $this->overwrite = $overwrite ? true : false;
  284. return $this;
  285. }
  286. public function setDestinationPath($destinationPath) {
  287. $this->destinationPath = $destinationPath;
  288. return $this;
  289. }
  290. public function setExtractArchives($extract = true) {
  291. $this->extractArchives = $extract;
  292. $this->validExtensions[] = 'zip';
  293. return $this;
  294. }
  295. public function setName($name) {
  296. $this->name = $this->fuel('sanitizer')->fieldName($name);
  297. return $this;
  298. }
  299. public function setLowercase($lowercase = true) {
  300. $this->lowercase = $lowercase ? true : false;
  301. return $this;
  302. }
  303. public function setAllowAjax($allowAjax = true) {
  304. $this->allowAjax = $allowAjax ? true : false;
  305. return $this;
  306. }
  307. public function error($text, $flags = 0) {
  308. $this->errors[] = $text;
  309. parent::error($text, $flags);
  310. }
  311. public function getErrors($clear = false) {
  312. $errors = $this->errors;
  313. if($clear) $this->errors = array();
  314. return $errors;
  315. }
  316. }