venerdì 26 aprile 2013

PHP e stringhe con caratteri strani ossia il charset UTF-8 e dintorni nel PHP (Parte 3/4)

Il carattere sostitutivo UTF-8
per le codifiche errate
Ora che abbiamo visto cos'è UTF-8, e come utilizzarlo all'interno dei nostri file html, è giunto il momento di vedere come si comporta PHP rispetto a tale codifica.

PHP ha una tipizzazione debole. Ciò significa che sebbene ci siano 8 tipi di dato primitivo, questi sono utilizzati in modo trasparente all'utente. In altre parole nella maggior parte dei casi è PHP che decide quale tipo utilizzare per una data variabile, convertendo automaticamente il tipo di una variabile in un altro secondo proprie regole interne. In talune circostanze tali conversioni automatiche possono spiazzare l'utente e portare a errori logici con risultati inattesi.

Fra i tipi primitivi abbiamo le stringhe. Ad esempio quando scriviamo


$str = 'Questa è una stringa';


Stiamo definendo una variabile di nome $str avente come valore il letterale stringa compreso tra gli apici. Fin qui tutto normale, ma come è codificato (qual'è il suo charset) il letterale stringa?

Iniziamo con il dire che il tipo stringa di PHP non va inteso come una sequenza di caratteri. In realtà in PHP il tipo stringa è una array di byte, come già illustrato nella guida sulle stringhe. In più tutto il codice all'interno di un file slavato con charset UTF-8 è codificato UTF-8, e PHP adotta la codifica del file. In tal senso quel letterale è codificato UTF-8. Ciò significa che all'interno della variabile $str si trova una stringa codificata UTF-8.

Conseguenza di quanto detto è che la stringa codificata UTF-8 Questa è una stringa è composta di 20 caratteri ed è memorizzata in 21 byte. Infatti i caratteri alfabetici appartengono al range di codifica 0-7F e richiedono un solo byte, mentre il carattere accentato, che richiede più di 7 bit nella codifica Unicode ma meno di 11, è codificato con 2 byte. Essendo però le stringhe PHP degli array di byte, ne consegue che la funzione strlen() restituirà 21 e non 20. Infatti la suddetta funzione restituisce la dimensione in byte di $str. Ciò può essere fuorviante se, abituati a lavorare con ISO8859-1 o ASCII, in cui la strlen() è sempre stata utilizzata per ottenere la lunghezza in caratteri della stringa. Ciò però è normale per quelle codifiche che utilizzano un byte per ogni carattere. D'altra parte, essendo UTF-8 una codifica che può impegnare da 1 a 4 byte, al fine di svolgere in modo corretto le operazioni sulle stringhe occorre ricorrere alle funzioni mb_xxxx(), in cui mb sta per multibyte.

La funzione giusta per ottenere la lunghezza della stringa è mb_strlen($str,'UTF-8');. Se non si vuole stare sempre ad indicare la codifica adottata, è possibile utilizzare la funzione mb_internal_encoding('UTF-8'); in modo che, omettendo l'argomento opzionale per la codifica, sia utilizzata automaticamente UTF-8. Fanno eccezione le funzioni per le espressioni regolari della famiglia mb_xxxx() per le quali il charset va indicato con mb_regex_encoding('UTF-8');. In alternativa è possibile utilizzare le funzioni preg_xxxx(), come preg_match() o preg_match_all(), ricordando di concludere il pattern con il modificatore u facendo in modo che il sistema sappia che il pattern è codificato UTF-8.

In generale è sufficiente richiamare le due funzioni di specifica dell'enconding, assicurarandosi un sereno utilizzo delle funzioni mb_xxxx() nei propri script, un po' come si fa per la session_start(); prima dell'utilizzo della variabile super global $_SESSION.

Altre funzioni per le stringhe della famiglia strxxxx() possono ancora essere utilizzate, tenendo ben presente che lavorano su array di byte e non sui caratteri. In più tali funzioni possono, in alcuni casi, essere non binary safe, ovvero trattare il carattere NUL, con codice ASCII 0, come terminatore di stringa. Ma questo non dovrebbe essere un problema nel trattamento di stringhe codificate UTF-8 ma piuttosto nel trattamento di flussi di dati binari.

Alcune implementazioni di parser UTF-8 definite "ingenue" nelle specifiche IETF per UTF-8 potrebbero accettare la codifica C080 al posto di 00 per il carattere NUL. Di norma realizzate per compatibilità con le funzioni del C che utilizzano il carattere NUL per terminare le stringhe. Tale ingenuità apre però apre le porte a possibili problemi di sicurezza e causare altri problemi come indicato al punto 10 delle specifiche UTF-8. Come però scritto nel primo post di questa serie l'ottetto C0 non può comparire nelle codifiche UTF-8, pertanto non dovrebbe essere accettato.
Lo script di test, presente a fondo pagina, in cui è mostrato come reagiscono diversi browser alla combinazione C080
Inutile dire che firefox e chrome hanno fornito la visualizzazione attesa per una codifica errata mostrando il carattere �.
Vediamo un esempio per mostrare come le stringhe PHP siano array di byte. Si consideri il carattere 'è' che in Unicode ha indice E8.Ossia in binario 1110 1000. Trasformandolo in UTF-8, seguendo quanto appreso nel primo post di questa serie, si ha una combinazione di 2 byte (l'indice Unicode ha più di 7 bit ma meno di 11). In UTF-8 il primo byte è 11000011 ed il secondo 10101000 in cui la parte in corsivo rappresenta il dato che ricomposto è il codice Unicode. Trasformando i due byte in esadecimale avremo che il carattere 'è' è rappresentato dai byte C3 e A8. Volendo generare direttamente il codice esadecimale UTF-8 all'interno di una stringa PHP, che si ricorda ancora una volta essere un array di byte, potremo scrivere nel codice PHP i due byte come segue:

<?php
    $str = "\xC3\xA8";
    echo $str;
?>

In questo modo $str contiene il carattere 'è' se interpretato da chi lo riceve (il browser) come UTF-8 ossia i due byte C3 e A8.

A conclusione di questa breve guida il codice per testare quanto detto fin'ora il cui risultato dell'esecuzione è mostrato nell'immagine precedente.

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Esempi su UTF-8</title>
    </head>
    <body>
        <?php
        mb_internal_encoding('UTF-8');
        //Se il file è codificato UTF-8, i letterali stringa sono codificati UTF-8
        //memorizzati con tale codifica nelle variabili.
        $str = 'Questa è una stringa';
             
        //Comportamento di strlen e mb_strlen
        echo '$str contiene la stringa: '.$str.'<br>';
        echo 'Composta da '.strlen($str).' bytes.<br>';
        echo 'Composta da '.mb_strlen($str).' caratteri '.mb_internal_encoding().'<br>';
     
        //Tattare la stringa come array di byte
        $str = "\xC3\xA8";
        echo "In \$str è stato memorizzata la stringa <strong>$str</strong> avente codice esadecimale ";
        for($i=0,$n=strlen($str);$i<$n;$i++)
            printf("%X",ord($str{$i}));
        echo '<br>';
         
        //La combinazione errata C080
        $str = "\xC0\x80";
        echo "La combinazione C080 produce: $str";
        ?>
    </body>
</html>

Ora che è stato analizzato il comportamento di PHP rispetto ad uno script memorizzato in un file codificato UTF-8, nel prossimo post non resta che vedere come si comporta MySQL, il terzo elemento della famigerata famiglia AMP (Apache MySQL PHP) e come fare in modo che MySQL e PHP possano scambiare correttamente flussi di dati codificati UTF-8.