AudithSoftworks /
Basis
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php namespace App\Services; |
||
| 2 | |||
| 3 | use App\Events\Files\Uploaded; |
||
| 4 | use App\Exceptions\FileStream as FileStreamExceptions; |
||
| 5 | use App\Models\File; |
||
| 6 | use Illuminate\Http\Request; |
||
| 7 | use Symfony\Component\HttpFoundation\File\UploadedFile; |
||
| 8 | |||
| 9 | class FileStream |
||
| 10 | { |
||
| 11 | /** |
||
| 12 | * @var \Illuminate\Contracts\Filesystem\Filesystem |
||
| 13 | */ |
||
| 14 | public $filesystem; |
||
| 15 | |||
| 16 | /** |
||
| 17 | * Folder to hold uploaded chunks. |
||
| 18 | * |
||
| 19 | * @var string |
||
| 20 | */ |
||
| 21 | public $temporaryChunksFolder; |
||
| 22 | |||
| 23 | /** |
||
| 24 | * Chunks will be cleaned once in 1000 requests on average. |
||
| 25 | * |
||
| 26 | * @var float |
||
| 27 | */ |
||
| 28 | public $chunksCleanupProbability = 0.001; |
||
| 29 | |||
| 30 | /** |
||
| 31 | * By default, chunks are considered loose and deletable, in 1 week. |
||
| 32 | * |
||
| 33 | * @var int |
||
| 34 | */ |
||
| 35 | public $chunksExpireIn; |
||
| 36 | |||
| 37 | /** |
||
| 38 | * Upload size limit. |
||
| 39 | * |
||
| 40 | * @var int |
||
| 41 | */ |
||
| 42 | public $sizeLimit; |
||
| 43 | |||
| 44 | public function __construct() |
||
| 45 | { |
||
| 46 | $this->filesystem = app('filesystem')->disk(); |
||
| 47 | $this->chunksExpireIn = config('filesystems.disks.local.chunks_expire_in'); |
||
| 48 | $this->temporaryChunksFolder = DIRECTORY_SEPARATOR . '_chunks'; |
||
| 49 | if (app('config')->has('filesystems.chunks_ttl') && is_int(config('filesystems.chunks_ttl'))) { |
||
| 50 | $this->chunksExpireIn = config('filesystems.chunks_ttl'); |
||
| 51 | } |
||
| 52 | if (app('config')->has('filesystems.size_limit') && is_int(config('filesystems.size_limit'))) { |
||
| 53 | $this->sizeLimit = config('filesystems.size_limit'); |
||
| 54 | } |
||
| 55 | } |
||
| 56 | |||
| 57 | /** |
||
| 58 | * Write the uploaded file to the local filesystem. |
||
| 59 | * |
||
| 60 | * @param \Illuminate\Http\Request $request |
||
| 61 | * |
||
| 62 | * @return \Illuminate\Http\JsonResponse |
||
| 63 | */ |
||
| 64 | public function handleUpload(Request $request) |
||
| 65 | { |
||
| 66 | $fineUploaderUuid = null; |
||
| 67 | if ($request->has('qquuid')) { |
||
| 68 | $fineUploaderUuid = $request->get('qquuid'); |
||
| 69 | } |
||
| 70 | |||
| 71 | //------------------------------ |
||
| 72 | // Is it Post-processing? |
||
| 73 | //------------------------------ |
||
| 74 | |||
| 75 | if ($request->has('post-process') && $request->get('post-process') == 1) { |
||
| 76 | # Combine chunks. |
||
| 77 | $this->combineChunks($request); |
||
| 78 | |||
| 79 | return collect(event(new Uploaded($fineUploaderUuid, $request)))->last(); // Return the result of the second event listener. |
||
| 80 | } |
||
| 81 | |||
| 82 | //---------------- |
||
| 83 | // Prelim work. |
||
| 84 | //---------------- |
||
| 85 | |||
| 86 | if (!file_exists($this->temporaryChunksFolder) || !is_dir($this->temporaryChunksFolder)) { |
||
| 87 | $this->filesystem->makeDirectory($this->temporaryChunksFolder); |
||
| 88 | } |
||
| 89 | |||
| 90 | # Temp folder writable? |
||
| 91 | if (!is_writable($absolutePathToTemporaryChunksFolder = config('filesystems.disks.local.root') . $this->temporaryChunksFolder) || !is_executable($absolutePathToTemporaryChunksFolder)) { |
||
| 92 | throw new FileStreamExceptions\TemporaryUploadFolderNotWritableException; |
||
| 93 | } |
||
| 94 | |||
| 95 | # Cleanup chunks. |
||
| 96 | if (1 === mt_rand(1, 1 / $this->chunksCleanupProbability)) { |
||
| 97 | $this->cleanupChunks(); |
||
| 98 | } |
||
| 99 | |||
| 100 | # Check upload size against the size-limit, if any. |
||
| 101 | if (!empty($this->sizeLimit)) { |
||
| 102 | $uploadIsTooLarge = false; |
||
| 103 | $request->has('qqtotalfilesize') && intval($request->get('qqtotalfilesize')) > $this->sizeLimit && $uploadIsTooLarge = true; |
||
| 104 | $this->filesizeFromHumanReadableToBytes(ini_get('post_max_size')) < $this->sizeLimit && $uploadIsTooLarge = true; |
||
| 105 | $this->filesizeFromHumanReadableToBytes(ini_get('upload_max_filesize')) < $this->sizeLimit && $uploadIsTooLarge = true; |
||
| 106 | if ($uploadIsTooLarge) { |
||
| 107 | throw new FileStreamExceptions\UploadTooLargeException; |
||
| 108 | } |
||
| 109 | } |
||
| 110 | |||
| 111 | # Is there attempt for multiple file uploads? |
||
| 112 | $collectionOfUploadedFiles = collect($request->file()); |
||
| 113 | if ($collectionOfUploadedFiles->count() > 1) { |
||
| 114 | throw new FileStreamExceptions\MultipleSimultaneousUploadsNotAllowedException; |
||
| 115 | } |
||
| 116 | |||
| 117 | /** @var UploadedFile $file */ |
||
| 118 | $file = $collectionOfUploadedFiles->first(); |
||
| 119 | |||
| 120 | //-------------------- |
||
| 121 | // Upload handling. |
||
| 122 | //-------------------- |
||
| 123 | |||
| 124 | if ($file->getSize() == 0) { |
||
| 125 | throw new FileStreamExceptions\UploadIsEmptyException; |
||
| 126 | } |
||
| 127 | |||
| 128 | $name = $file->getClientOriginalName(); |
||
| 129 | if ($request->has('qqfilename')) { |
||
| 130 | $name = $request->get('qqfilename'); |
||
| 131 | } |
||
| 132 | if (empty($name)) { |
||
| 133 | throw new FileStreamExceptions\UploadFilenameIsEmptyException; |
||
| 134 | } |
||
| 135 | |||
| 136 | $totalNumberOfChunks = $request->has('qqtotalparts') ? $request->get('qqtotalparts') : 1; |
||
| 137 | |||
| 138 | if ($totalNumberOfChunks > 1) { |
||
| 139 | $chunkIndex = intval($request->get('qqpartindex')); |
||
| 140 | $targetFolder = $this->temporaryChunksFolder . DIRECTORY_SEPARATOR . $fineUploaderUuid; |
||
| 141 | if (!$this->filesystem->exists($targetFolder)) { |
||
| 142 | $this->filesystem->makeDirectory($targetFolder); |
||
| 143 | } |
||
| 144 | |||
| 145 | if (!$file->isValid()) { |
||
| 146 | throw new FileStreamExceptions\UploadAttemptFailedException; |
||
| 147 | } |
||
| 148 | $file->move(storage_path('app' . $targetFolder), $chunkIndex); |
||
| 149 | |||
| 150 | return response()->json(['success' => true, 'uuid' => $fineUploaderUuid]); |
||
| 151 | } else { |
||
| 152 | if (!$file->isValid()) { |
||
| 153 | throw new FileStreamExceptions\UploadAttemptFailedException; |
||
| 154 | } |
||
| 155 | $file->move(storage_path('app'), $fineUploaderUuid); |
||
| 156 | |||
| 157 | return collect(event(new Uploaded($fineUploaderUuid, $request)))->last(); // Return the result of the second event listener. |
||
| 158 | } |
||
| 159 | } |
||
| 160 | |||
| 161 | /** |
||
| 162 | * @param \Illuminate\Http\Request $request |
||
| 163 | * |
||
| 164 | * @return bool |
||
| 165 | */ |
||
| 166 | public function isUploadResumable(Request $request) |
||
| 167 | { |
||
| 168 | $fineUploaderUuid = $request->get('qquuid'); |
||
| 169 | $chunkIndex = intval($request->get('qqpartindex')); |
||
| 170 | $numberOfExistingChunks = count($this->filesystem->files($this->temporaryChunksFolder . DIRECTORY_SEPARATOR . $fineUploaderUuid)); |
||
| 171 | if ($numberOfExistingChunks < $chunkIndex) { |
||
| 172 | throw new FileStreamExceptions\UploadIncompleteException; |
||
| 173 | } |
||
| 174 | |||
| 175 | return true; |
||
| 176 | } |
||
| 177 | |||
| 178 | /** |
||
| 179 | * @param string $size |
||
| 180 | * |
||
| 181 | * @return false|string |
||
| 182 | */ |
||
| 183 | public function filesizeFromHumanReadableToBytes($size) |
||
| 184 | { |
||
| 185 | if (preg_match('/^([\d,.]+)\s?([kmgtpezy]?i?b)$/i', $size, $matches) !== 1) { |
||
| 186 | return false; |
||
| 187 | } |
||
| 188 | $coefficient = $matches[1]; |
||
| 189 | $prefix = strtolower($matches[2]); |
||
| 190 | |||
| 191 | $binaryPrefices = ['b', 'kib', 'mib', 'gib', 'tib', 'pib', 'eib', 'zib', 'yib']; |
||
| 192 | $decimalPrefices = ['b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb']; |
||
| 193 | |||
| 194 | $base = in_array($prefix, $binaryPrefices) ? 1024 : 1000; |
||
| 195 | $flippedPrefixMap = $base == 1024 ? array_flip($binaryPrefices) : array_flip($decimalPrefices); |
||
| 196 | $factor = array_pull($flippedPrefixMap, $prefix); |
||
| 197 | |||
| 198 | return sprintf("%d", bcmul(str_replace(',', '', $coefficient), bcpow($base, $factor))); |
||
| 199 | } |
||
| 200 | |||
| 201 | /** |
||
| 202 | * @param int $bytes |
||
| 203 | * @param int $decimals |
||
| 204 | * @param bool $binary |
||
| 205 | * |
||
| 206 | * @return string |
||
| 207 | */ |
||
| 208 | public function filesizeFromBytesToHumanReadable($bytes, $decimals = 2, $binary = true) |
||
| 209 | { |
||
| 210 | $binaryPrefices = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; |
||
| 211 | $decimalPrefices = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; |
||
| 212 | $factor = intval(floor((strlen($bytes) - 1) / 3)); |
||
| 213 | |||
| 214 | return sprintf("%.{$decimals}f", $bytes / pow($binary ? 1024 : 1000, $factor)) . ' ' . $binary ? $binaryPrefices[$factor] : $decimalPrefices[$factor]; |
||
| 215 | } |
||
| 216 | |||
| 217 | /** |
||
| 218 | * @param string $path |
||
| 219 | * |
||
| 220 | * @return string |
||
| 221 | */ |
||
| 222 | public function getAbsolutePath($path) |
||
| 223 | { |
||
| 224 | return config('filesystems.disks.local.root') . DIRECTORY_SEPARATOR . trim($path, DIRECTORY_SEPARATOR); |
||
| 225 | } |
||
| 226 | |||
| 227 | /** |
||
| 228 | * @param string $hash |
||
| 229 | * @param string $tag |
||
| 230 | * |
||
| 231 | * @return bool |
||
| 232 | * @throws \Exception |
||
| 233 | */ |
||
| 234 | public function deleteFile($hash, $tag = '') |
||
| 235 | { |
||
| 236 | /** @var \App\Models\File $file */ |
||
| 237 | $file = File::findOrFail($hash); |
||
| 238 | if ($file->load('uploaders')->count()) { |
||
|
0 ignored issues
–
show
|
|||
| 239 | /** @var \App\Models\User $me */ |
||
| 240 | $me = app('sentinel')->getUser(); |
||
| 241 | if ($file->uploaders->contains('id', $me->id)) { |
||
| 242 | $pivotToDelete = $file->uploaders()->newPivotStatement()->where('user_id', '=', $me->id); |
||
| 243 | if (!empty($tag)) { |
||
| 244 | $pivotToDelete->where('tag', '=', $tag); |
||
| 245 | } |
||
| 246 | $pivotToDelete->delete(); |
||
| 247 | $file->load('uploaders'); |
||
| 248 | } |
||
| 249 | !$file->uploaders->count() && app('filesystem')->disk($file->disk)->delete($file->path) && $file->delete(); |
||
| 250 | } |
||
| 251 | |||
| 252 | return true; |
||
| 253 | } |
||
| 254 | |||
| 255 | private function cleanupChunks() |
||
| 256 | { |
||
| 257 | foreach ($this->filesystem->directories($this->temporaryChunksFolder) as $file) { |
||
| 258 | if (time() - $this->filesystem->lastModified($file) > $this->chunksExpireIn) { |
||
| 259 | $this->filesystem->deleteDirectory($file); |
||
| 260 | } |
||
| 261 | } |
||
| 262 | } |
||
| 263 | |||
| 264 | /** |
||
| 265 | * @param \Illuminate\Http\Request $request |
||
| 266 | * |
||
| 267 | * @return void |
||
| 268 | */ |
||
| 269 | private function combineChunks(Request $request) |
||
| 270 | { |
||
| 271 | # Prelim |
||
| 272 | $fineUploaderUuid = $request->get('qquuid'); |
||
| 273 | $chunksFolder = $this->temporaryChunksFolder . DIRECTORY_SEPARATOR . $fineUploaderUuid; |
||
| 274 | $totalNumberOfChunks = $request->has('qqtotalparts') ? intval($request->get('qqtotalparts')) : 1; |
||
| 275 | |||
| 276 | # Do we have all chunks? |
||
| 277 | $numberOfExistingChunks = count($this->filesystem->files($chunksFolder)); |
||
| 278 | if ($numberOfExistingChunks != $totalNumberOfChunks) { |
||
| 279 | throw new FileStreamExceptions\UploadIncompleteException; |
||
| 280 | } |
||
| 281 | |||
| 282 | # We have all chunks, proceed with combine. |
||
| 283 | $targetStream = fopen($this->getAbsolutePath($fineUploaderUuid), 'wb'); |
||
| 284 | for ($i = 0; $i < $totalNumberOfChunks; $i++) { |
||
| 285 | $chunkStream = fopen($this->getAbsolutePath($chunksFolder . DIRECTORY_SEPARATOR . $i), 'rb'); |
||
| 286 | stream_copy_to_stream($chunkStream, $targetStream); |
||
| 287 | fclose($chunkStream); |
||
| 288 | } |
||
| 289 | fclose($targetStream); |
||
| 290 | $this->filesystem->deleteDirectory($chunksFolder); |
||
| 291 | } |
||
| 292 | } |
||
| 293 |
If you implement
__calland you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.This is often the case, when
__callis implemented by a parent class and only the child class knows which methods exist: