Partage
  • Partager sur Facebook
  • Partager sur Twitter

Manipulez les fichier MHTML

Sujet résolu
    18 mars 2024 à 17:41:45

    Bonjour

    Je dois manipuler les fichiers mhtml mais je suis un peu perdu. Je dois par exemple decoder(base64) quand c'est nécessaire et aussi convertir le texte en UTF-8.

    Je voudrais d'abord en savoir plus sur ce format de fichier, et est-ce pertinent de "maitriser" sa structure et tout ? C'est un travail qui m'a été assigné mais je peux discuter si c'est inutile, ou si il y'a mieux.

    Maintenant comment savoir quand une section par exemple doit être décoder ou convertie sous un autre charset s'il vous plait ?

    J'attends vos suggestions {#emotions_dlg.smile}

    • Partager sur Facebook
    • Partager sur Twitter
      18 mars 2024 à 19:39:53

      Salut

      Si je me souviens bien, le MHTML est plus ou moins un fichier qui possède une structure similaire à un e-mail au niveau des données, mais sans les en-têtes des mails. Le jeu de caractères ne devrait pas avoir d'incidence tant que le texte s'affiche correctement, et les données en base64 ne devraient pas être impactées.

      Concrètement, tu dis que tu dois manipuler, mais ça implique de les lire ou aussi de les générer ?

      • Partager sur Facebook
      • Partager sur Twitter
        18 mars 2024 à 23:09:32

        Je suis sur un projet de traductions de divers fichiers,  et on vient de me parler du mhtml. Je ne connais rien dessus et ca semble "vieux", mais bon je dois travailler avec. Je dois pouvoir recuperer un fichier mhtml, traitez chaque section(je remarque des ------=_NextPart... a chaque section): Si le texte n'est pas en utf-8 je le convvertis en utf-8, si c'est une donnee en base64 je la decode(objectif ? Je ne sais pas). A la fin je dois pouvoir generer un mhtml avec ces "nouvelles" donnees oui.

        Il y'a d'autres petits points mais mon probleme est de comprendre meme le mhtml. Avec des exemples de fichier .mhtml je commence a mieux comprendre la structure. Avant que je ne me lance, cette bibliotheque: php-mime-mail-parser/php-mime-mail-parser: A fully tested email parser for PHP 8.0+ (mailparse extension wrapper). (github.com) peut m'etre utilie d'apres vous ? 

        Et j'edite mes fichiers .mhtml avec Notepad, mon superieur me parlais de Word sauf que Word ne reconnait pas le format, est-ce normal ?

        Edit

        Je trouve que les fonctions de cette extension me seront utiles. Mais la derniere version sur windows est pour la 8.1 et je suis a 8.2(version php). Triste.

        -
        Edité par Asmitta 19 mars 2024 à 0:29:18

        • Partager sur Facebook
        • Partager sur Twitter
          20 mars 2024 à 21:29:23

          Disons que connaissant la bouillie pseudo-HTML que Word génère, je pense qu'utiliser Word pour générer/modifier du MHTML est une solution de dernier recours. Par contre, je dois reconnaître que c'est probablement plus sûr que de modifier à la main avec un éditeur de texte basique.

          Je pense qu'il pourrait être possible d'utiliser les fonctions imap_* de PHP dans une certaine mesure, vu que, comme expliqué, le MHTML est une sorte d'e-mail avec de la structure HTML autour entre autres.

          • Partager sur Facebook
          • Partager sur Twitter
            23 mars 2024 à 9:13:57

            Bonjour, je me suis servi de mailparse(avec https://github.com/php-mime-mail-parser/php-mime-mail-parser ), c'est pour les mails mais oui le mhtml est un mail sana en-tete comme tu l'as dit.

            J'ai un probleme avec les encodages. Voici ma classe: 

            <?php
            
            namespace App\Services\Mhtml;
            
            use PhpMimeMailParser\Parser;
            
            
            class MhtmlProcessor
            {
                private array $images;
                private ?string $decodedBody;
                private array $unhandledSections;
            
                private $parser;
            
                public function __construct(private ?string $content)
                {
                    $this->parser = new Parser();
                    $this->images = [];
                    $this->unhandledSections = [];
                    if ($content !== null)
                        $this->parser->setText($content);
                }
            
                private function clearData()
                {
                    $this->images = [];
                    $this->decodedBody = null;
                    $this->unhandledSections = [];
                    $this->parser->__destruct();
                }
            
                public function setContent(string $content): void
                {
                    $this->clearData();
                    $this->content = $content;
                    $this->parser = new Parser();
                    $this->parser->setText($content);
                }
            
                public function getBody(): ?string
                {
                    if ($this->decodedBody == null)
                        return null;
            
                    $cleanedData = htmlspecialchars_decode($this->decodedBody);
                    $cleanedData = str_replace("\x00", "", $cleanedData);
            
                    return $cleanedData;
                }
            
                public function process(): void
                {
                    foreach ($this->parser->getParts() as $key => $part) {
                        if ($key === 1) continue;
            
            
                        if (
                            $part['content-type'] == 'text/html'
                            && $part['charset'] == 'unicode'
                        ) {
                            $this->decodedBody = htmlspecialchars($this->parser->getMessageBody('html'));
                        } elseif (
                            str_starts_with($part['content-type'], 'image/')
                            && $part['transfer-encoding'] == 'base64'
                        ) {
                            $data = substr(
                                $this->content,
                                $part['starting-pos-body'],
                                $part['ending-pos-body'] - $part['starting-pos-body']
                            );
                            $this->images[] = [
                                'data' => $data,
                                'headers' => $part['headers']
                            ];
                        } else {
                            $content = substr(
                                $this->content,
                                $part['starting-pos-body'],
                                $part['ending-pos-body'] - $part['starting-pos-body']
                            );
            
                            if (
                                !str_starts_with($part['content-type'], 'application/')
                                && $part['transfer-encoding'] == 'base64'
                            ) {
                                $content = base64_decode($content, true);
                                $encoding = mb_detect_encoding($content);
                                if ($encoding) {
                                    $content =  mb_convert_encoding($content, 'UTF-8', $encoding);
                                }
                            }
            
                            $this->unhandledSections[] = [
                                'headers' => $part['headers'],
                                'content' => $content
                            ];
                        }
                    }
                }
            }
            

            Comment je l'utilise: 

                #[Route('/mhtml', name: 'app_home_mhtml', methods: 'POST')]
                public function mhtml(Request $request): JsonResponse
                {
                    $file = $request->files->get('file');
                    if ($file == null) {
                        return $this->json(['Missing file'], Response::HTTP_BAD_REQUEST);
                    }
            
                    $content = file_get_contents($file->getPathName());
                    $mhtmlProcessor = new MhtmlProcessor($content);
                    $mhtmlProcessor->process();
                    return $this->json(['body' => $mhtmlProcessor->getBody()]);
                }

            Mais le rendu dans le navigateur(et meme dans la console ou tout autre "endpoint") n'affiche pas les caracteres "etrangers":

            ��
            Test file 5
            Hello, this is a test file with some text in it in various character sets
            
            ���� ���

            Je devrais pourtant avoir ceci(a la place de cette seconde ligne illisible):

            Γεια σασ.


            J'ai fait pas mal de combinaisons avec mb_convert_encoding mais ces caracteres ne sont jamais comme il fauit. Des idees s'il vous plait ?

            • Partager sur Facebook
            • Partager sur Twitter
              23 mars 2024 à 9:45:38

              Quelle est la version de PHP utilisée, et quel est le jeu de caractères du fichier (tel qu'indiqué quand ouvert avec Notepad++ ou un éditeur de code) ? Est-ce que les caractères non latins sont affichés correctement quand le fichier MHTML est ouvert ainsi ? Parce que je pense que mb_detect_encoding() peut se tromper si on ne spécifie pas un autre paramètre qui contient une liste de jeux de caractères possibles, j'ai déjà eu le cas.

              Il y a aussi le fait que potentiellement, toutes les parties peuvent être dans leur propre jeu de caractères et que celui-ci ne soit pas global, ce qui peut là aussi gêner mb_detect_encoding().

              A noter que les deux caractères en début de partie affichée, tu n'en parles pas, que sont-ils censés être ?

              • Partager sur Facebook
              • Partager sur Twitter
                24 mars 2024 à 10:10:03

                Dans Notepad le charset est ASCII, le mhtml orignal bien sur. Et le mb_detect_encoding donne bien ASCII a ce niveau, puis je convertis en UTF-8 mais le rendu est tel qu'on le voit. Pour les deux caractères de debut je n'ai aucune idee.

                Je viens de faire un petit test:

                <?php 
                
                $string = 'Γεια σασ';
                echo mb_convert_encoding(
                  mb_convert_encoding($string, 'ASCII', 'UTF-8'), 
                  'UTF-8', 
                  'ASCII'
                );
                
                // Affiche: ???? ???

                Ah la suite s'annonce difficile

                Edit

                J'utilise ce package pour convertir en utf-8 maintenant: neitanod/forceutf8: PHP Class Encoding featuring popular Encoding::toUTF8() function --formerly known as forceUTF8()-- that fixes mixed encoded strings. (github.com). J'ai plus les "points d'interrogation" mais c'est pas meilleur.

                Test file 5
                Hello, this is a test file with some text in it in various character sets
                
                 

                -
                Edité par Asmitta 24 mars 2024 à 10:12:29

                • Partager sur Facebook
                • Partager sur Twitter
                  25 mars 2024 à 10:53:48

                  Ymox avait écrit:

                  […] Est-ce que les caractères non latins sont affichés correctement quand le fichier MHTML est ouvert ainsi ?
                  […] Il y a aussi le fait que potentiellement, toutes les parties peuvent être dans leur propre jeu de caractères et que celui-ci ne soit pas global, ce qui peut là aussi gêner mb_detect_encoding().
                  […] A noter que les deux caractères en début de partie affichée, tu n'en parles pas, que sont-ils censés être ?

                  Et quel est le jeu de caractères détecté/utilisé par ce qui te permet d'afficher le résultat (navigateur) ? Est-ce qu'en jouant avec le jeu de caractères hors de ton code, tu arrives à voir ce à quoi tu t'attends ?

                  • Partager sur Facebook
                  • Partager sur Twitter
                    25 mars 2024 à 19:09:19

                    Les caracteres "etrangers"(ceux en utf-8) sont tous en ASCII a la base. Le fichier source ouvert avec Word affiche bien les caractères, mais dans notepad c'est en ASCII(ANSI si je dois reprendre exactement).

                    En convertissant en utf-8 j'ai donc ces "carres"(avec Encoding::fixUTF8 j'ai plus les points d'interrogations), que ce soit avec Word, Notepad ou le navigateur ces caractères restent inconnus après la conversion. 

                    >Et quel est le jeu de caractères détecté/utilisé par ce qui te permet d'afficher le résultat (navigateur)

                    UTF-8

                    >Est-ce qu'en jouant avec le jeu de caractères hors de ton code, tu arrives à voir ce à quoi tu t'attends ?

                    Hors de mon code comment ?

                    Bonsoir

                    • Partager sur Facebook
                    • Partager sur Twitter
                      25 mars 2024 à 23:31:09

                      Que Word les affiche correctement ne m'étonne pas trop (et encore), moi j'aimerais savoir si c'est aussi le cas avec un éditeur de texte (donc pas Word qui est un traitement de texte) ou de code, merci — plutôt un outil comme Notepad++ ou même VS Code, ils ont tous les deux un support à mon avis suffisamment étendu des jeux de caractères, même si VS Code me semble un peu plus complet. Les deux, par contre, peuvent ne pas détecter correctement, il faudra jouer avec Encodage > Encoder en… sous Notepad++ (PAS Convertir en) ou cliquer sur le jeu de caractères affiché en bas à droite avec VS Code et réouvrir avec un autre jeu de caractères. A tester à mon sens, si tous les caractères du fichier ainsi ouvert ne s'affichent pas :

                      • ISO 8859-1
                      • ISO 8859-15
                      • Windows-1252 alias cp-1252
                      • OEM850

                      Attention, le bloc notes de Windows affiche ANSI, mais c'est plus probablement Windows-1252, qui est "le standard" pour les fichiers de Microsoft en Europe centrale. Word, pour les fichiers "courants", utilise du XML en UTF8 voire même UTF-16, mais je n'ai aucune idée de ce qu'il utilise avec du MHTML, d'autant qu'il peut aussi ne pas faire la moindre conversion sur un fichier existant.

                      Le fait que ton outil pour convertir en UTF-8 ne réussit pas montre que le jeu de caractères de départ n'est pas celui attendu, et sans cela, ni sans avoir un jeu de caractères qui permet d'afficher le tout, on ne pourra pas vraiment avancer. Je rappelle que suivant comment, le jeu de caractères peut changer d'une partie à l'autre. A voir si le HTML est dans une partie unique ou scindé.

                      • Partager sur Facebook
                      • Partager sur Twitter
                        26 mars 2024 à 6:02:08 - Message modéré pour le motif suivant : Merci de créer votre propre sujet


                          26 mars 2024 à 10:07:46

                          BONJOUR !! (Matin plutôt joyeux chez moi)

                          J'ai suivi ta suggestion en changeant l'encodage à chaque fois mais rien. Avant je faisais:

                          $cleanedData = $this->decodedBody;
                          $cleanedData = str_replace("\x00", "", $cleanedData);
                          
                          // Je faisais mes mb_convert_encoding ou Encoding::fixUTF8 ici
                          
                          return $cleanedData;

                          J'ai changé cela en:

                          $cleanedData = $this->decodedBody;
                          $cleanedData = iconv("UTF-16", "UTF-8//TRANSLIT//IGNORE", $cleanedData); // str_replace était une des sources du problèmes alors
                          
                          return $cleanedData;

                          Et j'ai bien ceci maintenant: 

                          Test file 5
                          Hello, this is a test file with some text in it in various character sets
                          
                          Γεια σας

                          Si à la place de iconv je fais ceci: 

                          $encoding = mb_detect_encoding($cleanedData);
                          if ($encoding) {
                              $cleanedData = mb_convert_encoding($cleanedData, 'UTF-8', $encoding);
                          }

                          Ca ne marchera pas, mb_detect_encoding est donc défaillant à ce niveau or je dois connaitre l'encodage de départ pour réussir, l'utf-16 marchera peut-être pas toujours.


                          Merci pour cette phrase: Word, pour les fichiers "courants", utilise du XML en UTF8 voire même UTF-16



                          Edit:


                          Dans le fichier word(ouvert avec notepad) la section convertit(celle avec laquelle on travaille depuis) a un header qui a pour charset "unicode". J'avais mis de cote cela mais dans mes recherches j'ai vu que unicode => UTF-16 ou UTF-32. Mon probleme est resolu.

                          Voici la classe finale:

                          <?php
                          
                          namespace App\Services\Mhtml;
                          
                          use App\Services\ErrorLogger;
                          use ForceUTF8\Encoding;
                          use PhpMimeMailParser\Parser;
                          
                          
                          class MhtmlProcessor
                          {
                              private array $images;
                              private ?string $decodedBody;
                              private array $unhandledSections;
                              private $mhtmlGenerator;
                          
                              private $parser;
                          
                              private ErrorLogger $logger;
                          
                              public static function headersToString($headers)
                              {
                                  $headersString = "";
                                  foreach ($headers as $key => $value) {
                                      $headersString .= ucfirst($key) . ': ' . $value . "\r\n";
                                  }
                                  return $headersString;
                              }
                          
                              public function __construct(private ?string $content)
                              {
                                  $this->parser = new Parser();
                                  $this->mhtmlGenerator = new MhtmlGenerator();
                                  $this->logger = new ErrorLogger('dev-p.log');
                                  $this->images = [];
                                  $this->unhandledSections = [];
                                  if ($content !== null)
                                      $this->parser->setText($content);
                              }
                          
                              private function clearData()
                              {
                                  $this->images = [];
                                  $this->decodedBody = null;
                                  $this->unhandledSections = [];
                                  $this->parser->__destruct();
                                  $this->mhtmlGenerator->clearParts();
                              }
                          
                              private function convertImage($matches)
                              {
                                  $path = $matches[1];
                                  $this->logger->logError($path);
                          
                                  $image = array_filter($this->images, fn ($k) => str_ends_with($k, $path), ARRAY_FILTER_USE_KEY);
                                  if (!empty($image)) {
                                      $data = $image[0]['data'];
                                      return 'src="data:image/png;base64,' . rtrim($data) . '"';
                                  }
                          
                                  return '';
                              }
                          
                              public function setContent(string $content): void
                              {
                                  $this->clearData();
                                  $this->content = $content;
                                  $this->parser = new Parser();
                                  $this->parser->setText($content);
                              }
                          
                              public function getBody(): ?string
                              {
                                  if ($this->decodedBody == null)
                                      return null;
                          
                                  $cleanedData = $this->decodedBody;
                                  $encoding = mb_detect_encoding($cleanedData, ['UTF-16', 'UTF-32'], strict: true);
                                  if ($encoding) {
                                      $cleanedData = mb_convert_encoding($cleanedData, 'UTF-8', $encoding);
                                      // $cleanedData = iconv("UTF-16", "UTF-8//TRANSLIT//IGNORE", $cleanedData);
                                  }
                          
                                  return $cleanedData;
                              }
                          
                              public function getWordReadableBody(array $bodyPart): string
                              {
                                  $headers = $bodyPart['headers'];
                                  $headers['content-transfer-encoding'] = 'quoted-printable'; // base64 => quoted-printable
                                  $headers['content-type'] = 'text/html; charset="utf-8"'; // unicode => utf-8
                          
                                  // '=' => '=3D'
                                  $body = str_ireplace(
                                      '<meta http-equiv=Content-Type content="text/html; charset=unicode">',
                                      '<meta http-equiv=3DContent-Type content=3D"text/html; charset=3DUTF-8">',
                                      $this->getBody()
                                  );
                          
                                  // Remove xml content in the body header, if not Word will not show the text
                                  $body = preg_replace('/<xml>(.*?)<\/xml>\r\n/s', '', $body);
                                  $body = preg_replace('/xmlns:(.*?)="(.*?)"\r\n/s', '', $body);
                          
                                  // Remove <style> content in the body header, not necessary but make the content clearer in notepad
                                  $body = preg_replace('/<style>(.*?)<\/style>\r\n/s', '', $body);
                                  $body = preg_replace("/style='(.*?)'/s", '', $body);
                          
                                  // Remove all html comments, not necessary
                                  $body = preg_replace('/<!--(.*?)-->/s', '', $body);
                          
                                  // Replace image links by their base64 data -- NOT WORKING NOW --
                                  $body = preg_replace_callback(
                                      '/src="([^"]+)"/',
                                      fn ($_) => $this->convertImage($_),
                                      $body
                                  );
                          
                                  return static::headersToString($headers) . "\r\n" . $body;
                              }
                          
                              public function getExportableContent(): string
                              {
                                  return $this->mhtmlGenerator->generate();
                              }
                          
                              public function process(): void
                              {
                                  foreach ($this->parser->getParts() as $key => $part) {
                                      if ($key === 1) {
                                          $contentTypeHeader = $this->parser->getRawHeader('Content-Type');
                                          preg_match('/boundary=(.*)/', $contentTypeHeader, $matches);
                                          if (isset($matches[1])) {
                                              $boundary = str_replace('"', '', $matches[1]);
                                              $this->mhtmlGenerator->setBoundary($boundary);
                                          }
                          
                                          $this->mhtmlGenerator->addPart($this->parser->getHeadersRaw());
                                          continue;
                                      }
                          
                                      if (
                                          $part['content-type'] == 'text/html'
                                          && $part['charset'] == 'unicode'
                                      ) {
                                          $this->decodedBody = $this->parser->getMessageBody('html');
                                      } elseif (
                                          str_starts_with($part['content-type'], 'image/')
                                          && $part['transfer-encoding'] == 'base64'
                                      ) {
                          
                                          $this->mhtmlGenerator->addPart(substr(
                                              $this->content,
                                              $part['starting-pos'],
                                              $part['ending-pos'] - $part['starting-pos']
                                          ));
                          
                                          $data = substr(
                                              $this->content,
                                              $part['starting-pos-body'],
                                              $part['ending-pos-body'] - $part['starting-pos-body']
                                          );
                                          $this->images[] = [
                                              'data' => $data,
                                              'type' => $part['content-type'],
                                              'encoding' => $part['transfer-encoding'],
                                              'location' => $part['content-location']
                                          ];
                                      } else {
                                          $content = substr(
                                              $this->content,
                                              $part['starting-pos-body'],
                                              $part['ending-pos-body'] - $part['starting-pos-body']
                                          );
                          
                                          $this->mhtmlGenerator->addPart(substr(
                                              $this->content,
                                              $part['starting-pos'],
                                              $part['ending-pos'] - $part['starting-pos']
                                          ));
                          
                                          if (
                                              !str_starts_with($part['content-type'], 'application/')
                                              && $part['transfer-encoding'] == 'base64'
                                          ) {
                                              $content = base64_decode($content, true);
                                              $content = Encoding::fixUTF8($content, Encoding::ICONV_IGNORE);
                                          }
                          
                                          $this->unhandledSections[] = [
                                              'headers' => $part['headers'],
                                              'content' => $content
                                          ];
                                      }
                                  }
                          
                                  $this->mhtmlGenerator->addPart($this->getWordReadableBody($part), true);
                              }
                          }
                          

                          C'est la methode getBody() qui contient le code dont je parles depuis.

                          -
                          Edité par Asmitta 26 mars 2024 à 14:07:57

                          • Partager sur Facebook
                          • Partager sur Twitter

                          Manipulez les fichier MHTML

                          × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                          • Editeur
                          • Markdown