From 86e5c02c43fcdd938c1fc8f07f0b3364f95429c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Tue, 24 May 2022 12:38:30 +0200 Subject: [PATCH] Refactor Ticket Controller, move uploaded images to storage --- app/Http/Controllers/Controller.php | 71 +- app/Http/Controllers/TicketController.php | 604 +++++++++++------- app/Http/Middleware/XssSanitizer.php | 6 +- app/Http/Requests/CreateMessage.php | 23 +- app/Http/Requests/StoreTicket.php | 33 +- app/Http/Requests/UpdateTicket.php | 18 +- app/Lib/{Utils.php => ToolboxHelper.php} | 111 +++- app/Mail/TicketMention.php | 4 +- app/Mail/TicketUpdate.php | 4 +- app/Models/Message.php | 40 +- app/Models/Site.php | 69 +- app/Models/Ticket.php | 71 +- config/app.php | 2 +- ...22_05_24_122728_move_images_to_storage.php | 59 ++ resources/views/modules/loadLevel.blade.php | 10 +- resources/views/ticket/create.blade.php | 4 +- resources/views/ticket/edit.blade.php | 7 +- resources/views/ticket/form-inputs.blade.php | 6 +- resources/views/ticket/index.blade.php | 8 +- resources/views/ticket/show.blade.php | 36 +- 20 files changed, 830 insertions(+), 356 deletions(-) rename app/Lib/{Utils.php => ToolboxHelper.php} (54%) create mode 100644 database/migrations/2022_05_24_122728_move_images_to_storage.php diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 1fae538..a477dd0 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -8,6 +8,7 @@ use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Routing\Controller as BaseController; use App\Enums\TicketStatus; use App\Models\Ticket; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\View; @@ -55,9 +56,9 @@ class Controller extends BaseController /** @var \App\Models\User|null */ $user = Auth::user(); if (!is_null($user) and !$user->isAdmin()) { - $ticketsNew = $ticketsNew->where('is_internal', 0)->whereNotIn('site_id', $ignore); - $ticketsWaiting = $ticketsWaiting->where('is_internal', 0)->whereNotIn('site_id', $ignore); - $ticketsInProgress = $ticketsInProgress->where('is_internal', 0)->whereNotIn('site_id', $ignore); + $ticketsNew = $ticketsNew->where('is_internal', false)->whereNotIn('site_id', $ignore); + $ticketsWaiting = $ticketsWaiting->where('is_internal', false)->whereNotIn('site_id', $ignore); + $ticketsInProgress = $ticketsInProgress->where('is_internal', false)->whereNotIn('site_id', $ignore); } View::share('ticketsNew', $ticketsNew->count()); @@ -67,4 +68,68 @@ class Controller extends BaseController return $next($request); }); } + + /** + * Build Search query on specified fields + * splitting search into words or using whole sentence + * + * @param Builder $query The eloquent query builder. + * @param string $search The query string. + * @param callable $searchQuery The query string. + * @param string ...$fields Either a simple array or a string + * in case of a string the two fields would be concatenate to make the search. + * @return Builder + */ + protected function searchQuery( + Builder $query, + string $search, + callable $searchQuery = null, + string ...$fields + ): Builder { + if (count($fields)) { + // Search using words. + $words = explode(' ', $search); + $query->where(function ($query) use ($searchQuery, $words, $search, $fields) { + foreach ($words as $word) { + if (!strlen($word)) { + continue; + } + $this->searchOnFields($query, $word, $fields); + } //end foreach + // Search whole using whole sentence. + $this->searchOnFields($query, $search, $fields); + if ($searchQuery) { + $searchQuery($query); + } + }); + } //end if + return $query; + } + + /** + * Build query search on all fields + * + * @param Builder $query + * @param string $search + * @param array $fields + * @return Builder + */ + private function searchOnFields(Builder $query, string $search, array $fields): Builder + { + $table = $query->getModel()->getTable(); + foreach ($fields as $f) { + $f = str_replace('?', '\?', $f); + if (is_array($f)) { + $query->orWhereRaw( + sprintf("UPPER(CONCAT(%s)) LIKE UPPER(?)", collect($f)->map(function ($q) use ($table) { + return "`$table`.`$q`"; + })->implode(', \' \', ')), + ["%$search%"] + ); + } else { + $query->orWhereRaw("UPPER(`$table`.`$f`) LIKE UPPER(?)", ["%$search%"]); + } + } + return $query; + } } diff --git a/app/Http/Controllers/TicketController.php b/app/Http/Controllers/TicketController.php index 6f4c17f..3cfa550 100644 --- a/app/Http/Controllers/TicketController.php +++ b/app/Http/Controllers/TicketController.php @@ -8,35 +8,44 @@ use App\Events\NotifySubscriptions; use App\Events\TicketStatusChanged; use App\Http\Requests\ChangeTicketStatus; use App\Http\Requests\CreateMessage; -use App\Http\Requests\UpdateMessage; use App\Http\Requests\SortTickets; use App\Http\Requests\StoreTicket; +use App\Http\Requests\UpdateMessage; use App\Http\Requests\UpdateTicket; +use App\Lib\ToolboxHelper; use App\Mail\NewTicket; use App\Models\Message; use App\Models\Site; use App\Models\Ticket; use App\Models\User; -use Exception; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Session; use Illuminate\Support\Str; class TicketController extends Controller { + /** + * TicketController + */ public function __construct() { parent::__construct(); $this->authorizeResource(Ticket::class, 'ticket'); } + /** + * Tickets List + * + * @param SortTickets $request + * @return \Illuminate\Http\JsonResponse|\Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory + */ public function index(SortTickets $request) { - // handle ajax + // * Handle Ajax if ($request->ajax()) { $tickets = Ticket::with('user'); if ($request->s) { @@ -71,13 +80,12 @@ class TicketController extends Controller $q->whereId($user->id); }); }); - $tickets = $tickets->where('is_internal', 0); + $tickets = $tickets->where('is_internal', false); } $tickets = $tickets->where('status', ($request->st == TicketStatus::archived()) ? '=' : '<>', TicketStatus::archived()); - if ($request->search) { - $tickets = $this->searchTicket($tickets, $request); - } + + $tickets = $this->searchTicket($tickets, $request); $tickets = $this->filterTickets($tickets, $request); $tickets = $tickets->paginate(); @@ -87,6 +95,321 @@ class TicketController extends Controller return view('ticket.index', compact('tickets', 'sites', 'status')); } + /** + * Show Ticket + * + * @param Request $request + * @param Ticket $ticket + * @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory + */ + public function show(Request $request, Ticket $ticket) + { + if ($request->ajax()) { + return \response()->json([ + 'label' => sprintf( + 'Ticket %d (%s): %s', + $ticket->id, + $ticket->user ? $ticket->user->email : __('Utilisateur supprimé'), + Str::limit(\preg_replace('/[[:cntrl:]]+/', '', $ticket->text), 35) + ), + 'value' => $ticket->id + ]); + } + + /** @var \App\Models\User */ + $user = auth()->user(); + + if (!$user->can('update', $ticket)) { + abort(403); + } + + if ($ticket->is_internal && !$user->can('changeInternalStatus', $ticket)) { + abort(403); + } + + $messages = $ticket->messages()->orderBy('created_at', 'DESC')->get(); + $isSubscribed = (bool)$ticket->subscriptions()->where('user_id', $user->id)->count(); + return view('ticket.show', compact('ticket', 'messages', 'isSubscribed')); + } + + /** + * Show create Form + * + * @return \Illuminate\Http\RedirectResponse + */ + public function create() + { + /** @var \App\Models\User */ + $user = auth()->user(); + if ($user->isAdmin()) { + $sites = Site::orderBy('name')->get(); + } else { + $sites = $user->sites; + } + + if (!count($sites)) { + return redirect()->route('sites.create') + ->with('warning', trans('Vous devez tout d\'abord créer un site pour y ajouter un ticket')); + } + return view('ticket.create', compact('sites')); + } + + /** + * Create ticket + * + * @param StoreTicket $request + * @return \Illuminate\Http\RedirectResponse + */ + public function store(StoreTicket $request) + { + try { + return DB::transaction(function () use ($request) { + /** @var \App\Models\Site */ + $site = Site::findOrFail($request->site_id); + + /** @var \App\Models\User */ + $user = auth()->user(); + + $ticket = new Ticket($request->validated()); + + /** @var \Illuminate\Http\UploadedFile|null */ + $fileTicket = $request->file_ticket; + /** @var \Illuminate\Http\UploadedFile|null */ + $fileTicket2 = $request->file_ticket2; + + if ($fileTicket) { + $ticket->file = ToolboxHelper::storeImage($ticket->getTable(), $fileTicket); + } + if ($fileTicket2) { + $ticket->file2 = ToolboxHelper::storeImage($ticket->getTable(), $fileTicket2); + } + + $ticket->user()->associate(auth()->user()); + $ticket->site()->associate($site); + $ticket->saveOrFail(); + + $user->subscriptions()->toggle($ticket->id); + + if (config('mail.MAIL_ADMIN')) { + Mail::to(config('mail.MAIL_ADMIN'))->send(new NewTicket($ticket)); + } else { + throw new \ErrorException('Missing MAIL_ADMIN in .env'); + } + + return redirect()->route('ticket.show', $ticket) + ->with('success', trans('Ticket enregistré !')); + }); + } catch (\ErrorException $e) { + report($e); + Session::flash('error', trans('Une erreur est survenue')); + } //end try + + return \back(); + } + + /** + * Show update form + * + * @param Ticket $ticket + * @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory + * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass + */ + public function edit(Ticket $ticket) + { + /** @var \App\Models\User */ + $user = auth()->user(); + if ($user->isAdmin()) { + $sites = Site::orderBy('name')->get(); + } else { + $sites = $user->sites; + } + return view('ticket.edit', compact('ticket', 'sites')); + } + + /** + * Update Model + * + * @param UpdateTicket $request + * @param Ticket $ticket + * @return \Illuminate\Http\RedirectResponse + */ + public function update(UpdateTicket $request, Ticket $ticket) + { + $ticket->fill($request->validated()); + + try { + return DB::transaction(function () use ($request, $ticket) { + /** @var \App\Models\Site */ + $site = Site::findOrFail($request->site_id); + + /** @var \Illuminate\Http\UploadedFile|null */ + $fileTicket = $request->file_ticket; + /** @var \Illuminate\Http\UploadedFile|null */ + $fileTicket2 = $request->file_ticket2; + + if ($fileTicket) { + $ticket->file = ToolboxHelper::storeImage($ticket->getTable(), $fileTicket); + } + if ($fileTicket2) { + $ticket->file2 = ToolboxHelper::storeImage($ticket->getTable(), $fileTicket2); + } + + $ticket->site()->associate($site); + $ticket->saveOrFail(); + + return redirect()->route('ticket.edit', $ticket->id) + ->with('success', 'Modifications enregistrées !'); + }); + } catch (\ErrorException $e) { + report($e); + Session::flash('error', trans('Une erreur est survenue')); + } //end try + + return \back(); + } + + /** + * Change ticket Status + * + * @param ChangeTicketStatus $request + * @param Ticket $ticket + * @return \Illuminate\Http\RedirectResponse + */ + public function changeStatus(ChangeTicketStatus $request, Ticket $ticket) + { + $ticket->fill($request->validated()); + $ticket->saveOrFail(); + + return back() + ->with('warning', trans('Le status à été modifié')); + } + + /** + * Add a new Message to the Ticket + * + * @param CreateMessage $request + * @param Ticket $ticket + * @return \Illuminate\Http\RedirectResponse + */ + public function createMessage(CreateMessage $request, Ticket $ticket) + { + try { + return DB::transaction(function () use ($request, $ticket) { + /** @var \Illuminate\Http\UploadedFile|null */ + $fileMessage = $request->file_message; + + $message = new Message($request->validated()); + $message->user()->associate(auth()->user()); + $message->ticket()->associate($ticket); + + /** @var \App\Models\User */ + $user = auth()->user(); + + if ($request->subscribe) { + if (!$user->subscriptions()->find($ticket->id)) { + $user->subscriptions()->attach($ticket->id); + } + } elseif ($user->subscriptions()->find($ticket->id)) { + $user->subscriptions()->detach($ticket->id); + } + + $message->saveOrFail(); + + if ($fileMessage) { + $message->file = ToolboxHelper::storeImage($message->getTable(), $fileMessage); + $message->saveOrFail(); + } + return back() + ->with('success', trans('Votre message à été ajouté')); + }); + } catch (\ErrorException $e) { + report($e); + Session::flash('error', trans('Une erreur est survenue')); + } //end try + + return back(); + } + + /** + * Update Message + * + * @param UpdateMessage $request + * @param Ticket $ticket + * @param Message $message + * @return \Illuminate\Http\RedirectResponse + * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed + */ + public function updateMessage(UpdateMessage $request, Ticket $ticket, Message $message) + { + $message->message = $request->message; + $message->saveOrFail(); + + return back() + ->with('success', trans('Votre message à bien été modifié')); + } + + /** + * Change ticket status to archived + * + * @param Ticket $ticket + * @return \Illuminate\Http\RedirectResponse + */ + public function setToArchive(Ticket $ticket) + { + $ticket->status = TicketStatus::archived(); + $ticket->saveOrFail(); + return redirect()->route('ticket.index') + ->with('warning', trans('Le ticket est archivé')); + } + + /** + * Subscribe to ticket + * + * @param Ticket $ticket + * @return \Illuminate\Http\RedirectResponse + */ + public function subscribe(Ticket $ticket) + { + /** @var \App\Models\User */ + $user = auth()->user(); + User::find(Auth::user()->id)->subscriptions()->toggle($ticket->id); + if ($user->subscriptions()->where('ticket_id', $ticket->id)->exists()) { + return back() + ->with('success', trans('Vous avez été inscrit')); + } + return back()->with('warning', trans('Vous avez été désinscrit')); + } + + /** + * Reopen ticket + * + * @param Ticket $ticket + * @return \Illuminate\Http\RedirectResponse + */ + public function reopen(Ticket $ticket) + { + $message = trans(":ticketName à réouvert le ticket n°:ticketNumber", [ + 'ticketName' => $ticket->user->name, + 'ticketNumber' => $ticket->id, + ]); + $oldStatus = $ticket->status; + $ticket->status = TicketStatus::inprogress(); + $ticket->saveQuietly(); + event(new TicketStatusChanged($ticket, $oldStatus, null, $message)); + event(new NotifySubscriptions($ticket, $message, collect([$ticket->user]))); + User::find(Auth::user()->id)->subscriptions()->toggle($ticket->id); + return back()->with('warning', trans('Votre ticket à été réouvert.')); + } + + // * PRIVATES + + /** + * Filter Tickets request constructor + * + * @param Builder $tickets + * @param Request $request + * @return Builder + */ private function filterTickets(Builder $tickets, Request $request): Builder { if ($request->rst) { @@ -141,251 +464,52 @@ class TicketController extends Controller return $tickets; } + /** + * Search ticket sql request constructor + * + * @param Builder $tickets + * @param Request $request + * @return Builder + */ private function searchTicket(Builder $tickets, Request $request): Builder { - $words = explode(' ', $request->search); - return $tickets->where(function ($q) use ($words) { - foreach ($words as $word) { - if (!strlen($word)) { - continue; - } - $q = $q->where(function ($q) use ($word) { + if ($request->exists('search')) { + if (empty($request->search)) { + Session::remove('tickets.search'); + } else { + Session::put('tickets.search', $request->search); + } + } + + if (!Session::has('tickets.search')) { + return $tickets; + } + + return $this->searchQuery($tickets, Session::get('tickets.search'), function ($q) { + $words = explode(' ', Session::get('tickets.search')); + if (\is_array($words)) { + foreach ($words as $word) { + if (!strlen($word)) { + continue; + } $s = collect(TicketStatus::toArray())->filter(function (TicketStatus $t) use ($word) { - return strpos(strtolower($t->label), $word) !== false; + return Str::slug($t->label) === Str::slug($word); })->map(function (TicketStatus $ticket) { return $ticket->value; }); - $t = collect(TicketType::toArray())->filter(function (TicketType $t) use ($word) { - return strpos(strtolower($t->label), $word) !== false; + $t = collect(TicketType::toArray())->filter(function (TicketType $s) use ($word) { + return Str::slug($s->label) === Str::slug($word); })->map(function (TicketType $ticket) { return $ticket->value; }); - $q->where('id', $word) - ->orwhere('text', 'LIKE', "%$word%") + $q->orWhere('id', $word) ->orWhereIn('status', $s) ->orWhereIn('type', $t) - ->orWhere('file', 'LIKE', "%$word%") - ->orWhere('file2', 'LIKE', "%$word%") ->orWhereHas('user', function ($q) use ($word) { $q->where('name', 'LIKE', "%$word%"); }); - }); - }//end foreach - }); - } - - public function show(Request $request, Ticket $ticket) - { - if ($request->ajax()) { - return \response()->json([ - 'label' => sprintf( - 'Ticket %d (%s): %s', - $ticket->id, - $ticket->user ? $ticket->user->email : __('Utilisateur supprimé'), - Str::limit(\preg_replace('/[[:cntrl:]]+/', '', $ticket->text), 35) - ), - 'value' => $ticket->id - ]); - } - - $user = Auth::user(); - - if (!$user->can('update', $ticket)) { - abort(403); - } - - if ($ticket->is_internal === 1 && !$user->can('changeInternalStatus', $ticket)) { - abort(403); - } - - $messages = $ticket->messages()->orderBy('created_at', 'DESC')->get(); - $isSubscribed = (bool)$ticket->subscriptions()->where('user_id', $user->id)->count(); - return view('ticket.show', compact('ticket', 'messages', 'isSubscribed')); - } - - public function create() - { - /** @var \App\Models\User */ - $user = auth()->user(); - if ($user->isAdmin()) { - $sites = Site::orderBy('name')->get(); - } else { - $sites = $user->sites; - } - - if (!count($sites)) { - return redirect()->route('sites.create') - ->with('warning', 'Vous devez tout d\'abord créer un site pour y ajouter un ticket'); - } - return view('ticket.create', compact('sites')); - } - - public function store(StoreTicket $request) - { - $site = Site::findOrFail($request->site_id); - - $ticket = new Ticket($request->validated()); - - if ($request->file_ticket) { - $ticket->file = Ticket::saveFile($site, $ticket, $ticket->type, $request->file_ticket); - } - if ($request->file_ticket2) { - $ticket->file2 = Ticket::saveFile($site, $ticket, $ticket->type, $request->file_ticket2); - } - - if ($request->has('is_internal')) { - $ticket->is_internal = 1; - } else { - $ticket->is_internal = 0; - } - - $ticket->user()->associate(auth()->user()); - $ticket->saveOrFail(); - /** @var \App\Models\User */ - $user = auth()->user(); - $user->subscriptions()->toggle($ticket->id); - - if (config('mail.MAIL_ADMIN')) { - Mail::to(config('mail.MAIL_ADMIN'))->send(new NewTicket($ticket)); - } else { - throw new Exception('Missing MAIL_ADMIN in .env'); - } - - return redirect()->route('ticket.show', $ticket) - ->with('success', 'Ticket enregistré !'); - } - - // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass - public function edit(Ticket $ticket) - { - /** @var \App\Models\User */ - $user = auth()->user(); - if ($user->isAdmin()) { - $sites = Site::orderBy('name')->get(); - } else { - $sites = $user->sites; - } - return view('ticket.edit', compact('ticket', 'sites')); - } - - public function update(UpdateTicket $request, Ticket $ticket) - { - $site = Site::findOrFail($request->site_id); - $ticket->fill($request->validated()); - - if ($request->file_ticket) { - if (File::exists($ticket->file)) { - File::delete($ticket->file); - } - $ticket->file = Ticket::saveFile($site, $ticket, $ticket->type, $request->file_ticket); - } - if ($request->file_ticket2) { - if (File::exists($ticket->file2)) { - File::delete($ticket->file2); - } - $ticket->file2 = Ticket::saveFile($site, $ticket, $ticket->type, $request->file_ticket2); - } - - if ($request->has('is_internal')) { - $ticket->is_internal = 1; - } else { - $ticket->is_internal = 0; - } - - $ticket->save(); - - return redirect()->route('ticket.edit', $ticket->id) - ->with('success', 'Modifications enregistrées !'); - } - - public function changeStatus(ChangeTicketStatus $request, Ticket $ticket) - { - $ticket->fill($request->validated()); - $ticket->save(); - - return back() - ->with('warning', trans('Le status à été modifié')); - } - - public function createMessage(CreateMessage $request, Ticket $ticket) - { - $fileMessage = $request->file_message; - - $message = new Message($request->validated()); - $message->user()->associate(auth()->user()); - $message->ticket()->associate($ticket); - - /** @var \App\Models\User */ - $user = auth()->user(); - - // Toggle auth user subscription - if ($request->subscribe) { - if (!$user->subscriptions()->find($ticket->id)) { - $user->subscriptions()->attach($ticket->id); - } - } elseif ($user->subscriptions()->find($ticket->id)) { - $user->subscriptions()->detach($ticket->id); - } - - $message->saveOrFail(); - - // ici $message->id n'est pas connu lors de la création du message. - // on doit donc sauvegarder cette info après le create initial - if ($request->file_message) { - $siteSlug = Str::slug($ticket->site->name); - $fileMessage = $request->file_message; - $fileName = $siteSlug . '-ticket-' . $ticket->id . - '-message-' . $message->id . '.' . $fileMessage->extension(); - $fileMessage->move(public_path('fichiers/' . $siteSlug . '/' . $ticket->id . '/'), $fileName); - $filePath = 'fichiers/' . $siteSlug . '/' . $ticket->id . '/' . $fileName; - $message->file = $filePath; - $message->saveOrFail(); - } - - - return back() - ->with('success', trans('Votre message à été ajouté')); - } - - public function updateMessage(UpdateMessage $request, Ticket $ticket, Message $message) - { - $message->message = $request->message; - $message->saveOrFail(); - - return back() - ->with('success', trans('Votre message à bien été modifié')); - } - - public function setToArchive(Ticket $ticket) - { - $ticket->status = TicketStatus::archived(); - $ticket->saveOrFail(); - return redirect()->route('ticket.index') - ->with('warning', trans('Le ticket est archivé')); - } - - public function subscribe(Ticket $ticket) - { - /** @var \App\Models\User */ - $user = auth()->user(); - User::find(Auth::user()->id)->subscriptions()->toggle($ticket->id); - $status = $user->subscriptions()->where('ticket_id', $ticket->id)->exists(); - if ($status) { - return back() - ->with('success', trans('Vous avez été inscrit')); - } - return back()->with('warning', trans('Vous avez été désinscrit')); - } - - public function reopen(Ticket $ticket) - { - $message = "{$ticket->user->name} à réouvert le ticket n°{$ticket->id}"; - $oldStatus = $ticket->status; - $ticket->status = TicketStatus::inprogress(); - $ticket->saveQuietly(); - event(new TicketStatusChanged($ticket, $oldStatus, null, $message)); - event(new NotifySubscriptions($ticket, $message, collect([$ticket->user]))); - User::find(Auth::user()->id)->subscriptions()->toggle($ticket->id); - return back()->with('warning', trans('Votre ticket à été réouvert.')); + } //end foreach + } //end if + }, 'text', 'file', 'file2'); } } diff --git a/app/Http/Middleware/XssSanitizer.php b/app/Http/Middleware/XssSanitizer.php index e01de74..955d285 100644 --- a/app/Http/Middleware/XssSanitizer.php +++ b/app/Http/Middleware/XssSanitizer.php @@ -2,7 +2,7 @@ namespace App\Http\Middleware; -use App\Lib\Utils; +use App\Lib\ToolboxHelper; use Illuminate\Http\Request; class XssSanitizer @@ -29,12 +29,12 @@ class XssSanitizer \array_walk_recursive($isJson, function (&$item) { if (\is_string($item)) { $item = clean($item); - $item = Utils::stripCkEditorSchemesOnMediaInputs($item); + $item = ToolboxHelper::stripCkEditorSchemesOnMediaInputs($item); } }); } else { $input = clean($input); - $input = Utils::stripCkEditorSchemesOnMediaInputs($input); + $input = ToolboxHelper::stripCkEditorSchemesOnMediaInputs($input); } } else { $input = strip_tags($input); diff --git a/app/Http/Requests/CreateMessage.php b/app/Http/Requests/CreateMessage.php index 3d85442..ad93dfb 100644 --- a/app/Http/Requests/CreateMessage.php +++ b/app/Http/Requests/CreateMessage.php @@ -13,13 +13,28 @@ class CreateMessage extends FormRequest */ public function authorize() { - /** @var \App\Models\User */ - $user = auth()->user(); + /** @var \App\Models\User|null */ + if (!($user = auth()->user())) { + return false; + } + return auth()->check() and auth()->user()->sites and ( + $user->isAdmin() or + auth()->user()->sites->contains(request()->route()->ticket->site_id) + ); + } + + /** + * Prepare the data for validation. + + + * + * @return void + */ + protected function prepareForValidation() + { $this->merge([ 'subscribe' => $this->subscribe ? true : false ]); - return auth()->check() and ($user->isAdmin() or - auth()->user()->sites->contains(request()->route()->ticket->site_id)); } /** diff --git a/app/Http/Requests/StoreTicket.php b/app/Http/Requests/StoreTicket.php index 0cce664..a5bbb8b 100644 --- a/app/Http/Requests/StoreTicket.php +++ b/app/Http/Requests/StoreTicket.php @@ -15,16 +15,34 @@ class StoreTicket extends FormRequest */ public function authorize() { - /** @var \App\Models\User */ - $user = auth()->user(); - if (!$user->isAdmin()) { - $this->merge([ - 'status' => TicketStatus::new()->value, - ]); - } return auth()->check(); } + /** + * Prepare the data for validation. + * + * @return void + */ + protected function prepareForValidation() + { + /** @var \App\Models\User|null */ + $user = auth()->user(); + if ($user) { + if (!$user->isAdmin()) { + $this->merge([ + 'status' => $this->site->status->value, + 'is_internal' => false, + ]); + } else { + $this->merge([ + 'is_internal' => $this->is_internal ? true : false, + ]); + } + } + } + + + /** * Get the validation rules that apply to the request. * @@ -75,6 +93,7 @@ class StoreTicket extends FormRequest 'text' => 'required|string|min:40', 'file_ticket' => 'mimes:jpeg,jpg,png,pdf|max:2048', 'file_ticket2' => 'mimes:jpeg,jpg,png,pdf|max:2048', + 'is_internal' => 'required|boolean' ]; } } diff --git a/app/Http/Requests/UpdateTicket.php b/app/Http/Requests/UpdateTicket.php index 0ec1413..b7fa8e6 100644 --- a/app/Http/Requests/UpdateTicket.php +++ b/app/Http/Requests/UpdateTicket.php @@ -4,15 +4,19 @@ namespace App\Http\Requests; class UpdateTicket extends StoreTicket { - public function authorize() + /** + * Determine if the user is authorized to make this request. + * + * @return boolean + */ + public function authorize(): bool { - /** @var \App\Models\User */ - $user = auth()->user(); - if (!$user->isAdmin()) { - $this->merge(['status' => $this->site->status->value, - ]); + /** @var \App\Models\User|null */ + if (!($user = auth()->user())) { + return false; } return auth()->check() and ($user->isAdmin() or - request()->route()->ticket->created_by === auth()->user()->id); + optional(request()->route()->ticket)->created_by === $user->id + ); } } diff --git a/app/Lib/Utils.php b/app/Lib/ToolboxHelper.php similarity index 54% rename from app/Lib/Utils.php rename to app/Lib/ToolboxHelper.php index 03a7655..ab9ca09 100644 --- a/app/Lib/Utils.php +++ b/app/Lib/ToolboxHelper.php @@ -3,9 +3,16 @@ namespace App\Lib; use App\Enums\LoadLevel; +use Illuminate\Http\Request; +use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\View; +use Illuminate\Support\Str; +use Illuminate\Validation\ValidationException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -class Utils +class ToolboxHelper { /** * Compute plain text to html representation. @@ -143,4 +150,106 @@ class Utils $input = \preg_replace('/(?<=)/m', '', $input); return $input; } + + /** + * Assert That no Back Office url is present on txt + * + * @param string $field + * @param string $text + * @return string + * @throws \Illuminate\Validation\ValidationException If url is a bo url. + */ + public static function assertNoInternalBoUrls(string $field, string $text): string + { + $routes = Route::getRoutes(); + // * Url replace and email replace. + $text = preg_replace_callback( + // phpcs:ignore Generic.Files.LineLength.TooLong + '/(?P(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*))/', + function ($matches) use ($routes, $field) { + $match = collect($matches)->first() ?? ''; + $url = $match; + try { + $request = Request::create($url); + // If a route is match then it do not throws exception. + if ( + \parse_url(config('app.url'), \PHP_URL_HOST) === \parse_url($url, \PHP_URL_HOST) && + trim($routes->match($request)->getPrefix() ?? '', '/') === 'bo' + ) { + // Removes the matched url. + throw ValidationException::withMessages([ + $field => sprintf( + '%s : Il est interdit de coller une url du back office dans ce champ', + trans($field) + ) + ]); + } + return $match; + } catch (NotFoundHttpException $e) { + return $match; + } + }, + $text + ); + return $text; + } + + /** + * Store an image of different types + * + * @param string $table + * @param \Illuminate\Http\UploadedFile|\SplFileInfo|string $image + * @return string + * @throws \ErrorException If file copy fails. + */ + public static function storeImage(string $table, $image): string + { + /** @var \Illuminate\Http\UploadedFile $image */ + if (\is_object($image) and get_class($image) === UploadedFile::class) { + $newPath = self::prepareMoveFile($table, $image->getClientOriginalName()); + $newPath = str_replace(storage_path('app'), '', $newPath); + $image->storePubliclyAs( + pathinfo($newPath, \PATHINFO_DIRNAME), + pathinfo($newPath, \PATHINFO_BASENAME) + ); + return 'storage' . str_replace('/public', '', $newPath); + } + /** @var \SplFileInfo $image */ + if (\is_object($image) and get_class($image) === \SplFileInfo::class) { + $newPath = self::prepareMoveFile($table, $image->getBaseName()); + if (!File::copy($image->getRealPath(), $newPath)) { + throw new \ErrorException(sprintf( + 'Failed to copy file %s to %s', + $image->getRealPath(), + $newPath + )); + } + return \ltrim(str_replace(\storage_path('app/public'), '/storage', $newPath), '/'); + } + if (\is_string($image)) { + return $image; + } + } + + /** + * Prepare storage path and return + * the image full path + * + * @param string $table Model table name. + * @param string $filename The basename for the file. + * @return string + */ + private static function prepareMoveFile(string $table, string $filename): string + { + $pathFolder = \storage_path(sprintf('app/public/images/%s', $table)); + if (!File::exists($pathFolder)) { + File::makeDirectory($pathFolder, 0755, true); + } + $filename = sprintf('%s-%s', Str::uuid(), $filename); + $fullPath = sprintf('%s/%s', $pathFolder, $filename); + if (File::exists($fullPath)) { + File::delete($fullPath); + } + return $fullPath; + } } diff --git a/app/Mail/TicketMention.php b/app/Mail/TicketMention.php index c131173..e45f258 100644 --- a/app/Mail/TicketMention.php +++ b/app/Mail/TicketMention.php @@ -2,7 +2,7 @@ namespace App\Mail; -use App\Lib\Utils; +use App\Lib\ToolboxHelper; use App\Models\Ticket; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; @@ -38,7 +38,7 @@ class TicketMention extends Mailable public function build() { $ticket = $this->ticket; - $corpsMaj = Utils::plainToHtml($this->corpsMaj); + $corpsMaj = ToolboxHelper::plainToHtml($this->corpsMaj); return $this->subject("[SupportWeb] Vous avez été mentionné sur le ticket n° $ticket->id") ->view('emails.ticket-mention', compact('ticket', 'corpsMaj')); } diff --git a/app/Mail/TicketUpdate.php b/app/Mail/TicketUpdate.php index 0b2bdc2..3fb5d83 100644 --- a/app/Mail/TicketUpdate.php +++ b/app/Mail/TicketUpdate.php @@ -2,7 +2,7 @@ namespace App\Mail; -use App\Lib\Utils; +use App\Lib\ToolboxHelper; use App\Models\Ticket; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; @@ -38,7 +38,7 @@ class TicketUpdate extends Mailable public function build() { $ticket = $this->ticket; - $corpsMaj = \html_entity_decode(Utils::plainToHtml($this->corpsMaj)); + $corpsMaj = \html_entity_decode(ToolboxHelper::plainToHtml($this->corpsMaj)); return $this->subject("[SupportWeb][{$ticket->site->name}] Ticket n°$ticket->id") ->view('emails.ticket-update', compact('ticket', 'corpsMaj')); } diff --git a/app/Models/Message.php b/app/Models/Message.php index 170b601..be98669 100644 --- a/app/Models/Message.php +++ b/app/Models/Message.php @@ -6,16 +6,25 @@ use App\Events\NotifySubscriptions; use App\Events\NotifyTicketCreator; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Http\UploadedFile; class Message extends Model { use HasFactory; + /** + * The attributes that are mass assignable. + * + * @var array + */ protected $fillable = [ 'message' ]; + /** + * The attributes that are mass assignable. + * + * @return void + */ protected static function booted() { static::created(function (Message $message) { @@ -50,7 +59,7 @@ class Message extends Model // https://supportweb.ldc.fr/ticket/583/message/create // SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '583-8' for key 'PRIMARY' // $ticket->subscriptions()->attach($messageUser); - // Add subscription + // Add subscription. if (!$messageUser->subscriptions()->find($ticket->id)) { $messageUser->subscriptions()->attach($ticket->id); \session()->flash('info', trans('Vous avez été inscrit.')); @@ -69,28 +78,25 @@ class Message extends Model }); } + // * RELATIONSHIPS + + /** + * BelongsTo Ticket relation + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ public function ticket() { return $this->belongsTo(Ticket::class, 'ticket_id'); } + /** + * BelongsTo User relation + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ public function user() { return $this->belongsTo(User::class, 'user_id'); } - - public function saveFile(Site $site, Ticket $ticket, UploadedFile $file) - { - $siteAssetFullPath = Site::ticketAssetsFullPath($site->slug_name); - $fileAssetPath = Site::ticketAssetsPath($site->slug_name); - $fileName = sprintf( - '%s-%s-%d-%d.%s', - $site->slug_name, - $ticket->id, - rand(), - $file->extension() - ); - $file->move(sprintf('%s/messages/%d', $siteAssetFullPath, $ticket->id), $fileName); - return sprintf('%s/%d/%s', $fileAssetPath, $ticket->id, $fileName); - } } diff --git a/app/Models/Site.php b/app/Models/Site.php index c0bb1e6..dba17f9 100644 --- a/app/Models/Site.php +++ b/app/Models/Site.php @@ -18,20 +18,45 @@ class Site extends Model use IsCrudModel; use SoftDeletes; + /** + * Crud Label + * + * @var string + */ protected $crudLabelColumn = 'name'; + /** + * Crud Value + * + * @var string + */ protected $crudValueColumn = 'id'; + /** + * The attributes that are mass assignable. + * + * @var array + */ protected $fillable = [ 'name', 'status', 'description', 'todo' ]; + /** + * The model's attributes. + * + * @var array + */ protected $attributes = [ 'status' => '', ]; - public function __construct($attributes = []) + /** + * Site + * + * @param array $attributes + */ + public function __construct(array $attributes = []) { self::$editableProperties = [ 'name' => [ @@ -142,6 +167,11 @@ class Site extends Model parent::__construct($attributes); } + /** + * Perform any actions required after the model boots. + * + * @return void + */ protected static function booted() { static::created(function (Site $site) { @@ -171,32 +201,67 @@ class Site extends Model }); } + // * RELATIONSHIPS + + /** + * HasMany Message relation + * The users of type client that would have access to this site tickets + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ public function users() { return $this->belongsToMany(User::class); } + /** + * HasMany Ticket relation + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ public function tickets() { return $this->hasMany(Ticket::class, 'site_id'); } + /** + * BelongsToMany Project relation + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ public function projects() { return $this->belongsToMany(Project::class); } + /** + * MorphMany Log relation + * + * @return \Illuminate\Database\Eloquent\Relations\MorphMany + */ public function logs() { return $this->morphMany(Log::class, 'parent'); } + /** + * MorphMany File relation + * + * @return \Illuminate\Database\Eloquent\Relations\MorphMany + */ public function files() { return $this->morphMany(File::class, 'parent'); } - public function getSlugNameAttribute() + // * METHODS + + /** + * Getter slugName + * + * @return string + */ + public function getSlugNameAttribute(): string { return Str::slug($this->name); } diff --git a/app/Models/Ticket.php b/app/Models/Ticket.php index 219d0ba..b1957e0 100644 --- a/app/Models/Ticket.php +++ b/app/Models/Ticket.php @@ -13,7 +13,6 @@ use App\Mail\TicketMention; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\ModelNotFoundException; -use Illuminate\Http\UploadedFile; use Illuminate\Support\Collection; use Kwaadpepper\Enum\Traits\CastsEnums; @@ -22,11 +21,30 @@ class Ticket extends Model use HasFactory; use CastsEnums; + /** + * The attributes that should be cast to Enums. + * + * @var array + */ protected $enumCasts = [ 'type' => TicketType::class, 'status' => TicketStatus::class ]; + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'is_internal' => 'bool' + ]; + + /** + * The attributes that are mass assignable. + * + * @var array + */ protected $fillable = [ 'site_id', 'type', @@ -35,10 +53,15 @@ class Ticket extends Model 'is_internal' ]; + /** + * Perform any actions required after the model boots. + * + * @return void + */ protected static function booted() { static::creating(function (Ticket $ticket) { - // If evolution force the ticket status on estimateTodo + // If evolution force the ticket status on estimateTodo. if ($ticket->type->equals(TicketType::evolution())) { $ticket->status = TicketStatus::estimateTodo(); } @@ -64,41 +87,51 @@ class Ticket extends Model }); } + // * RELATIONSHIPS + + /** + * BelongsTo Site relation + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ public function site() { return $this->belongsTo(Site::class); } + /** + * BelongsTo User relation + * The user that created the ticket + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ public function user() { return $this->belongsTo(User::class, 'created_by'); } + /** + * HasMany Message relation + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ public function messages() { return $this->hasMany(Message::class, 'ticket_id'); } + /** + * BelongsToMany User relation + * The Users that are following this ticket + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ public function subscriptions() { return $this->belongsToMany(User::class, 'subscriptions'); } - public static function saveFile(Site $site, Ticket $ticket, TicketType $type, UploadedFile $file) - { - $siteAssetFullPath = Site::ticketAssetsFullPath($site->slug_name); - $fileAssetPath = Site::ticketAssetsPath($site->slug_name); - $fileName = sprintf( - '%s-%s-%d-%d.%s', - $site->slug_name, - $type->getDefinition(), - $ticket->id, - rand(), - $file->extension() - ); - $file->move(sprintf('%s/%d', $siteAssetFullPath, $ticket->id), $fileName); - return sprintf('%s/%d/%s', $fileAssetPath, $ticket->id, $fileName); - } + // * METHODS /** * Parse message @user tags and send emails to concerned users @@ -110,7 +143,7 @@ class Ticket extends Model public function sendBeenMentionnedToUser(Message $message): Collection { $ticket = $this; - // find @user + send mail si un seul user est trouvé + // Find @user + send mail si un seul user est trouvé. $usersNotified = collect([]); $matches = []; $ticket->load('site'); @@ -128,7 +161,7 @@ class Ticket extends Model ->orWhere('role', Role::developper()->value); })->firstOrFail(); - if ($ticket->is_internal === 1) { + if ($ticket->is_internal) { if ($user->isAdmin()) { $usersNotified->add($user); SendTicketMentionMail::dispatchAfterResponse( diff --git a/config/app.php b/config/app.php index f0559ff..b2c68ba 100644 --- a/config/app.php +++ b/config/app.php @@ -233,7 +233,7 @@ return [ // * Application custom 'TicketType' => App\Enums\TicketType::class, 'TicketStatus' => App\Enums\TicketStatus::class, - 'Utils' => App\Lib\Utils::class, + 'ToolboxHelper' => App\Lib\ToolboxHelper::class, 'CrudAction' => App\Enums\CrudAction::class, 'CrudType' => App\Enums\CrudType::class, 'Role' => App\Enums\Role::class, diff --git a/database/migrations/2022_05_24_122728_move_images_to_storage.php b/database/migrations/2022_05_24_122728_move_images_to_storage.php new file mode 100644 index 0000000..c404e2d --- /dev/null +++ b/database/migrations/2022_05_24_122728_move_images_to_storage.php @@ -0,0 +1,59 @@ +map(function (Ticket $ticket) { + if ($ticket->file) { + $ticket->file = "storage/{$ticket->file}"; + } + if ($ticket->file2) { + $ticket->file2 = "storage/{$ticket->file2}"; + } + $ticket->saveQuietly(); + }); + Message::all()->map(function (Message $message) { + if ($message->file) { + $message->file = "storage/{$message->file}"; + } + $message->saveQuietly(); + }); + ModelsFile::all()->map(function (ModelsFile $fichier) { + if ($fichier->file) { + $fichier->file = "storage/{$fichier->file}"; + } + $fichier->saveQuietly(); + }); + } //end if + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('storage', function (Blueprint $table) { + }); + } +} diff --git a/resources/views/modules/loadLevel.blade.php b/resources/views/modules/loadLevel.blade.php index e5e9987..0e446fd 100644 --- a/resources/views/modules/loadLevel.blade.php +++ b/resources/views/modules/loadLevel.blade.php @@ -1,16 +1,16 @@ {{ __('Charge') }} :  - @switch(Utils::computeWorkload()) + @switch(ToolboxHelper::computeWorkload()) @case(LoadLevel::weak()) - {{ Utils::computeWorkload()->label }} + {{ ToolboxHelper::computeWorkload()->label }} @break @case(LoadLevel::stable()) - {{ Utils::computeWorkload()->label }} + {{ ToolboxHelper::computeWorkload()->label }} @break @case(LoadLevel::high()) - {{ Utils::computeWorkload()->label }} + {{ ToolboxHelper::computeWorkload()->label }} @break @case(LoadLevel::overload()) - {{ Utils::computeWorkload()->label }} + {{ ToolboxHelper::computeWorkload()->label }} @break @endswitch diff --git a/resources/views/ticket/create.blade.php b/resources/views/ticket/create.blade.php index b32cd72..e6456ec 100644 --- a/resources/views/ticket/create.blade.php +++ b/resources/views/ticket/create.blade.php @@ -4,13 +4,11 @@ @section('content')
-
Créer un ticketRetour
+
{{ __('Créer un ticket') }}{{ __('Retour') }}
@csrf - @include('ticket.form-inputs') -
diff --git a/resources/views/ticket/edit.blade.php b/resources/views/ticket/edit.blade.php index 7462cbe..4bb5a41 100644 --- a/resources/views/ticket/edit.blade.php +++ b/resources/views/ticket/edit.blade.php @@ -4,17 +4,12 @@ @section('content')
-
Modifier un ticketRetour
+
{{ __('Modifier un ticket') }}{{ __('Retour') }}
@csrf @method('PUT') - @if (session('success')) -
{!! session('success') !!}
- @endif - @include('ticket.form-inputs') -
diff --git a/resources/views/ticket/form-inputs.blade.php b/resources/views/ticket/form-inputs.blade.php index 53d7bfb..098f878 100644 --- a/resources/views/ticket/form-inputs.blade.php +++ b/resources/views/ticket/form-inputs.blade.php @@ -3,7 +3,7 @@ $userPr = Auth::user()->sites ?? collect([]); @endphp
- + is_internal === 1)) checked @endif> + is_internal)) checked @endif>
-@endcan \ No newline at end of file +@endcan diff --git a/resources/views/ticket/index.blade.php b/resources/views/ticket/index.blade.php index 94b19e7..e6203ce 100644 --- a/resources/views/ticket/index.blade.php +++ b/resources/views/ticket/index.blade.php @@ -45,8 +45,8 @@ $userPr = Auth::user()->sites ?? collect([]);
- - + +
@@ -120,7 +120,7 @@ $userPr = Auth::user()->sites ?? collect([]); {{ $ticket->id }} - @if ($ticket->is_internal === 1) + @if ($ticket->is_internal) 🔒 @endif @@ -157,7 +157,7 @@ $userPr = Auth::user()->sites ?? collect([]); {{ $ticket->site->name }} - {{__('Ticket n°') }} {{ $ticket->id }} - @if ($ticket->is_internal === 1) + @if ($ticket->is_internal) 🔒 @endif  -  diff --git a/resources/views/ticket/show.blade.php b/resources/views/ticket/show.blade.php index 63583c4..74797a9 100644 --- a/resources/views/ticket/show.blade.php +++ b/resources/views/ticket/show.blade.php @@ -3,31 +3,13 @@ @section('content')

