dir(); $images = collect(File::files($dir)) ->filter(fn ($f) => in_array(strtolower($f->getExtension()), self::EXTENSIONS, true)) ->map(function ($f) { $size = @getimagesize($f->getPathname()) ?: [null, null]; return [ 'name' => $f->getFilename(), 'url' => config('signature.public_url').'/img/'.rawurlencode($f->getFilename()), 'kb' => (int) ceil($f->getSize() / 1024), 'width' => $size[0], 'height' => $size[1], 'modified' => $f->getMTime(), 'used_in' => $this->usages($f->getFilename()), ]; }) ->sortByDesc('modified')->values(); return view('admin.images.index', ['images' => $images, 'maxKb' => config('signature.image_max_kb')]); } public function store(Request $request): RedirectResponse { $request->validate([ 'image' => ['required', 'file', 'mimes:png,jpg,jpeg,gif', 'mimetypes:image/png,image/jpeg,image/gif', 'max:'.config('signature.image_max_kb')], 'name' => ['nullable', 'string', 'max:80'], ], [ 'image.mimes' => 'Formats acceptés : PNG, JPEG ou GIF.', 'image.mimetypes' => 'Le fichier n\'est pas une image PNG, JPEG ou GIF valide.', 'image.max' => 'Image trop lourde (2 Mo maximum ; visez moins de 100 Ko pour une signature).', ]); $file = $request->file('image'); abort_unless(@getimagesize($file->getRealPath()), 422, 'Image illisible.'); $ext = strtolower($file->getClientOriginalExtension()) === 'jpeg' ? 'jpg' : strtolower($file->getClientOriginalExtension()); $base = Str::slug(pathinfo($request->input('name') ?: $file->getClientOriginalName(), PATHINFO_FILENAME)) ?: 'image'; $name = $this->uniqueName($base, $ext); $file->move($this->dir(), $name); @chmod($this->dir().DIRECTORY_SEPARATOR.$name, 0644); AuditLog::record('admin', $this->actor(), 'image.upload', null, ['file' => $name], $this->tenantId()); return back()->with('status', "Image « {$name} » en ligne. Dans un template : {{site_url}}/img/{$name}"); } public function destroy(string $name): RedirectResponse { $name = basename($name); $path = $this->dir().DIRECTORY_SEPARATOR.$name; abort_unless(File::isFile($path) && in_array(strtolower(pathinfo($name, PATHINFO_EXTENSION)), self::EXTENSIONS, true), 404); if ($used = $this->usages($name)) { return back()->withErrors(['image' => "« {$name} » est utilisée par : ".implode(', ', $used).'. Retirez-la d\'abord.']); } File::delete($path); AuditLog::record('admin', $this->actor(), 'image.delete', null, ['file' => $name], $this->tenantId()); return back()->with('status', "Image « {$name} » supprimée."); } private function dir(): string { $dir = config('signature.image_path'); File::ensureDirectoryExists($dir); return $dir; } private function uniqueName(string $base, string $ext): string { $name = "{$base}.{$ext}"; for ($i = 2; File::exists($this->dir().DIRECTORY_SEPARATOR.$name); $i++) { $name = "{$base}-{$i}.{$ext}"; } return $name; } /** Templates (publiés ou brouillons) et campagnes qui référencent l'image. */ private function usages(string $name): array { $like = '%/img/'.$name.'%'; $templates = Template::where('tenant_id', $this->tenantId()) ->where(fn ($q) => $q->where('html_full', 'like', $like)->orWhere('html_short', 'like', $like) ->orWhere('draft_html_full', 'like', $like)->orWhere('draft_html_short', 'like', $like)) ->pluck('name')->map(fn ($n) => "template {$n}"); $campaigns = Campaign::where('tenant_id', $this->tenantId())->where('image_path', $name) ->pluck('name')->map(fn ($n) => "campagne {$n}"); return $templates->merge($campaigns)->all(); } }