restore: bring back f8a1310 (security review) state

Restores all files to the f8a1310 security-review snapshot as requested,
plus the 2 boot-critical fixes from a24c8a2 (config/session.php env()
instead of app()->environment(), and removal of the duplicate $activeTab
in ProjectMap.php) so the application actually boots.

Forward commit, no history rewrite. The 7d854ff state remains in history.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 10:36:44 +02:00
parent c44958ac16
commit 941dbd5997
26 changed files with 1163 additions and 1196 deletions
+85 -43
View File
@@ -4,7 +4,6 @@ namespace App\Livewire;
use Livewire\Component;
use Livewire\WithFileUploads;
use Livewire\Attributes\On;
use App\Models\Media;
use App\Models\Project;
use App\Models\Phase;
@@ -12,44 +11,60 @@ use App\Models\Layer;
use App\Models\Feature;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class MediaManager extends Component
{
use WithFileUploads;
// Polimórfico: a qué entidad pertenece
/**
* Whitelist of allowed mediable types (prevents arbitrary class instantiation).
* Keys are the public string accepted in mount(); values are FQCN.
*/
private const ALLOWED_TYPES = [
'App\\Models\\Project' => \App\Models\Project::class,
'App\\Models\\Phase' => \App\Models\Phase::class,
'App\\Models\\Layer' => \App\Models\Layer::class,
'App\\Models\\Feature' => \App\Models\Feature::class,
'App\\Models\\Inspection' => \App\Models\Inspection::class,
'App\\Models\\Issue' => \App\Models\Issue::class,
];
public $mediableType;
public $mediableId;
public $entity; // instancia cargada
public $entity;
public $mediaItems = [];
// Subida
public $uploadFiles = [];
public $uploadFiles = [];
public $uploadDescription = '';
public $uploadCategory = 'image';
public $uploadCategory = 'image';
// Modal visor
public $showViewer = false;
public $showViewer = false;
public $viewingMedia = null;
protected $rules = [
'uploadFiles.*' => 'required|file|max:102400', // 100MB total
'uploadFiles.*' => 'required|file|max:51200', // 50 MB per file
'uploadDescription' => 'nullable|string|max:500',
'uploadCategory' => 'required|in:image,document,other',
'uploadCategory' => 'required|in:image,document,other',
];
protected $messages = [
'uploadFiles.*.max' => 'Cada archivo debe pesar menos de 100MB.',
'uploadFiles.*.max' => 'Cada archivo debe pesar menos de 50 MB.',
];
public function mount($mediableType, $mediableId)
{
$this->mediableType = $mediableType;
$this->mediableId = $mediableId;
// Validate type against whitelist to prevent RCE via class instantiation
if (!array_key_exists($mediableType, self::ALLOWED_TYPES)) {
abort(400, 'Invalid mediable type.');
}
$this->mediableType = $mediableType;
$this->mediableId = (int) $mediableId;
$modelClass = self::ALLOWED_TYPES[$mediableType];
$this->entity = $modelClass::findOrFail($this->mediableId);
$this->entity = $mediableType::findOrFail($mediableId);
$this->loadMedia();
}
@@ -77,37 +92,58 @@ class MediaManager extends Component
return;
}
// Allowed MIME types (server-side validation)
$allowedMimes = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/plain', 'text/csv',
'application/zip', 'application/x-zip-compressed',
];
$uploaded = 0;
foreach ($this->uploadFiles as $file) {
$mime = $file->getMimeType();
$ext = $file->getClientOriginalExtension();
$size = $file->getSize();
$name = $file->getClientOriginalName();
// Determinar categoría automática
if (!in_array($mime, $allowedMimes, true)) {
session()->flash('error', "Tipo de archivo no permitido: {$mime}");
continue;
}
$ext = $file->getClientOriginalExtension();
$size = $file->getSize();
$name = substr($file->getClientOriginalName(), 0, 255);
$category = $this->uploadCategory;
if (str_starts_with($mime, 'image/')) {
$category = 'image';
} elseif (in_array($mime, ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'])) {
} elseif (in_array($mime, [
'application/pdf', 'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
], true)) {
$category = 'document';
}
// Guardar en disco
$entityType = class_basename($this->entity);
$dir = "uploads/{$entityType}s/{$this->mediableId}/media";
$dir = "uploads/{$entityType}s/{$this->mediableId}/media";
$path = $file->store($dir, 'public');
Media::create([
'mediable_type' => $this->mediableType,
'mediable_id' => $this->mediableId,
'name' => $name,
'file_path' => $path,
'file_type' => $mime,
'mediable_type' => $this->mediableType,
'mediable_id' => $this->mediableId,
'name' => $name,
'file_path' => $path,
'file_type' => $mime,
'file_extension' => $ext,
'file_size' => $size,
'category' => $category,
'description' => $this->uploadDescription,
'uploaded_by' => $user->id,
'file_size' => $size,
'category' => $category,
'description' => $this->uploadDescription,
'uploaded_by' => $user->id,
]);
$uploaded++;
@@ -116,18 +152,21 @@ class MediaManager extends Component
$this->reset(['uploadFiles', 'uploadDescription']);
$this->loadMedia();
// Notificar al mapa si corresponde
$this->dispatch('mediaUploaded', [
'mediableType' => $this->mediableType,
'mediableId' => $this->mediableId,
'mediableId' => $this->mediableId,
]);
session()->flash('message', "$uploaded archivo(s) subido(s) correctamente.");
session()->flash('message', "{$uploaded} archivo(s) subido(s) correctamente.");
}
public function deleteMedia($mediaId)
{
$media = Media::findOrFail($mediaId);
// Ensure the media belongs to the entity this component manages (IDOR prevention)
$media = Media::where('id', $mediaId)
->where('mediable_type', $this->mediableType)
->where('mediable_id', $this->mediableId)
->firstOrFail();
$user = Auth::user();
if (!$user->hasRole('Admin') && $media->uploaded_by !== $user->id) {
@@ -142,28 +181,31 @@ class MediaManager extends Component
public function viewMedia($mediaId)
{
$media = Media::findOrFail($mediaId);
$media = Media::where('id', $mediaId)
->where('mediable_type', $this->mediableType)
->where('mediable_id', $this->mediableId)
->firstOrFail();
if (!$media->is_image) {
// Si no es imagen, abrir en nueva pestaña
$this->dispatch('openUrl', $media->url);
return;
}
$this->viewingMedia = $media;
$this->showViewer = true;
$this->showViewer = true;
}
public function closeViewer()
{
$this->showViewer = false;
$this->viewingMedia = null;
$this->showViewer = false;
$this->viewingMedia = null;
}
public function render()
{
return view('livewire.media-manager', [
'entityName' => class_basename($this->entity) . ': ' . ($this->entity->name ?? $this->entity->id),
'images' => $this->mediaItems->filter(fn($m) => $m->is_image),
'documents' => $this->mediaItems->filter(fn($m) => !$m->is_image),
'images' => $this->mediaItems->filter(fn ($m) => $m->is_image),
'documents' => $this->mediaItems->filter(fn ($m) => !$m->is_image),
]);
}
}
}