1: <?php
  2: 
  3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17:  18:  19:  20:  21:  22:  23:  24:  25:  26:  27:  28:  29:  30:  31:  32:  33:  34:  35:  36:  37: 
 38: 
 39: class SystemFineUploadHandler
 40: {
 41: 
 42:     public $allowedExtensions = array();
 43:     public $allowedMimeTypes = array();
 44:     public $sizeLimit = null;
 45:     public $inputName = 'qqfile';
 46:     public $chunksFolder = 'chunks';
 47: 
 48:     public $chunksCleanupProbability = 0.001; 
 49:     public $chunksExpireIn = 604800; 
 50: 
 51:     protected $uploadName;
 52:     public $claims;
 53: 
 54:      55:  56:  57: 
 58:     public function __construct(\stdClass $claims)
 59:     {
 60:         $this->claims = $claims;
 61:     }
 62: 
 63:      64:  65: 
 66:     public function getName()
 67:     {
 68:         if (isset($_REQUEST['qqfilename'])) {
 69:             return $_REQUEST['qqfilename'];
 70:         }
 71: 
 72:         if (isset($_FILES[$this->inputName])) {
 73:             return $_FILES[$this->inputName]['name'];
 74:         }
 75:     }
 76: 
 77:      78:  79:  80: 
 81:     public function getUploadName()
 82:     {
 83:         return $this->uploadName;
 84:     }
 85: 
 86:      87:  88:  89:  90:  91:  92: 
 93:     public function combineChunks($uploadDirectory, $name = null)
 94:     {
 95:         $uuid = $_POST['qquuid'];
 96:         if ($name === null) {
 97:             $name = $this->getName();
 98:         }
 99:         $targetFolder = $this->chunksFolder.DIRECTORY_SEPARATOR.$uuid;
100:         $totalParts = isset($_REQUEST['qqtotalparts']) ? (int)$_REQUEST['qqtotalparts'] : 1;
101: 
102:         $targetPath = join(DIRECTORY_SEPARATOR, array($uploadDirectory, $uuid, $name));
103:         $this->uploadName = $name;
104: 
105:         if (!file_exists($targetPath)) {
106:             mkdir(dirname($targetPath), 0777, true);
107:         }
108:         $target = fopen($targetPath, 'wb');
109: 
110:         for ($i=0; $i<$totalParts; $i++) {
111:             $chunk = fopen($targetFolder.DIRECTORY_SEPARATOR.$i, "rb");
112:             stream_copy_to_stream($chunk, $target);
113:             fclose($chunk);
114:         }
115: 
116:         
117:         fclose($target);
118: 
119:         for ($i=0; $i<$totalParts; $i++) {
120:             unlink($targetFolder.DIRECTORY_SEPARATOR.$i);
121:         }
122: 
123:         rmdir($targetFolder);
124: 
125:         if (!is_null($this->sizeLimit) && filesize($targetPath) > $this->sizeLimit) {
126:             unlink($targetPath);
127:             
128:             header("HTTP/1.0 413 Request Entity Too Large");
129:             return array("success" => false, "uuid" => $uuid, "preventRetry" => true);
130:         }
131: 
132:         return array("success" => true, "uuid" => $uuid);
133:     }
134: 
135:     136: 137: 138: 139: 140: 
141:     public function handleUpload($uploadDirectory, $name = null)
142:     {
143:         if (is_writable($this->chunksFolder) &&
144:             1 == mt_rand(1, 1/$this->chunksCleanupProbability)) {
145:             
146:             $this->cleanupChunks();
147:         }
148: 
149:         
150:         
151:         if ($this->toBytes(ini_get('post_max_size')) < $this->sizeLimit ||
152:             $this->toBytes(ini_get('upload_max_filesize')) < $this->sizeLimit) {
153:             $neededRequestSize = max(1, $this->sizeLimit / 1024 / 1024) . 'M';
154:             return array(
155:                 'error'=>"Server error. Increase post_max_size and upload_max_filesize to ".$neededRequestSize
156:             );
157:         }
158: 
159:         if ($this->isInaccessible($uploadDirectory)) {
160:             return array('error' => "Server error. Uploads directory isn't writable");
161:         }
162: 
163:         $type = $_SERVER['CONTENT_TYPE'];
164:         if (isset($_SERVER['HTTP_CONTENT_TYPE'])) {
165:             $type = $_SERVER['HTTP_CONTENT_TYPE'];
166:         }
167: 
168:         if (!isset($type)) {
169:             return array('error' => "No files were uploaded.");
170:         } elseif (strpos(strtolower($type), 'multipart/') !== 0) {
171:             return array(
172:                 'error' => "Server error. Not a multipart request. Please set forceMultipart to default value (true)."
173:             );
174:         }
175: 
176:         
177:         $file = $_FILES[$this->inputName];
178:         $size = $file['size'];
179:         if (isset($_REQUEST['qqtotalfilesize'])) {
180:             $size = $_REQUEST['qqtotalfilesize'];
181:         }
182: 
183:         if ($name === null) {
184:             $name = $this->getName();
185:         }
186: 
187:         
188:         if ($file['error']) {
189:             return array('error' => 'Upload Error #'.$file['error']);
190:         }
191: 
192:         
193:         if ($name === null || $name === '') {
194:             return array('error' => 'File name empty.');
195:         }
196: 
197:         
198:         if ($size == 0) {
199:             return array('error' => 'File is empty.');
200:         }
201: 
202:         if (!is_null($this->sizeLimit) && $size > $this->sizeLimit) {
203:             return array('error' => 'File is too large.', 'preventRetry' => true);
204:         }
205: 
206:         
207:         $pathinfo = pathinfo($name);
208:         $ext = isset($pathinfo['extension']) ? strtolower($pathinfo['extension']) : '';
209: 
210:         if ($this->allowedExtensions
211:             && !in_array(strtolower($ext), array_map("strtolower", $this->allowedExtensions))) {
212:             $these = implode(', ', $this->allowedExtensions);
213:             return array(
214:                 'error' => 'File has an invalid extension, it should be one of '. $these . '.',
215:                 'preventRetry' => true
216:             );
217:         }
218: 
219:         $mimeType = '';
220:         if (!empty($this->allowedMimeTypes)) {
221:             $mimeType = mime_content_type($_FILES[$this->inputName]['tmp_name']);
222:             if (!in_array($mimeType, $this->allowedMimeTypes)) {
223:                 return array('error' => 'File is of an invalid type.', 'preventRetry' => true);
224:             }
225:         }
226: 
227:         
228:         $totalParts = isset($_REQUEST['qqtotalparts']) ? (int)$_REQUEST['qqtotalparts'] : 1;
229: 
230:         $uuid = $_REQUEST['qquuid'];
231:         if ($totalParts > 1) {
232:             
233: 
234:             $chunksFolder = $this->chunksFolder;
235:             $partIndex = (int)$_REQUEST['qqpartindex'];
236: 
237:             if (!is_writable($chunksFolder) && !is_executable($uploadDirectory)) {
238:                 return array('error' => "Server error. Chunks directory isn't writable or executable.");
239:             }
240: 
241:             $targetFolder = $this->chunksFolder.DIRECTORY_SEPARATOR.$uuid;
242: 
243:             if (!file_exists($targetFolder)) {
244:                 mkdir($targetFolder, 0775, true);
245:             }
246: 
247:             $target = $targetFolder.'/'.$partIndex;
248: 
249:             $storeResult = $this->storeUploadedFile($target, $mimeType, $uuid);
250:             if (false !== $storeResult) {
251:                 return $storeResult;
252:             }
253:         } else {
254:             
255: 
256:             $target = join(DIRECTORY_SEPARATOR, array($uploadDirectory, $uuid, $name));
257: 
258:             if ($target) {
259:                 $this->uploadName = basename($target);
260: 
261:                 $storeResult = $this->storeUploadedFile($target, $mimeType, $uuid);
262:                 if (false !== $storeResult) {
263:                     return $storeResult;
264:                 }
265:             }
266: 
267:             return array('error'=> 'Could not save uploaded file.' .
268:                 'The upload was cancelled, or server error encountered');
269:         }
270:     }
271: 
272:     protected function storeUploadedFile($target, $mimeType, $uuid)
273:     {
274:         if (!is_dir(dirname($target))) {
275:             mkdir(dirname($target), 0775, true);
276:         }
277:         if (move_uploaded_file($_FILES[$this->inputName]['tmp_name'], $target)) {
278:             return array('success'=> true, "uuid" => $uuid);
279:         }
280:         return false;
281:     }
282: 
283:     284: 285: 286: 287: 288: 
289:     public function handleDelete($uploadDirectory, $name = null)
290:     {
291:         if ($this->isInaccessible($uploadDirectory)) {
292:             return array(
293:                 'error' => "Server error. Uploads directory isn't writable"
294:                             . ((!$this->isWindows()) ? " or executable." : ".")
295:             );
296:         }
297: 
298:         $targetFolder = $uploadDirectory;
299:         $uuid = false;
300:         $method = $_SERVER["REQUEST_METHOD"];
301:         if ($method == "DELETE") {
302:             $url = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
303:             $tokens = explode('/', $url);
304:             $uuid = $tokens[sizeof($tokens)-1];
305:         } elseif ($method == "POST") {
306:             $uuid = $_REQUEST['qquuid'];
307:         } else {
308:             return array("success" => false,
309:                 "error" => "Invalid request method! ".$method
310:             );
311:         }
312: 
313:         $target = join(DIRECTORY_SEPARATOR, array($targetFolder, $uuid));
314: 
315:         if (is_dir($target)) {
316:             $this->removeDir($target);
317:             return array("success" => true, "uuid" => $uuid);
318:         } else {
319:             return array("success" => false,
320:                 "error" => "File not found! Unable to delete.".$url,
321:                 "path" => $uuid
322:             );
323:         }
324:     }
325: 
326:     327: 328: 329: 330: 331: 332: 333: 
334:     protected function getUniqueTargetPath($uploadDirectory, $filename)
335:     {
336:         
337:         
338:         
339: 
340:         if (function_exists('sem_acquire')) {
341:             $lock = sem_get(ftok(__FILE__, 'u'));
342:             sem_acquire($lock);
343:         }
344: 
345:         $pathinfo = pathinfo($filename);
346:         $base = $pathinfo['filename'];
347:         $ext = isset($pathinfo['extension']) ? $pathinfo['extension'] : '';
348:         $ext = $ext == '' ? $ext : '.' . $ext;
349: 
350:         $unique = $base;
351:         $suffix = 0;
352: 
353:         
354: 
355:         while (file_exists($uploadDirectory . DIRECTORY_SEPARATOR . $unique . $ext)) {
356:             $suffix += rand(1, 999);
357:             $unique = $base.'-'.$suffix;
358:         }
359: 
360:         $result =  $uploadDirectory . DIRECTORY_SEPARATOR . $unique . $ext;
361: 
362:         
363:         if (!touch($result)) {
364:             
365:             $result = false;
366:         }
367: 
368:         if (function_exists('sem_acquire')) {
369:             sem_release($lock);
370:         }
371: 
372:         return $result;
373:     }
374: 
375:     376: 377: 378: 379: 380: 
381:     protected function cleanupChunks()
382:     {
383:         foreach (scandir($this->chunksFolder) as $item) {
384:             if ($item == "." || $item == "..") {
385:                 continue;
386:             }
387: 
388:             $path = $this->chunksFolder.DIRECTORY_SEPARATOR.$item;
389: 
390:             if (!is_dir($path)) {
391:                 continue;
392:             }
393: 
394:             if (time() - filemtime($path) > $this->chunksExpireIn) {
395:                 $this->removeDir($path);
396:             }
397:         }
398:     }
399: 
400:     401: 402: 403: 404: 
405:     protected function removeDir($dir)
406:     {
407:         foreach (scandir($dir) as $item) {
408:             if ($item == "." || $item == "..") {
409:                 continue;
410:             }
411: 
412:             if (is_dir($item)) {
413:                 $this->removeDir($item);
414:             } else {
415:                 unlink(join(DIRECTORY_SEPARATOR, array($dir, $item)));
416:             }
417:         }
418:         rmdir($dir);
419:     }
420: 
421:     422: 423: 424: 425: 
426:     protected function toBytes($str)
427:     {
428:         $val = trim($str);
429:         $last = strtolower($str[strlen($str)-1]);
430:         switch ($last) {
431:             case 'g':
432:                 $val *= 1024; 
433:             case 'm':
434:                 $val *= 1024; 
435:             case 'k':
436:                 $val *= 1024; 
437:         }
438:         return $val;
439:     }
440: 
441:     442: 443: 444: 445: 446: 447: 448: 449: 450: 451: 452: 
453:     protected function isInaccessible($directory)
454:     {
455:         $isWin = $this->isWindows();
456:         $folderInaccessible =
457:             ($isWin) ? !is_writable($directory) : (!is_writable($directory) && !is_executable($directory));
458:         return $folderInaccessible;
459:     }
460: 
461:     462: 463: 464: 465: 
466: 
467:     protected function isWindows()
468:     {
469:         $isWin = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN');
470:         return $isWin;
471:     }
472: }
473: