PHP - Come eliminare i caratteri non validi da un file o stringa XML in formato UTF-8

php-cgi.exe - The FastCGI process exited unexpectedly error and how to fix it

Ieri ho scritto due righe su un metodo PHP per eliminare l'header e il footer di un file XML.P7M firmato elettronicamente (a patto che sia in formato CAdES). Pur trattandosi di un metodo non particolarmente elegante, possiamo dire che svolge egregiamente il suo lavoro, consentendo di eliminare dal file in questione l'header e il footer contenenti le informazioni relative alla firma.

Oggi verserò un altro tributo alle funzioni "brutte ma (almeno) funzionanti" pubblicando anche la funzione che ho sviluppato a corredo della precedente per ripulire il contenuto del suddetto file da caratteri XML non validi, così da poter utilizzare la stringa risultante come parametro per la creazione di di un oggetto SimpleXML:

Il codice di cui sopra è fortemente basato sul contenuto di due risposte presenti sul sito StackOverflow, per la precisione questa e questa: riconoscimenti e ringraziamenti vanno pertanto ai rispettivi autori. Come è possibile vedere, si tratta di una regexp che elimina tutti i caratteri non UTF-8, seguita da un approccio iterativo char-by-char che elimina tutti i caratteri non considerati validi in un contenuto XML. Nello specifico, ho dovuto utilizzare entrambi gli approcci poiché i file XML che dovevo processare erano affetti da entrambi i problemi.

Come ho detto sopra, si tratta di un metodo piuttosto brutto a vedersi e altamente inefficiente, probabilmente persino più di quello pubblicato in precedenza... Ma nonostante questo si è rivelato perfettamente adatto allo scopo, motivo per cui - vista l'esigua quantità di tempo a disposizione - non mi sono fatto scrupoli a utilizzarlo, almeno come workaround temporaneo in attesa di tempi migliori.

A tal proposito, se a qualcuno venisse l'idea (e la voglia) di fare di meglio, accetterò volentieri il suo suggerimento.... Fino ad allora, direi che il "double-clawed hammer" del PHP ha colpito ancora!

PHP - Come eliminare i caratteri non validi da un file o stringa XML in formato UTF-8

Prometto solennemente di non usarlo più per un pò... :)

About Ryan

IT Project Manager, Web Interface Architect e Lead Developer di numerosi siti e servizi web ad alto traffico in Italia e in Europa. Dal 2010 si occupa anche della progettazione di App e giochi per dispositivi Android, iOS e Mobile Phone per conto di numerose società italiane. Microsoft MVP for Development Technologies dal 2018.

View all posts by Ryan