- @php - $back = route('ticket.index'); - $url = parse_url(url()->previous()); - if ( - ($url['host'] ?? '') === parse_url(config('app.url'), PHP_URL_HOST) and - ($url['path'] ?? '') === '/ticket' - ) { - $queries = explode('&', $url['query'] ?? ''); - foreach($queries as $q) { - $q = explode('=', $q); - if(($q[0] ?? '') === 'pr' and ctype_digit(($q[1] ?? ''))) { - $back = route('ticket.index', ['pr' => $q[1]]); - } - } - } - @endphp - ← + ← {{ __('Ticket n° :ticketId : :ticketSiteName - :ticketTypeLabel', [ 'ticketId' => $ticket->id, 'ticketSiteName' => $ticket->site->name, 'ticketTypeLabel' => $ticket->type->label ]) }} - @if ($ticket->is_internal === 1) - 🔒 - @endif + @if ($ticket->is_internal)🔒@endif {{ $ticket->status->label }}

@@ -35,7 +17,7 @@
@csrf
- +
@@ -43,7 +25,7 @@ @can('changeStatus', $ticket)
@csrf - @foreach(TicketStatus::toArray() as $ticketStatus) @endforeach @@ -120,9 +102,9 @@
@else
- {!! Utils::plainToHtml($message->message) !!} + {!! ToolboxHelper::plainToHtml($message->message) !!} @if ($message->file !== null) -
file))data-fslightbox="gallery" @endif href="{{ asset($message->file) }}" target="_blank">Ouvrir la pièce jointe
+
file))data-fslightbox="gallery" @endif href="{{ asset($message->file) }}" target="_blank">Ouvrir la pièce jointe
@endif

