expiration = Setting::get('default_expiration', '7d') ?: '7d'; } /** * Register the files a visitor chose and hand the browser what it encrypts and sends them * with. A file an admin limit refuses gets `null` in its place and the reason under `files`. * * @param array $files * @return array */ public function registerFiles(array $files, ShareService $shareService): array { $this->resetErrorBag('files'); $targets = []; foreach ($files as $file) { try { $shareFile = $shareService->registerFile( $this->pendingShare(), (string) ($file['name'] ?? ''), (int) ($file['size'] ?? -1), isset($file['path']) ? (string) $file['path'] : null, ); } catch (ValidationException $e) { if (! $this->getErrorBag()->has('files')) { $this->addError('files', $e->errors()['files'][0]); } $targets[] = null; continue; } if ($this->pendingToken !== $shareFile->share->token) { $this->pendingToken = $shareFile->share->token; session()->push('pending_shares', $this->pendingToken); } $header = $shareService->readHeader($shareFile); $targets[] = [ 'id' => $shareFile->id, 'url' => Str::beforeLast(route('upload.chunk', ['shareFile' => $shareFile, 'index' => 0]), '/'), 'key' => $shareFile->share->encryption_key, 'noncePrefix' => bin2hex($header['noncePrefix']), 'chunkSize' => $header['chunkSize'], 'chunkCount' => $header['chunkCount'], ]; } return $targets; } /** * Take files out of the pending share, whether or not their upload finished. * * @param array $fileIds */ public function removeFiles(array $fileIds, ShareService $shareService): void { $files = $this->pendingShare()?->files()->whereIn('id', array_map('intval', $fileIds))->get() ?? []; foreach ($files as $file) { $shareService->removeFile($file); } $this->resetErrorBag('files'); } /** * Fill in a generated password as protection is switched on, when the admin chose "Prefilled". * A password already in the field stays. */ public function updatedUsePassword(bool $value): void { $passwordGenerator = app(PasswordGeneratorService::class); if ($value && $this->password === '' && $passwordGenerator->mode() === 'prefill') { $this->password = $passwordGenerator->generate(); } } public function generatePassword(PasswordGeneratorService $passwordGenerator): void { if ($passwordGenerator->mode() === 'off') { return; } $this->password = $passwordGenerator->generate(); $this->resetErrorBag('password'); } public function createShare(ShareService $shareService): void { $rules = []; if (! Setting::get('allow_never_expire', false)) { $rules['expiration'] = ['required', 'string', 'in:1h,24h,48h,7d,14d,30d']; } if ($this->usePassword) { $rules['password'] = ['required', 'string', 'min:8']; } if ($rules !== []) { $this->validate($rules, [ 'expiration.required' => __('An expiration time is required.'), ]); } $pendingShare = $this->pendingShare(); if ($pendingShare === null) { $this->addError('files', __('Please select at least one file to upload.')); return; } $share = $shareService->completeShare($pendingShare, [ 'password' => $this->usePassword ? $this->password : null, 'expires_at' => match ($this->expiration) { '1h' => now()->addHour(), '24h' => now()->addDay(), '48h' => now()->addDays(2), '7d' => now()->addWeek(), '14d' => now()->addDays(14), '30d' => now()->addMonth(), default => null, }, 'max_downloads' => $this->maxDownloads ?: null, ]); session()->put('pending_shares', array_values(array_diff(session('pending_shares', []), [$share->token]))); // The page the upload leads to offers the password once more, next to the link; it is // never stored in the clear, so this flash is the only way it gets there. if ($this->usePassword) { session()->flash('share_password', [ 'token' => $share->token, 'password' => Crypt::encryptString($this->password), ]); } $this->redirect(route('share.created', $share), navigate: true); } public function render(): mixed { $shareService = app(ShareService::class); $pendingFiles = $this->pendingShare()?->files()->orderBy('id')->get() ?? collect(); return view('livewire.file-uploader', [ 'pendingFiles' => $pendingFiles, 'allFilesUploaded' => $pendingFiles->isNotEmpty() && $pendingFiles->every(fn ($file): bool => $file->completed_at !== null), 'isStorageFull' => $shareService->isStorageFull(), 'siteTitle' => Setting::get('site_title'), 'siteDescription' => Setting::get('site_description'), 'siteLogo' => Setting::get('site_logo'), 'allowNeverExpire' => (bool) Setting::get('allow_never_expire', false), 'passwordGeneratorMode' => app(PasswordGeneratorService::class)->mode(), ]); } /** * This page's pending share, while it is still pending and this session started it. */ private function pendingShare(): ?Share { if ($this->pendingToken === null || ! in_array($this->pendingToken, session('pending_shares', []), true)) { return null; } return Share::query()->where('token', $this->pendingToken)->whereNull('completed_at')->first(); } }