{{-- Row actions for the Booking Types DataTable — docs/wireframes-v12/booking-type.html (~line 144):
`.action-btns` and `.btn-icon-sm` are this page's own names, not the projects listing's `.action-group` / `.btn-icon`; both pairs are declared in the wireframe stylesheet and the wireframe uses this one here. Rendered by BookingTypesDataTable through rawColumns(['actions']), **so this output is injected into the page unescaped** and the rule projects/partials/actions.blade.php states applies: nothing user-controlled may reach it except through an escaped attribute or Js::from(). There are exactly two such places and both are covered — * `data-row`, one JSON attribute written with `{{ }}`, which escapes the JSON for an HTML attribute. nrems-master-modals.js reads it with JSON.parse and writes the name into the input through `.value`, so a booking type called `` is a string on the way in, a string in the attribute and a string in the field. * the delete confirmation, through Js::from() — `{{ }}` escapes for HTML and not for Javascript, and commit 3492ec1 is the stored XSS that proved the difference. What the payload holds, and why: id which row a rejected edit belongs to (the hidden reopen_id). name the Edit modal's prefill. url where the Edit form should PUT. **Generated server-side** by route(), so the client never builds a path out of an id — the module's JS has no route table and cannot acquire one. ## Delete is a POST form, never a link A GET that destroys data is triggered by any crawler or prefetch. The confirm() guard is the wireframe-free option — no modal, no new JS — and is the same affordance the projects listing uses, class for class apart from the icon-button name this page draws. The delete may still be refused on the server: a booking type in use by a live customer doc type cannot be deleted (CR-7 §3.1). The button does not try to know that — a listing cell cannot be trusted with an integrity rule, and the controller answers with an error flash either way. Every control is gated on its own ability, so a view-only holder of `booking_types.view` gets an empty cell rather than two buttons that 403. yajra hands the row's model in as $model. --}} @php /** @var \App\Models\BookingType $model */ $row = [ 'id' => $model->getKey(), 'name' => $model->name, 'url' => route('booking_types.update', $model), ]; // JSON_INVALID_UTF8_SUBSTITUTE, and JSON_THROW_ON_ERROR behind it. // // A name is bytes somebody typed. MySQL will hold an invalid UTF-8 sequence // in a utf8mb4 column if it arrives as one, and json_encode's DEFAULT answer // to that is to return `false` — which Blade renders as `data-row=""`, which // JSON.parse rejects, which leaves the pencil silently dead with nothing on // screen or in a log to say why. Silent is the worst of the three outcomes. // // SUBSTITUTE replaces the bad bytes with U+FFFD, so the dialog opens and the // user sees a replacement character in a name that already had one. THROW is // kept underneath it for every OTHER encoding failure (recursion, INF/NAN — // none reachable from these three keys today), where a 500 naming the real // fault beats a dead button. Chosen over a bare THROW because this partial // renders once per row of a listing: one hostile byte in one name would // otherwise take the whole page down rather than one row's edit button. $rowJson = json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_THROW_ON_ERROR); @endphp
@can('update', $model) @endcan @can('delete', $model) {{-- CR-16 item J2: a plain button, not a form with a confirm(). `data-url` is the destroy route rendered by route() — the client's own instruction — and `data-confirm` is the SAME lang sentence the confirm() showed, which the shared dialog writes with textContent. The masters' own `.btn-icon-sm` is kept: each partial keeps its own wrapper and icon classes. A type that is in use still refuses, now as 422 JSON carrying its own `trans_choice` sentence (item J7). --}} @endcan