Ir al contenido principal
Laravel, thinking fast.

Esa clave card es un hash del contenido, y existe por un fallo real:

// Identified like a choice card so dismissing one palette does not also
// dismiss the next one.
'card' => 'pal-'.substr(md5((string) json_encode($data['palettes'])), 0, 8),

Una conversación puede producir dos tarjetas de elección. Sin identidad, el frontend trata «la tarjeta» como una sola cosa —así que responder a la segunda marca también la primera como respondida, y volver a renderizar reinicia ambas.

Hashear el contenido en vez de usar un contador hace que el identificador sea estable entre renderizados. La transcripción se republica tras cada iteración y se relee entera; un identificador basado en el índice se desplazaría al insertarse mensajes, y la interfaz perdería la pista de qué tarjeta ya había atendido el usuario.

Una tarjeta que se actualiza, no cinco que se apilan

El progreso no es una serie de eventos. Es una sola cosa cambiando:

private function updateOrAddProvisioningCard(int $progress, array $state): array
{
    foreach (array_reverse(array_keys($state['messages'])) as $index) {
        if (($state['messages'][$index]['role'] ?? '') === 'status'
            && ($state['messages'][$index]['type'] ?? '') === 'provisioning') {
            $state['messages'][$index]['progress'] = $progress;

            return $state;
        }
    }

    $state['messages'][] = ['role' => 'status', 'type' => 'provisioning', 'progress' => 0 + $progress];

    return $state;
}

Busca la tarjeta existente y modifícala; sólo añade si no hay ninguna. Busca hacia atrás porque la tarjeta relevante es la más reciente.

Añadir siempre produciría cinco tarjetas de progreso al 30 %, 55 %, 80 %, y un lector desplazándose hacia arriba entre una pila de barras obsoletas. Algunos resultados son eventos y otros son estado. Los eventos se añaden; el estado se actualiza en su sitio.

Representar texto del modelo sin abrir una inyección

La prosa del modelo es entrada no confiable. Contiene fragmentos aportados por el usuario, puede contener markdown y va a parar a HTML.

La implementación obvia —escapar todo y luego convertir el markdown— tiene un fallo que costó encontrar:

private function parseMessageMarkdown(string $content): string
{
    // Links come out of the raw text first. Escaping the whole string up front
    // double-encodes a URL that already contains an entity — an `&` in the
    // source became `&` and the href resolved wrong.
    $links = [];

    $content = preg_replace_callback(
        '/\[([^\]]+)\]\((https?:\/\/[^\s\)]+)\)/',
        function (array $m) use (&$links): string {
            $token = "\x00LINK".count($links)."\x00";
            $label = preg_replace('/\*\*(.+?)\*\*/s', '<strong>$1</strong>', e($m[1])) ?? e($m[1]);
            $links[$token] = '<a href="'.e($m[2], false).'" target="_blank" rel="noopener noreferrer">'.$label.'</a>';

            return $token;
        },
        $content
    ) ?? $content;

    $safe = e($content);
    $safe = preg_replace('/\*\*(.+?)\*\*/s', '<strong>$1</strong>', $safe) ?? $safe;

    return strtr($safe, $links);
}

El orden es todo el diseño:

  1. Extrae primero los enlaces, sustituyendo cada uno por un marcador que no puede aparecer en texto real.
  2. Escapa el resto —todo lo que escribió el modelo queda ya inerte.
  3. Aplica el pequeño conjunto de formato que permites —aquí, negrita.
  4. Vuelve a sustituir los enlaces, ya construidos y escapados individualmente.

El fallo que forzó esto: escapar primero hacía que una URL con &amp; se convirtiera en &amp;amp;, y el enlace apuntaba a la dirección equivocada. El e($m[2], false) del href es la otra mitad —false desactiva la doble codificación para un valor que ya puede contener entidades.

Dos reglas que generalizar.

Usa lista blanca para el formato. Negrita y enlaces, no un analizador de markdown completo. Cada construcción que soportas es superficie de ataque, y una burbuja de chat casi no necesita ninguna.

Escapa una vez, deliberadamente, en un punto que elijas tú. La mayoría de los fallos de doble codificación son dos escapes bienintencionados en capas distintas.