From ce7a56d00beeb96d15f7d931ed60f4a96da51065 Mon Sep 17 00:00:00 2001 From: Alessandro Costa de Oliveira Date: Mon, 31 Aug 2026 12:46:22 -0300 Subject: [PATCH 1/3] =?UTF-8?q?Escapa=20texto=20em=20aspas=20simples=20em?= =?UTF-8?q?=20links=20nos=20coment=C3=A1rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chamados/show/card-comentarios-user.blade.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/resources/views/chamados/show/card-comentarios-user.blade.php b/resources/views/chamados/show/card-comentarios-user.blade.php index 15083a9f..87653366 100644 --- a/resources/views/chamados/show/card-comentarios-user.blade.php +++ b/resources/views/chamados/show/card-comentarios-user.blade.php @@ -48,6 +48,12 @@ function validURL(str) { '(\\#[-a-z\\d_]*)?$','i'); // fragment locator return !!pattern.test(str); } + + function hasOddSingleQuotes(str) { + var quotes = str.match(/'/g); + return quotes !== null && quotes.length % 2 === 1; + } + /* Procura palavra por palavra formato de url e converte em tag html */ $(function() { $("#comentario").on("blur", function() { @@ -55,17 +61,22 @@ function validURL(str) { var text1 = text.replace(/(?:\r\n|\r|\n)/g, '
'); var array = text1.trim().split(/\s+/); var html = ''; + var insideSingleQuotes = false; for (var i = 0; i < array.length; i++) { - if (validURL(array[i])) { + var ignoreLink = insideSingleQuotes || array[i].indexOf("'") !== -1; + if (!ignoreLink && validURL(array[i])) { if (array[i].search('http://') === -1 && array[i].search('https://') === -1) { array[i] = "http://" + array[i]; } array[i] = "" + array[i] + ""; } + if (hasOddSingleQuotes(array[i])) { + insideSingleQuotes = !insideSingleQuotes; + } html += array[i] + " "; } $(this).val(html); }) }) -@endsection \ No newline at end of file +@endsection From 86ee1da53427e40ed7329ad3538b71e01e7404aa Mon Sep 17 00:00:00 2001 From: Alessandro Costa de Oliveira Date: Mon, 31 Aug 2026 12:49:34 -0300 Subject: [PATCH 2/3] =?UTF-8?q?Nos=20links=20nos=20coment=C3=A1rios=20cons?= =?UTF-8?q?iderar=20https=20quando=20n=C3=A3o=20informado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/views/chamados/show/card-comentarios-user.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/chamados/show/card-comentarios-user.blade.php b/resources/views/chamados/show/card-comentarios-user.blade.php index 87653366..8e850193 100644 --- a/resources/views/chamados/show/card-comentarios-user.blade.php +++ b/resources/views/chamados/show/card-comentarios-user.blade.php @@ -66,9 +66,9 @@ function hasOddSingleQuotes(str) { var ignoreLink = insideSingleQuotes || array[i].indexOf("'") !== -1; if (!ignoreLink && validURL(array[i])) { if (array[i].search('http://') === -1 && array[i].search('https://') === -1) { - array[i] = "http://" + array[i]; + array[i] = "https://" + array[i]; } - array[i] = "" + array[i] + ""; + array[i] = "" + array[i] + ""; } if (hasOddSingleQuotes(array[i])) { insideSingleQuotes = !insideSingleQuotes; From a31566f98b99d3aa89548a2cc49e52bc07727ae2 Mon Sep 17 00:00:00 2001 From: Alessandro Costa de Oliveira Date: Mon, 31 Aug 2026 13:39:12 -0300 Subject: [PATCH 3/3] =?UTF-8?q?Permite=20que=20usu=C3=A1rio=20edite=20seus?= =?UTF-8?q?=20coment=C3=A1rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 5 +- app/Http/Controllers/ComentarioController.php | 41 ++++++ app/Http/Controllers/FilaController.php | 4 + app/Models/Comentario.php | 28 ++++ app/Models/Fila.php | 6 + config/chamados.php | 1 + config/filas.php | 2 + readme.md | 2 + .../show/card-comentarios-user.blade.php | 42 +++++- .../show/comentarios-add-modal.blade.php | 3 + .../filas/partials/config-card.blade.php | 31 ++++ routes/web.php | 1 + tests/Unit/ComentarioTest.php | 133 ++++++++++++++++++ 13 files changed, 296 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/ComentarioTest.php diff --git a/.env.example b/.env.example index 77d798d8..4591aa9c 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,10 @@ APP_URL=http://chamados # Definir o parâmetro em MB, o padrão aqui é 16MB (se não estiver definido valor algum) UPLOAD_MAX_FILESIZE=16 +# Timeout padrão em horas para edição de comentários próprios nas filas que habilitarem essa opção. +# Cada fila pode sobrescrever esse valor nas suas configurações. Use 0 para permitir edição sem limite de tempo. +EDITAR_COMENTARIOS_TIMEOUT_HORAS=12 + # Se a aplicação está atrás de um proxy que conecta com o usuário por https # mas conecta com a aplicação por http, você deve habilitar essa flag ######## Deprecado em favor de LARAVEL_TOOLS_FORCAR_HTTPS (2/2023) ############ @@ -220,4 +224,3 @@ WS_FOTO_FAKE_PATH= DUSK_DRIVER_URL='http://selenium:4444/wd/hub' DUSK_START_MAXIMIZED=true DUSK_HEADLESS_DISABLED=true - diff --git a/app/Http/Controllers/ComentarioController.php b/app/Http/Controllers/ComentarioController.php index b09901b6..e9e40db3 100644 --- a/app/Http/Controllers/ComentarioController.php +++ b/app/Http/Controllers/ComentarioController.php @@ -6,6 +6,7 @@ use App\Models\Comentario; use Carbon\Carbon; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; class ComentarioController extends Controller { @@ -59,4 +60,44 @@ public function store(Request $request, Chamado $chamado) $request->session()->flash('alert-info', 'Comentário enviado com sucesso'); return redirect("chamados/$chamado->id"); } + + public function update(Request $request, Comentario $comentario) + { + $this->authorize('chamados.view', $comentario->chamado); + + if (!$comentario->podeSerEditadoPor(\Auth::user())) { + abort(403); + } + + $request->validate([ + 'comentario' => ['required'], + ]); + + $comentarioOriginal = $comentario->comentario; + $comentarioEditado = $request->comentario; + + if ($comentarioOriginal == $comentarioEditado) { + $request->session()->flash('alert-info', 'Nenhuma alteração realizada'); + return redirect("chamados/{$comentario->chamado->id}"); + } + + $comentario->comentario = $comentarioEditado; + $comentario->save(); + + Log::info('Comentário editado', [ + 'comentario_id' => $comentario->id, + 'chamado_id' => $comentario->chamado_id, + 'user_id' => \Auth::user()->id, + 'comentario_original' => $comentarioOriginal, + 'comentario_editado' => $comentarioEditado, + ]); + + Comentario::criarSystem( + $comentario->chamado, + 'O comentário de ' . e($comentario->user->name) . ' foi editado.' + ); + + $request->session()->flash('alert-info', 'Comentário editado com sucesso'); + return redirect("chamados/{$comentario->chamado->id}"); + } } diff --git a/app/Http/Controllers/FilaController.php b/app/Http/Controllers/FilaController.php index 6090a987..4bebf29d 100644 --- a/app/Http/Controllers/FilaController.php +++ b/app/Http/Controllers/FilaController.php @@ -104,6 +104,10 @@ public function update(Request $request, Fila $fila) } if ($request->card == 'config') { + $request->validate([ + 'config.editar_comentarios_timeout_horas' => 'required|integer|min:0', + ]); + $settings = $request->settings; $fila->settings()->set('instrucoes', $settings['instrucoes']); diff --git a/app/Models/Comentario.php b/app/Models/Comentario.php index c8303ac4..ff8d144b 100644 --- a/app/Models/Comentario.php +++ b/app/Models/Comentario.php @@ -5,6 +5,7 @@ use App\Models\Chamado; use App\Models\User; use App\Observers\ComentarioObserver; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -51,6 +52,33 @@ public static function tipos() return ['user', 'system']; } + public function podeSerEditadoPor(User $user) + { + if ($this->tipo != 'user') { + return false; + } + + if ($this->user_id != $user->id) { + return false; + } + + if (!$this->chamado->fila->config->editar_comentarios) { + return false; + } + + $timeout = $this->chamado->fila->config->editar_comentarios_timeout_horas; + if ($timeout == 0) { + return true; + } + + return $this->created_at->greaterThanOrEqualTo(Carbon::now()->subHours($timeout)); + } + + public function foiEditado() + { + return $this->created_at && $this->updated_at && $this->updated_at->greaterThan($this->created_at); + } + /** * relacionamento com user */ diff --git a/app/Models/Fila.php b/app/Models/Fila.php index 161ebc78..31271875 100644 --- a/app/Models/Fila.php +++ b/app/Models/Fila.php @@ -190,6 +190,9 @@ public function getConfigAttribute($value) $out = new \StdClass; $out->triagem = $value->triagem ?? config('filas.config.triagem'); $out->patrimonio = $value->patrimonio ?? config('filas.config.patrimonio'); + $out->editar_comentarios = $value->editar_comentarios ?? config('filas.config.editar_comentarios'); + $out->editar_comentarios_timeout_horas = $value->editar_comentarios_timeout_horas + ?? config('filas.config.editar_comentarios_timeout_horas'); $out->visibilidade = $v; $out->status = $value->status ?? config('filas.config.status'); return $out; @@ -219,6 +222,9 @@ public function setConfigAttribute($value) $config = new \StdClass; $config->triagem = $value['triagem']; $config->patrimonio = $value['patrimonio']; + $config->editar_comentarios = $value['editar_comentarios'] ?? 0; + $config->editar_comentarios_timeout_horas = $value['editar_comentarios_timeout_horas'] + ?? config('filas.config.editar_comentarios_timeout_horas'); $config->visibilidade = $v; $config->status = $value['status']; diff --git a/config/chamados.php b/config/chamados.php index be829fd0..ab658170 100644 --- a/config/chamados.php +++ b/config/chamados.php @@ -11,6 +11,7 @@ 'usar_foto' => env('USAR_FOTO', false), 'upload_max_filesize' => (int) env('UPLOAD_MAX_FILESIZE', '16') * 1024, + 'editar_comentarios_timeout_horas' => (int) env('EDITAR_COMENTARIOS_TIMEOUT_HORAS', 12), // deprecado em 2/23. Remover no próximo release 'forcar_https' => env('FORCAR_HTTPS', false), diff --git a/config/filas.php b/config/filas.php index 0145bd6d..dfa288e2 100644 --- a/config/filas.php +++ b/config/filas.php @@ -11,6 +11,8 @@ 'setores' => 'todos', # todos ou interno ], 'patrimonio' => 0, + 'editar_comentarios' => 0, + 'editar_comentarios_timeout_horas' => config('chamados.editar_comentarios_timeout_horas'), ]; $template = '{}'; diff --git a/readme.md b/readme.md index c2485d2a..16644edf 100644 --- a/readme.md +++ b/readme.md @@ -151,6 +151,8 @@ Cadastre uma nova URL no configurador de senha única utilizando o caminho `http Há várias opções que precisam ser ajustadas nesse arquivo. Faça com atenção para não deixar passar nada. O arquivo está todo documentado. +A variável `EDITAR_COMENTARIOS_TIMEOUT_HORAS` define o timeout padrão, em horas, para edição de comentários próprios nas filas que habilitarem essa opção. Cada fila pode sobrescrever esse valor nas suas configurações; use `0` para permitir edição sem limite de tempo. + ### Popular banco de dados php artisan migrate diff --git a/resources/views/chamados/show/card-comentarios-user.blade.php b/resources/views/chamados/show/card-comentarios-user.blade.php index 8e850193..9cce7504 100644 --- a/resources/views/chamados/show/card-comentarios-user.blade.php +++ b/resources/views/chamados/show/card-comentarios-user.blade.php @@ -19,10 +19,48 @@ @forelse ($chamado->comentarios->where('tipo','user')->sortByDesc('created_at') as $comentario)
{{ $comentario->user->name }} - {{ $comentario->created_at->format('d/m/Y H:i') }} + @if ($comentario->foiEditado()) + editado + @endif + @if ($comentario->podeSerEditadoPor(Auth::user())) + + @endif
-