40 Comments on “PHP - Come eliminare i caratteri non validi da un file o stringa XML in formato UTF-8”

  1. Pingback: PHP - Estrarre il contenuto XML da un file XML.P7M (CAdES, Fattura PA)
  2. Grazie per la routine… c’è ancora un carattere che non viene eliminato e che viene identificato con
    82
    00
    Sai come si può eliminare?
    Grazie

  3. Buongiorno, grazie per il codice. Avrei una domanda: con alcuni file P7M non riscontro alcun problema, con altri mi ritrovo con tag modificati nell’XML che impediscono il corretto uso di simplexml_load_string. Ad esempio alcune volte mi ritrovo con </Aliqu/otaIVA>, altre con </FatturaEl&ettronicaBody>, altre ancora con . Come posso ovviare? Grazie

    1. Ciao Davide,

      conosciamo bene il problema che citi, relativo alla sequenza di EOT+NOREP+EOX|EOT+ utilizzata per segmentare i file P7M/CAdES. La prima regex che trovi all’interno della funzione PHP presente in questo articolo dovrebbe essere sufficiente a risolvere il problema:

      $string = preg_replace(‘/(\x{0004}(?:\x{201A}|\x{FFFD})(?:\x{0003}|\x{0004}).)/u’, ”, $string)

      Puoi testarla online con i tuoi file/testi qui:
      https://www.phpliveregex.com/p/qFD

      Se per caso non ti dovesse funzionare, prova a utilizzare questo metodo alternativo:

      mb_regex_encoding(‘UTF-8’);
      $string = mb_eregi_replace(‘(\x{0004}(?:\x{201A}|\x{FFFD})(?:\x{0003}|\x{0004}).)’, ”, $string);

      Facci sapere!

      1. Provate entrambe ma senza successo. Ho risolto passando il contenuto in un nuovo DomDocument con recover true in modo da “pulire” gli errori e ottenere l’XML ben formattato finale. Sto testando in questi giorni per vedere se emergono problemi, al momento va. Grazie

  4. questo funziona su Linux
    $string = preg_replace(‘/\x{0004}[\x{201A}\x{FFFD}][\x{0003}\x{0004}]./u’, ”, $string);
    ma non funziona su windows.
    Qualche idea?

    1. Non saprei. Che errore dà esattamente? Che versione di PHP e di PCRE? (phpinfo per vedere entrambe)

      Eventualmente prova a provare questo metodo alternativo:

      mb_regex_encoding(‘UTF-8’);
      $string = mb_eregi_replace(‘(\x{0004}(?:\x{201A}|\x{FFFD})(?:\x{0003}|\x{0004}).)’, ”, $string);

      Fammi sapere.

        1. Ciao,

          stesso caso di Davide e di ERS, il tuo problema è legato alla sequenza di separazione che prevede 4 caratteri che dovrebbero essere eliminati dalle regex che ho consigliato di utilizzare e che trovi nelle altre risposte. Se riscontri ancora problemi dopo averle provate entrambe, mandaci il file che non funziona (eventualmente epurato dei dati personali) e provvederemo ad analizzare il caso specifico.

          1. purtroppo mi restituisce una stringa vuota utilizzando sia
            mb_regex_encoding(‘UTF-8’);
            $string = mb_eregi_replace(‘(\x{0004}(?:\x{201A}|\x{FFFD})(?:\x{0003}|\x{0004}).)’, ”, $string);
            e sia
            preg_replace(‘/(\x{0004}(?:\x{201A}|\x{FFFD})(?:\x{0003}|\x{0004}).)/u’, ”, $string);
            utilizzo PHP7

      1. Grazie per l’aiuto, questo ha funzionato perfettamente su Windows xampplite con php ver. 5.31:
        mb_regex_encoding(‘UTF-8’);
        $string = mb_eregi_replace(‘(\x{0004}(?:\x{201A}|\x{FFFD})(?:\x{0003}|\x{0004}).)’, ”, $string);
        il problema che riscontravo era diverso la precedente linea:
        $string = preg_replace(‘/\x{0004}[\x{201A}\x{FFFD}][\x{0003}\x{0004}]./u’, ”, $string);
        che mi restituiva $string = ” dopo essere richiamata.
        Su Liunx con php 5.51 invece sia la vecchia che la nuova riga di codice funzionano.
        Preciso che su tutti i file fatture su cui ho fatto i test risultavano tutti encode in base64, per cui ho dovuto eseguire prima:
        $string = base64_decode($string);
        anche se non dovrebbe essere di rilievo la successiva funzione sanitizeXML
        Grazie ancora.

  5. Grazie per l’aiuto, questo ha funzionato perfettamente su Windows xampplite con php ver. 5.31:
    mb_regex_encoding(‘UTF-8’);
    $string = mb_eregi_replace(‘(\x{0004}(?:\x{201A}|\x{FFFD})(?:\x{0003}|\x{0004}).)’, ”, $string);
    il problema che riscontravo era diverso la precedente linea:
    $string = preg_replace(‘/\x{0004}[\x{201A}\x{FFFD}][\x{0003}\x{0004}]./u’, ”, $string);
    che mi restituiva $string = ” dopo essere richiamata.
    Su Liunx con php 5.51 invece sia la vecchia che la nuova riga di codice funzionano.
    Preciso che su tutti i file fatture su cui ho fatto i test risultavano tutti encode in base64, per cui ho dovuto eseguire prima:
    $string = base64_decode($string);
    anche se non dovrebbe essere di rilievo la successiva funzione sanitizeXML
    Grazie ancora.

      1. Prova a dare un’occhiata alla risposta di Davide poco sopra:

        “Ho risolto passando il contenuto in un nuovo DomDocument con recover true in modo da “pulire” gli errori e ottenere l’XML ben formattato finale. Sto testando in questi giorni per vedere se emergono problemi, al momento va. Grazie”

        Il workaround che lui propone dovrebbe essere grossomodo così:

        function simplexml_load_string_safe($string)
        {
        libxml_use_internal_errors(true);
        $d = new DOMDocument(“1.0”, “UTF-8”);
        $d->strictErrorChecking = false;
        $d->validateOnParse = false;
        $d->recover = true;
        $d->loadXML(sanitizeXML($string));
        $x = simplexml_import_dom($d);
        libxml_clear_errors();
        libxml_use_internal_errors(false);
        return $x;
        }

        Prova e facci sapere!

        1. Io ho questo problema:

          usando la tua soluzione postata fino a qualche settimana fa senza la correzione ottenevo:

              <AltxriDatiGestionali>
                <TipoDato>Targa</TipoDato>
                <RiferimentoTesto>AB123CD</RiferimentoTesto>
              </AltriDatiGestionali>
          

          Se noti c’è una X nel primo Altri dati gestionali (questo dopo averlo corretto con la funzione prima in quel punto c’erano altri caratteri non UTF).

          Se provo con $string = preg_replace(‘/\x{0004}[\x{201A}\x{FFFD}][\x{0003}\x{0004}]./u’, ”, $string); ottengo stringa vuota.

          Se uso il workaround di DAVIDE mi propone l’errore in entrambi i TAG

          Targa
          AB123CD

          Sono spalle al muro eheh :) se volete posso condividere il mio file per farvi fare qualche prova anche a voi.

          https://www.dropbox.com/s/c93clcw5n1h4jkp/IT01234567890_1AX0J.xml.p7m?dl=0

          1. Ciao Davide,

            Ho provato a convertire il tuo file con tutti i software che abbiamo realizzato ultimamente per convertire P7M (PHP, C# ed Electron/Javascript): tutto ok, nessun errore. Nello specifico, con PHP sto usando la seguente (già più volte consigliata in questi commenti):

            $string = mb_eregi_replace(‘(\x{0004}(?:\x{201A}|\x{FFFD})(?:\x{0003}|\x{0004}).)’, ”, $string);

            facci sapere!

  6. $string = preg_replace(‘/\x{0004}[\x{201A}\x{FFFD}][\x{0003}\x{0004}]./u’, ”, $string);

    mi restituisce una stringa vuota.

    L’altro metodo sembra funzioni per EOT+ETX, ma non per EOT+

    1. Non ho letto la conclusione del tuo commento, comunque prova questa RegEx meno permissiva al posto della precedente:

      /\x{0004}[\x{201A}\x{FFFD}][\x{0001}\x{0002}\x{0003}\x{0004}\x{0005}\x{0006}\x{0007}]./u

      Fammi sapere

  7. Purtroppo non è cambiato niente. Se uso
    $string = preg_replace(‘/\x{0004}[\x{201A}\x{FFFD}][\x{0001}\x{0002}\x{0003}\x{0004}\x{0005}\x{0006}\x{0007}]./u’,”, $string);

    ottengo una stringa vuota. Se uso
    mb_regex_encoding(‘UTF-8’);
    $string = mb_eregi_replace(‘(/\x{0004}[\x{201A}\x{FFFD}][\x{0001}\x{0002}\x{0003}\x{0004}\x{0005}\x{0006}\x{0007}].)’,”, $string);

    EOT+char non viene eliminato

    1. Occhio che nella mb_eregi_replace che stai utilizzando hai un carattere non valido (la / iniziale): riprova eliminandolo

      1. Ho inserito
        mb_eregi_replace(‘(\x{0004}.)’,”, $string);

        all’inizio, prima ancora dell’eliminazione dell’header della firma digitale. Questo perchè mi sono trovato un caso in cui il carattere dopo EOT era un new line, per cui comportava un errore anche durante la rimozione del footer della firma.

        Al momento sembra funzionare alla perfezione, per lo meno con le fatture che ho ricevuto fin ora, da una ventina di fornitori diversi.

  8. Buongiorno, rivolgo una domanda che apparentemente sembra non pertinente: la fatture che mi sono state trasmesse da SDI (in realtà giunte tutte tramite PEC a più soggetti che usano un mio script in php, fatto per mio uso nel lontano 2004 aggiornato poi per hobby, ed integrato con la fattura elettronica) mi sono giunte tutte base64 encode, vi risulta che l’encode sia fatto da SDI su tutte le fatture?
    A scopo di informazione generale, il mio script si collega direttamente alla casella PEC e sfruttando anche l’ottimo script di Ryan scarica ed elabora le fatture passive. Su una casistica di 400 fatture ricevute (forse più) al momento solo su 4 fatture firmate CADES permangono caratteri non validi UTF8 presenti all’interno dei tag e nei valori contenuti nei tag, facendo fallire l’importazione.
    Ieri sera ho individuato una strada alternativa, che ha avuto un primo esito positivo su uno dei 4 file, devo testarla su altri questa sera (e metterla in bella), rimanete connessi. Chiedo a tutti di rispondere al mio quesito poter farmi concludere il mio test e condividere delle info corrette.
    Grazie a tutti

  9. Ho provato e: FUNZIONA! (testato solo su LINUX)
    Come estrarre un file FATTURA.xml.p7m con caratteri UTF8 non validi nel corpo della fattura.
    L’approccio è all’opposto del presente articolo: ossia ottenere i certificati ed estrarre il file XML in chiaro usando detti certificati.
    Passo 1 ricavato dal seguente link
    https://quoll.it/firma-digitale-p7m-come-estrarre-il-contenuto/
    usare da terminale:
    wget -O – https://applicazioni.cnipa.gov.it/TSL/IT_TSL_signed.xml | perl -ne ‘if (//) {
    s/^\s+//; s/\s+$//;
    s/<\/*X509Certificate>//g;
    print “—–BEGIN CERTIFICATE—–\n”;
    while (length($
    )>64) {
    print substr($,0,64).”\n”;
    $
    =substr($,64);
    }
    print $
    .”\n”;
    print “—–END CERTIFICATE—–\n”;
    }’ >CA.pem

    Otteniamo così in CA.pem il file con tutti i certificati
    da terminale potremmo ottenere subito il file fattura in chiaro usando il seguente comando:
    openssl smime -in IT01234567890_flkab.xml.p7m -inform DER -verify -noverify -CAfile CA.pem -out IT01234567890_flkab.xml

    in php abbiamo bisogno di qualche passaggio in più, ecco lo script che ho usato:

    // funzione che inserisce MIME nel file p7m
    function der2smime($file) {
    $to=<<<TXT
    MIME-Version: 1.0
    Content-Disposition: attachment; filename=”smime.p7m”
    Content-Type: application/x-pkcs7-mime; smime-type=signed-data; name=”smime.p7m”
    Content-Transfer-Encoding: base64
    \n
    TXT;
    $from=file_get_contents($file);
    $to.=chunk_split(base64_encode($from));
    // memorizza file senza .p7m
    return file_put_contents(substr($file,0,-4),$to);
    }

    $file = “dati/IT01234567890_flkab.xml.p7m”;
    $file_no_p7m = substr($file,0,-4);
    $ver_p7m = der2smime($file);
    // ora esegue la messa in chiaro
    // carica $file_no_p7m che ora ha il MIME
    // usa i certificati in CA.pem
    // sovrascrive $file_no_p7m con il file XML in chiaro
    $status = openssl_pkcs7_verify ($file_no_p7m, PKCS7_NOVERIFY, ‘dati/NULL.pem’ ,array(“dati/CA.pem”),”dati/CA.pem”, $file_no_p7m);

    echo ‘status: ‘ . $status . ”;
    // 1 = operazione avvenuta con successo
    exit();

    Otteniamo quindi in IT01234567890_flkab.xml il file XML in chiaro al 100% senza potenziali caratteri UTF8 non rimossi!
    Grazie a tutti

    1. In teoria se estraggo il certificato tramite una applicazione windows il risultato dovrebbe essere lo stesso? Ho provato, ma non funziona.

      1. Questa sera provo e ti faccio sapere: hai dato una path assoluta a CA.pem sotto windows? Es. C:\xammp\htdocs\tua_dir\dati

        1. Ho usato Dike, con cui è possibile esportare il certificato incluso in un file P7m. Poi ho copiato il file ottenuto sul server e ho fatto girare lo script php.

          1. Tieni conto che questo script è solo per i file XML.p7m firmato con i caratteri utf8 non validi ossia con certificato PEM, non funziona con file XML.p7m senza caratteri utf8 non validi che sono firmati DER, per questi serve uno script con una leggera modifica in quanto il certificato è incluso nel file XML.p7m stesso e non è contenuto nel CA.pem in questo caso restituisce -1,
            Non so dirti se da Dike esporti il file CA.pem nel formato atteso da openssl_pkcs7_verify di PHP, di massima direi di NO rileggendo il sito segnalato da cui ho estratto le info. Prova ad aprire il tuo CA.pem se non presenta ricorsive le righe —- BEGIN CERTIFICATE —– —- END CERTIFICATE —-
            Direi che non è compatibile con lo script in PHP e va trasformato

      2. Quello era uno script per PERL

        Questo da terminale di linux:

        wget -O – https://applicazioni.cnipa.gov.it/TSL/IT_TSL_signed.xml | perl -ne ‘if (//) {
        s/^\s+//; s/\s+$//;
        s/<\/*X509Certificate>//g;
        print “—–BEGIN CERTIFICATE—–\n”;
        while (length($
        )>64) {
        print substr($,0,64).”\n”;
        $
        =substr($,64);
        }
        print $
        .”\n”;
        print “—–END CERTIFICATE—–\n”;
        }’ >CA.pem

        Eventualmente il file CA.pem si può condividere sono 400k oppure penso che Ryan possa tradurlo in uno script per windows. Linux è comodo perché include tutti questi linguaggi di sviluppo senza ricorrere ad altre applicazioni.

        1. Dunque. Il file su cui sto facendo i test ha i caratteri non validi. Però ho provato lo script usando il certificato estratto dal file stesso. Potrebbe essere questo il problema?

          Dike estrae un file .cer, e contiene —- BEGIN CERTIFICATE —– —- END CERTIFICATE —-, per cui ho provato semplicemente a rinominarlo .pem, come suggerito da qualcuno in rete. Ho provato a leggerlo con la funzione openssl_x509_parse e non riscontro problemi.

          P.S. si può estrarre il certificato dal file tramite PHP?

          1. Il file CA.pem contiene generato come spiegato contiene circa 300 certificati, non solamente uno. Detti certificati si abbinano al file xml con firma CADES. Provato anche stasera ed ho risolto per altri 2 file che contenevano caratteri UTF8 non validi. Con sanitizeXML non riuscivo a rimuoverli, segui la procedura che ho spiegato e vedrai che funziona. Attendo il commento di Ryan.
            Ciao

        2. Grazie a tutti per i preziosissimi contributi: aggiungo i miei finding personali al netto di circa 3000 fatture testate nelle ultime settimane/mesi e dei conseguenti progressi che ho fatto a mia volta (parliamo pur sempre di un articolo scritto più di 2 anni fa).

          • Il metodo “openssl” (di cui esiste una funzione nativa, un porting o un’alternativa di terze parti in tutti i principali linguaggi – al momento ho trovato come fare in PHP, Electron/NodeJS e ASP.NET C#) è ottimo per i file PKCS#7/DER ASN.1 compliant, che poi sono quelli che hanno il payload XML diviso in segmenti (i più ostici da affrontare con le RegExp): questo metodo converte circa l’80% dei file di cui attualmente dispongo, quindi è la prima cosa che tento (chiamiamolo TRY #1). N.B.: a seconda del linguaggio (e/o della libreria PKCS#7 utilizzata) può o meno essere necessario fare un Convert.FromBase64 nel caso dei file codificati in Base64.
          • per il 20% dei file restanti (quelli non ASN.1 compliant) al momento sto utilizzando le funzioni e le regexp già pubblicate in questo post, che in quei casi funzionano bene anche perché i suddetti file non hanno il problema dei separatori – anche se un passaggio di mb_eregi_replace(‘(\x{0004}.)’,”, $string) al momento lo sto facendo comunque per sicurezza. Questa procedura di fallback l’ho chiamata TRY #2.

          Unendo TRY #1 e TRY #2 al momento non riscontro errori su circa 3000 fatture (Base64 e non, ASN1 compliant e non, etc.), a parte qualche sporadico caso di formattazione errata all’origine: se ne avete qualcuna che risulta “immune” da entrambi i passaggi, fatemi sapere e (se potete epurarla dei dati personali) mandatemela pure!

          N.B.: Non dovendo mai verificare la validità dell’issuer, al momento non sto utilizzando la conversione da PEM (con file CA.pem), che comunque è certamente un’ottima aggiunta al codice… a patto di trovare il modo di scaricarlo in automatico, così da tenerlo sempre aggiornato. In un applicativo “automatizzato” dove la connessione a internet non è garantita questo potrebbe essere un problema, rendendo paradossalmente preferibile il TRY #2: sfortunatamente, per così dire, è proprio il mio caso, o per meglio dire il presupposto originario di questo articolo. Se questo problema non c’è, il decrypt da PEM andrebbe idealmente a sostituire il TRY #2 (che a quel punto diventerebbe il TRY #3 o potrebbe anche essere omesso). A quel punto la routine farebbe grossomodo le stesse cose che fanno i software come Dike et sim. – che infatti, non a caso, scaricano periodicamente l’elenco dei certificati aggiornati.

          Fonti: https://tools.ietf.org/html/rfc5652#section-5 e seg.

          Fatemi sapere cosa ne pensate!

          1. Potresti condividere il TRY#1 per PHP? Ho provato ma le mie conoscenze non sono all’altezza.

          2. Colgo nuovamente l’occasione per ringraziare Ryan e tutti gli intervenuti che mi hanno aiutato a districarmi in quello che stava diventando un “mal di testa”. Il mio script preleva le Fatture direttamente dalla PEC per cui do per certo che sia presente un collegamento alla rete. Quindi più che giusta l’osservazione che è necessario avere sempre un file CA.pem aggiornato.
            COME CREARE IL FILE CA.pem AUTOMATICAMENTE
            Lo script PHP che segue crea il file CA.pem prelevando la chiave pubblica direttamente dall’ente:
            loadXML($string);
            $X509Certificate = $dom->getElementsByTagName(‘X509Certificate’);
            // metto in s nel ciclo while di dati estratti dal DOM
            $s = ”;
            foreach ($X509Certificate as $v) {
            $i = 1;
            $s .= “—–BEGIN CERTIFICATE—–\n”;
            while(strlen($v->nodeValue) > (64 * $i)) {
            $s .= substr($v->nodeValue, ($i – 1) * 64, 64) . “\n”;
            $i++;
            }
            // aggiunge il resto
            $s .= substr($v->nodeValue, ($i – 1) * 64, 64) . “\n”;
            $s .= “—–END CERTIFICATE—–\n”;
            }
            // scrivo s nel file CA.pem
            file_put_contents(‘CA.pem’, $s);
            exit();
            ?>
            Questo a probabile conclusione della TRY #2.
            Ringrazio nuovamente Ryan e spero ci sia modo di confrontarci nuovamente in futuro.

  10. Era sparito un pezzo di codice
    [php
    // crea una copia di backup di CA.pem – non si sa mai servisse
    if (file_exists(‘CA.pem’)) copy (‘CA.pem’,’Ca.pem.bak’);
    $fileCA = ‘https://applicazioni.cnipa.gov.it/TSL/_IT_TSL_signed.xml’;
    $string = file_get_contents($fileCA);
    $dom = new DOMDocument;
    $dom->loadXML($string);
    $X509Certificate = $dom->getElementsByTagName(‘X509Certificate’);
    // metto in s nel ciclo while di dati estratti dal DOM
    $s = ”;
    foreach ($X509Certificate as $v) {
    $i = 1;
    $s .= “—–BEGIN CERTIFICATE—–\n”;
    while(strlen($v->nodeValue) > (64 * $i)) {
    $s .= substr($v->nodeValue, ($i – 1) * 64, 64) . “\n”;
    $i++;
    }
    // aggiunge il resto
    $s .= substr($v->nodeValue, ($i – 1) * 64, 64) . “\n”;
    $s .= “—–END CERTIFICATE—–\n”;
    }
    // scrivo s nel file CA.pem
    file_put_contents(‘CA.pem’, $s);
    ]
    exit();

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *


The reCAPTCHA verification period has expired. Please reload the page.

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.