diff --git a/Modules/Helpdesk/Database/Migrations/2026_06_17_084500_add_lotus_replication_fields_to_helpdesk_comments.php b/Modules/Helpdesk/Database/Migrations/2026_06_17_084500_add_lotus_replication_fields_to_helpdesk_comments.php new file mode 100644 index 00000000..6103ad88 --- /dev/null +++ b/Modules/Helpdesk/Database/Migrations/2026_06_17_084500_add_lotus_replication_fields_to_helpdesk_comments.php @@ -0,0 +1,132 @@ +uuid('guid')->nullable()->after('id'); + } + + if (! Schema::hasColumn('helpdesk_comments', 'unid')) { + $table->string('unid', 64)->nullable()->after('guid'); + } + + if (! Schema::hasColumn('helpdesk_comments', 'replica_id')) { + $table->string('replica_id', 64)->nullable()->after('unid'); + } + + if (! Schema::hasColumn('helpdesk_comments', 'parent_unid')) { + $table->string('parent_unid', 64)->nullable()->after('parent_id'); + } + + if (! Schema::hasColumn('helpdesk_comments', 'root_unid')) { + $table->string('root_unid', 64)->nullable()->after('parent_unid'); + } + + if (! Schema::hasColumn('helpdesk_comments', 'originator_id')) { + $table->string('originator_id', 96)->nullable()->after('root_unid'); + } + + if (! Schema::hasColumn('helpdesk_comments', 'sync_sequence')) { + $table->unsignedBigInteger('sync_sequence')->default(1)->after('originator_id'); + } + + if (! Schema::hasColumn('helpdesk_comments', 'sync_hash')) { + $table->string('sync_hash', 128)->nullable()->after('sync_sequence'); + } + + if (! Schema::hasColumn('helpdesk_comments', 'source_system')) { + $table->string('source_system', 80)->default('wms-helpdesk')->after('sync_hash'); + } + + if (! Schema::hasColumn('helpdesk_comments', 'source_record_id')) { + $table->string('source_record_id', 120)->nullable()->after('source_system'); + } + + if (! Schema::hasColumn('helpdesk_comments', 'body_text')) { + $table->text('body_text')->nullable()->after('body'); + } + + if (! Schema::hasColumn('helpdesk_comments', 'replicated_at')) { + $table->timestamp('replicated_at')->nullable()->after('updated_at'); + } + + if (! Schema::hasColumn('helpdesk_comments', 'synced_at')) { + $table->timestamp('synced_at')->nullable()->after('replicated_at'); + } + + if (! Schema::hasColumn('helpdesk_comments', 'tombstoned_at')) { + $table->timestamp('tombstoned_at')->nullable()->after('synced_at'); + } + }); + + Comment::query()->orderBy('id')->chunkById(100, static function ($comments): void { + foreach ($comments as $comment) { + $guid = $comment->guid ?: (string) Str::uuid(); + $unid = $comment->unid ?: strtoupper(hash('sha256', $guid.'|'.$comment->ticket_id.'|'.$comment->id)); + $parent = $comment->parent_id ? Comment::query()->find($comment->parent_id) : null; + $parentUnid = $parent?->unid; + $rootUnid = $parent?->root_unid ?: $parentUnid ?: $unid; + + DB::table('helpdesk_comments') + ->where('id', $comment->id) + ->update([ + 'guid' => $guid, + 'unid' => $unid, + 'parent_unid' => $parentUnid, + 'root_unid' => $rootUnid, + 'originator_id' => $comment->originator_id ?: $unid.':1', + 'sync_sequence' => $comment->sync_sequence ?: 1, + 'sync_hash' => hash('sha256', (string) $comment->body), + 'body_text' => trim(strip_tags((string) $comment->body)), + 'source_system' => $comment->source_system ?: 'wms-helpdesk', + ]); + } + }); + + Schema::table('helpdesk_comments', static function (Blueprint $table): void { + $table->unique('guid', 'helpdesk_comments_guid_unique'); + $table->unique('unid', 'helpdesk_comments_unid_unique'); + $table->index(['ticket_id', 'root_unid', 'parent_unid'], 'helpdesk_comments_lotus_thread_index'); + $table->index(['ticket_id', 'is_internal', 'created_at'], 'helpdesk_comments_visibility_index'); + }); + } + + public function down(): void + { + Schema::table('helpdesk_comments', static function (Blueprint $table): void { + $table->dropUnique('helpdesk_comments_guid_unique'); + $table->dropUnique('helpdesk_comments_unid_unique'); + $table->dropIndex('helpdesk_comments_lotus_thread_index'); + $table->dropIndex('helpdesk_comments_visibility_index'); + + $table->dropColumn([ + 'guid', + 'unid', + 'replica_id', + 'parent_unid', + 'root_unid', + 'originator_id', + 'sync_sequence', + 'sync_hash', + 'source_system', + 'source_record_id', + 'body_text', + 'replicated_at', + 'synced_at', + 'tombstoned_at', + ]); + }); + } +}; diff --git a/Modules/Helpdesk/Http/Controllers/TicketCommentStoreController.php b/Modules/Helpdesk/Http/Controllers/TicketCommentStoreController.php index 54f6c886..18a15f8e 100644 --- a/Modules/Helpdesk/Http/Controllers/TicketCommentStoreController.php +++ b/Modules/Helpdesk/Http/Controllers/TicketCommentStoreController.php @@ -9,6 +9,7 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Str; use Illuminate\Validation\Rule; use Modules\Helpdesk\Models\CannedResponse; use Modules\Helpdesk\Models\Ticket; @@ -56,6 +57,8 @@ final class TicketCommentStoreController extends Controller $body = trim($renderer->render($response->body, $ticket, $user)); } + $body = $this->normaliseHtmlBody($body); + validator(['commentBody' => $body], [ 'commentBody' => ['required', 'string', 'min:2'], ])->validate(); @@ -67,7 +70,7 @@ final class TicketCommentStoreController extends Controller ticket: $ticket, user: $user, body: $body, - internal: (bool) ($validated['internal'] ?? false), + internal: (bool) ($validated['internal'] ?? false) && $this->userCanCreateInternalNote($user), files: $uploadedFiles, parentId: filled($validated['parent_id'] ?? null) ? (int) $validated['parent_id'] : null, ); @@ -76,4 +79,31 @@ final class TicketCommentStoreController extends Controller ->route('helpdesk.tickets.show', $ticket) ->with('ticket-comment-added', filled($validated['parent_id'] ?? null) ? 'Reply posted.' : 'Comment posted.'); } + /** + * Keep supported formatting tags but strip scripts and unsafe inline handlers. + */ + private function normaliseHtmlBody(string $body): string + { + $body = preg_replace('/<\s*(script|style)[^>]*>.*?<\s*\/\s*\1\s*>/is', '', $body) ?: ''; + $body = preg_replace('/\s+on[a-z]+\s*=\s*("[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $body) ?: ''; + $body = preg_replace('/javascript\s*:/i', '', $body) ?: ''; + + return trim(strip_tags($body, '