{!! nl2br($comentario->comentario) !!}

+

{!! nl2br($comentario->comentario) !!}

+ @if ($comentario->podeSerEditadoPor(Auth::user())) + + @endif
@empty Não há comentários @@ -56,7 +94,7 @@ function hasOddSingleQuotes(str) { /* Procura palavra por palavra formato de url e converte em tag html */ $(function() { - $("#comentario").on("blur", function() { + $("textarea[name='comentario']").on("blur", function() { var text = $(this).val(); var text1 = text.replace(/(?:\r\n|\r|\n)/g, '
'); var array = text1.trim().split(/\s+/); diff --git a/resources/views/chamados/show/comentarios-add-modal.blade.php b/resources/views/chamados/show/comentarios-add-modal.blade.php index a4570d09..64e193fb 100644 --- a/resources/views/chamados/show/comentarios-add-modal.blade.php +++ b/resources/views/chamados/show/comentarios-add-modal.blade.php @@ -25,6 +25,9 @@
+ + Aceita tags HTML básicas. URLs são convertidas automaticamente em links, exceto quando estiverem entre aspas simples. +
@if ($chamado->status != 'Fechado') diff --git a/resources/views/filas/partials/config-card.blade.php b/resources/views/filas/partials/config-card.blade.php index 157daba0..1b70daf3 100644 --- a/resources/views/filas/partials/config-card.blade.php +++ b/resources/views/filas/partials/config-card.blade.php @@ -66,6 +66,37 @@ +
+ Comentários + +
+ permitir edição pelo autor: + +
+ +
+
+ +
+ +
+ + + Use 0 para permitir edição sem limite de tempo. +
+
+
+ diff --git a/routes/web.php b/routes/web.php index 8cb871cb..17734e30 100644 --- a/routes/web.php +++ b/routes/web.php @@ -74,6 +74,7 @@ // COMENTARIOS Route::post('comentarios/{chamado}/', [ComentarioController::class, 'store']); +Route::put('comentarios/{comentario}/', [ComentarioController::class, 'update']); // ARQUIVOS Route::resource('arquivos', ArquivoController::class); diff --git a/tests/Unit/ComentarioTest.php b/tests/Unit/ComentarioTest.php new file mode 100644 index 00000000..44b78e87 --- /dev/null +++ b/tests/Unit/ComentarioTest.php @@ -0,0 +1,133 @@ + 0, + 'patrimonio' => 0, + 'editar_comentarios' => 1, + 'visibilidade' => config('filas.config.visibilidade'), + 'status' => [], + ], $filaConfig); + + $fila = new Fila(); + $fila->setRawAttributes(['config' => json_encode($config)], true); + + $chamado = new Chamado(); + $chamado->setRelation('fila', $fila); + + $comentario = new Comentario(array_merge([ + 'user_id' => 1, + 'tipo' => 'user', + 'comentario' => 'Comentário', + ], $comentarioConfig)); + $comentario->created_at = $createdAt; + $comentario->updated_at = $updatedAt; + $comentario->setRelation('chamado', $chamado); + + return $comentario; + } + + public function testPodeEditarComentarioProprioDentroDoTimeoutQuandoFilaPermite() + { + $user = new User(); + $user->id = 1; + $comentario = $this->comentario(); + + $this->assertTrue($comentario->podeSerEditadoPor($user)); + } + + public function testNaoPodeEditarQuandoFilaNaoPermite() + { + $user = new User(); + $user->id = 1; + $comentario = $this->comentario(['editar_comentarios' => 0]); + + $this->assertFalse($comentario->podeSerEditadoPor($user)); + } + + public function testNaoPodeEditarComentarioDeOutroUsuario() + { + $user = new User(); + $user->id = 2; + $comentario = $this->comentario(); + + $this->assertFalse($comentario->podeSerEditadoPor($user)); + } + + public function testNaoPodeEditarDepoisDoTimeout() + { + $user = new User(); + $user->id = 1; + $comentario = $this->comentario([], [ + 'created_at' => Carbon::now()->subHours(config('chamados.editar_comentarios_timeout_horas') + 1), + ]); + + $this->assertFalse($comentario->podeSerEditadoPor($user)); + } + + public function testTimeoutDaFilaSobrescreveTimeoutGlobal() + { + config(['chamados.editar_comentarios_timeout_horas' => 12]); + + $user = new User(); + $user->id = 1; + $comentario = $this->comentario([ + 'editar_comentarios_timeout_horas' => 24, + ], [ + 'created_at' => Carbon::now()->subHours(config('chamados.editar_comentarios_timeout_horas') + 1), + ]); + + $this->assertTrue($comentario->podeSerEditadoPor($user)); + } + + public function testTimeoutZeroPermiteEditarSempre() + { + $user = new User(); + $user->id = 1; + $comentario = $this->comentario([ + 'editar_comentarios_timeout_horas' => 0, + ], [ + 'created_at' => Carbon::now()->subYear(), + ]); + + $this->assertTrue($comentario->podeSerEditadoPor($user)); + } + + public function testComentarioNovoNaoFicaSinalizadoComoEditado() + { + $data = Carbon::now(); + $comentario = $this->comentario([], [ + 'created_at' => $data, + 'updated_at' => $data, + ]); + + $this->assertFalse($comentario->foiEditado()); + } + + public function testComentarioEditadoFicaSinalizadoComoEditado() + { + $comentario = $this->comentario([], [ + 'created_at' => Carbon::now()->subMinute(), + 'updated_at' => Carbon::now(), + ]); + + $this->assertTrue($comentario->foiEditado()); + } +}