\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; public $mediaItems = []; public $uploadFiles = []; public $uploadDescription = ''; public $uploadCategory = 'image'; public $showViewer = false; public $viewingMedia = null; protected $rules = [ 'uploadFiles.*' => 'required|file|max:51200', // 50 MB per file 'uploadDescription' => 'nullable|string|max:500', 'uploadCategory' => 'required|in:image,document,other', ]; protected $messages = [ 'uploadFiles.*.max' => 'Cada archivo debe pesar menos de 50 MB.', ]; public function mount($mediableType, $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->loadMedia(); } public function loadMedia() { $this->mediaItems = Media::where('mediable_type', $this->mediableType) ->where('mediable_id', $this->mediableId) ->with('uploader') ->latest() ->get(); } public function upload() { $user = Auth::user(); if (!$user->can('upload layers') && !$user->hasRole('Admin')) { session()->flash('error', 'Sin permisos.'); return; } $this->validate(); if (empty($this->uploadFiles)) { session()->flash('error', 'Selecciona al menos un archivo.'); 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(); 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', ], true)) { $category = 'document'; } $entityType = class_basename($this->entity); $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, 'file_extension' => $ext, 'file_size' => $size, 'category' => $category, 'description' => $this->uploadDescription, 'uploaded_by' => $user->id, ]); $uploaded++; } $this->reset(['uploadFiles', 'uploadDescription']); $this->loadMedia(); $this->dispatch('mediaUploaded', [ 'mediableType' => $this->mediableType, 'mediableId' => $this->mediableId, ]); session()->flash('message', "{$uploaded} archivo(s) subido(s) correctamente."); } public function deleteMedia($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) { session()->flash('error', 'No puedes borrar archivos de otro usuario.'); return; } $media->delete(); $this->loadMedia(); session()->flash('message', 'Archivo eliminado.'); } public function viewMedia($mediaId) { $media = Media::where('id', $mediaId) ->where('mediable_type', $this->mediableType) ->where('mediable_id', $this->mediableId) ->firstOrFail(); if (!$media->is_image) { $this->dispatch('openUrl', $media->url); return; } $this->viewingMedia = $media; $this->showViewer = true; } public function closeViewer() { $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), ]); } }