{!! __('Créé le :date par :userName', [ @@ -172,7 +154,7 @@

{{__('Description') }}

-

{!! clean(Utils::plainToHtml($ticket->text)) !!}

+

{!! clean(ToolboxHelper::plainToHtml($ticket->text)) !!}

{!! __('Créé le :date par :userName', [ 'date' => \Carbon\Carbon::parse($ticket->created_at)->format('d/m/Y à H:i'), 'userName' => $ticket->user ? $ticket->user->name : __('Utilisateur supprimé') @@ -186,13 +168,13 @@ @if (!is_null($ticket->file))
  • {{ __('Pièce jointe n°1') }} : - file))data-fslightbox="gallery" @endif href="{{ asset($ticket->file) }}" target="_blank">{{ __('Ouvrir la pièce jointe') }} + file))data-fslightbox="gallery" @endif href="{{ asset($ticket->file) }}" target="_blank">{{ __('Ouvrir la pièce jointe') }}
  • @endif @if (!is_null($ticket->file2))
  • {{ __('Pièce jointe n°2') }} : - file2))data-fslightbox="gallery" @endif href="{{ asset($ticket->file2) }}" target="_blank">{{ __('Ouvrir la pièce jointe') }} + file2))data-fslightbox="gallery" @endif href="{{ asset($ticket->file2) }}" target="_blank">{{ __('Ouvrir la pièce jointe') }}
  • @endif