Files
projectsend/app/Modules/Files/Uploads/StoreUploadedFile.php
T
ignacionelson 6e47d76ba6 ProjectSend 2.0.0
Client file sharing, rebuilt from the ground up: a private area per
client, resumable uploads, folders, groups and categories, sharing with
expiry dates and download limits, comments, file versions, an activity
log, a REST API, and sixteen languages.

This repository begins here. ProjectSend 2 was developed privately, and
that development history is not published — the previous generation
remains available, with its own history, at projectsend/legacy.

Free software under the GNU General Public License v2, or (at your
option) any later version.
2026-08-14 01:38:12 -03:00

76 lines
2.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Modules\Files\Uploads;
use App\Models\User;
use App\Modules\Audit\Action;
use App\Modules\Audit\ActivityLogger;
use App\Modules\Files\Models\File;
/**
* The single place a stored payload becomes a File record — shared by
* the plain intake and the chunked-upload completion, so metadata
* persistence and auditing never diverge.
*/
class StoreUploadedFile
{
public function __construct(
private readonly ActivityLogger $activity,
) {}
public function create(
User $uploader,
string $originalName,
string $path,
string $mimeType,
int $size,
string $checksum,
?string $name = null,
?string $description = null,
?int $folderId = null,
string $disk = 'files',
Action $action = Action::FileUploaded,
): File {
$originalName = self::sanitizeFilename($originalName);
$file = File::query()->create([
'uploaded_by' => $uploader->id,
'folder_id' => $folderId,
'name' => $name !== null && $name !== ''
? $name
: pathinfo($originalName, PATHINFO_FILENAME),
'description' => $description,
'original_name' => $originalName,
'path' => $path,
'disk' => $disk,
'mime_type' => $mimeType,
'size' => $size,
'checksum' => $checksum,
]);
$this->activity->log($action, $uploader, $file);
return $file;
}
/**
* An uploader picks original_name freely — it is validated for length
* and nothing else — and it is echoed back in the Content-Disposition
* header of every download, preview and thumbnail response. A CR or LF
* in a header value is header injection; PHP's header() refuses to
* emit one, so today that fails closed as a 500 rather than a split
* response, but a filename should not be able to break the response at
* all. Strip the whole C0/C1 control range once, here, rather than
* hoping each of the five emission sites remembers to.
*/
public static function sanitizeFilename(string $name): string
{
$clean = preg_replace('/[\x00-\x1F\x7F]/u', '', $name) ?? $name;
$clean = trim($clean);
return $clean === '' ? 'file' : $clean;
}
}