Comment vérifier le type de fichier MIME avec javascript avant le téléchargement?

177

J'ai lu ceci et cette question qui semble suggérer que le type de fichier MIME pourrait être vérifié en utilisant javascript côté client. Maintenant, je comprends que la vraie validation doit encore être faite côté serveur. Je souhaite effectuer une vérification côté client pour éviter un gaspillage inutile des ressources du serveur.

Pour tester si cela peut être fait côté client, j'ai changé l'extension d'un JPEGfichier de test .pnget choisi le fichier à télécharger. Avant d'envoyer le fichier, j'interroge l'objet fichier à l'aide d'une console javascript:

document.getElementsByTagName('input')[0].files[0];

Voici ce que j'obtiens sur Chrome 28.0:

Fichier {webkitRelativePath: "", lastModifiedDate: Tue 16 Oct 2012 10:00:00 GMT + 0000 (UTC), nom: "test.png", type: "image / png", size: 500055…}

Il indique que le type est, image/pngce qui semble indiquer que la vérification est effectuée en fonction de l'extension de fichier au lieu du type MIME. J'ai essayé Firefox 22.0 et cela me donne le même résultat. Mais selon les spécifications du W3C , le reniflage MIME devrait être implémenté.

Ai-je raison de dire qu'il n'y a aucun moyen de vérifier le type MIME avec javascript pour le moment? Ou est-ce que je manque quelque chose?

Dépassement de question
la source
5
I want to perform a client side checking to avoid unnecessary wastage of server resource.Je ne comprends pas pourquoi vous dites que la validation doit être faite côté serveur, mais que vous voulez ensuite réduire les ressources du serveur. Règle d'or: ne faites jamais confiance aux commentaires des utilisateurs . Quel est l'intérêt de vérifier le type MIME côté client si vous ne faites que le faire côté serveur. C'est sûrement un «gaspillage inutile de ressources client »?
Ian Clark
7
Fournir une meilleure vérification / rétroaction du type de fichier aux utilisateurs côté client est une bonne idée. Cependant, comme vous l'avez indiqué, les navigateurs se basent simplement sur les extensions de fichier pour déterminer la valeur de la typepropriété des Fileobjets. Le code source du webkit, par exemple, révèle cette vérité. Il est possible d'identifier avec précision les fichiers côté client en recherchant, entre autres, des "octets magiques" dans les fichiers. Je travaille actuellement sur une bibliothèque MIT (dans le peu de temps libre dont je dispose) qui fera exactement cela. Si mes progrès vous intéressent, jetez un œil à github.com/rnicholus/determinater .
Ray Nicholus
32
@IanClark, le fait est que si le fichier est d'un type non valide, je peux le rejeter côté client plutôt que de gaspiller la bande passante de téléchargement uniquement pour le rejeter côté serveur.
Question Overflow
@RayNicholus, mec cool! Je regarderai à travers quand j'aurai le temps. Merci :)
Question Overflow
Êtes-vous sûr que votre fichier de test a toujours le type MIME image/jpeget que vous ne l'avez pas réellement modifié en changeant l'extension?
Bergi

Réponses:

344

Vous pouvez facilement déterminer le type de fichier MIME avec JavaScript FileReaderavant de le télécharger sur un serveur. Je conviens que nous devrions préférer la vérification côté serveur plutôt que côté client, mais la vérification côté client est toujours possible. Je vais vous montrer comment et fournir une démo fonctionnelle en bas.


Vérifiez que votre navigateur prend en charge à la fois Fileet Blob. Tous les principaux devraient.

if (window.FileReader && window.Blob) {
    // All the File APIs are supported.
} else {
    // File and Blob are not supported
}

Étape 1:

Vous pouvez récupérer les Fileinformations d'un <input>élément comme celui-ci ( ref ):

<input type="file" id="your-files" multiple>
<script>
var control = document.getElementById("your-files");
control.addEventListener("change", function(event) {
    // When the control has changed, there are new files
    var files = control.files,
    for (var i = 0; i < files.length; i++) {
        console.log("Filename: " + files[i].name);
        console.log("Type: " + files[i].type);
        console.log("Size: " + files[i].size + " bytes");
    }
}, false);
</script>

Voici une version glisser-déposer de ce qui précède ( ref ):

<div id="your-files"></div>
<script>
var target = document.getElementById("your-files");
target.addEventListener("dragover", function(event) {
    event.preventDefault();
}, false);

target.addEventListener("drop", function(event) {
    // Cancel default actions
    event.preventDefault();
    var files = event.dataTransfer.files,
    for (var i = 0; i < files.length; i++) {
        console.log("Filename: " + files[i].name);
        console.log("Type: " + files[i].type);
        console.log("Size: " + files[i].size + " bytes");
    }
}, false);
</script>

Étape 2:

Nous pouvons maintenant inspecter les fichiers et découvrir les en-têtes et les types MIME.

✘ Méthode rapide

Vous pouvez naïvement demander à Blob le type MIME du fichier qu'il représente en utilisant ce modèle:

var blob = files[i]; // See step 1 above
console.log(blob.type);

Pour les images, les types MIME reviennent comme suit:

image / image jpeg
/ png
...

Attention: le type MIME est détecté à partir de l'extension de fichier et peut être trompé ou usurpé. On peut renommer a .jpgen a .pnget le type MIME sera signalé comme image/png.


✓ Méthode appropriée d'inspection de l'en-tête

Pour obtenir le type MIME de bonne foi d'un fichier côté client, nous pouvons aller plus loin et inspecter les premiers octets du fichier donné pour les comparer aux soi-disant nombres magiques . Soyez averti que ce n'est pas tout à fait simple car, par exemple, JPEG a quelques «nombres magiques». C'est parce que le format a évolué depuis 1991. Vous pourriez vous en tirer en ne vérifiant que les deux premiers octets, mais je préfère vérifier au moins 4 octets pour réduire les faux positifs.

Exemple de signatures de fichier JPEG (4 premiers octets):

FF D8 FF E0 (SOI + ADD0)
FF D8 FF E1 (SOI + ADD1)
FF D8 FF E2 (SOI + ADD2)

Voici le code indispensable pour récupérer l'en-tête du fichier:

var blob = files[i]; // See step 1 above
var fileReader = new FileReader();
fileReader.onloadend = function(e) {
  var arr = (new Uint8Array(e.target.result)).subarray(0, 4);
  var header = "";
  for(var i = 0; i < arr.length; i++) {
     header += arr[i].toString(16);
  }
  console.log(header);

  // Check the file signature against known types

};
fileReader.readAsArrayBuffer(blob);

Vous pouvez ensuite déterminer le type MIME réel comme ceci (plus de signatures de fichiers ici et ici ):

switch (header) {
    case "89504e47":
        type = "image/png";
        break;
    case "47494638":
        type = "image/gif";
        break;
    case "ffd8ffe0":
    case "ffd8ffe1":
    case "ffd8ffe2":
    case "ffd8ffe3":
    case "ffd8ffe8":
        type = "image/jpeg";
        break;
    default:
        type = "unknown"; // Or you can use the blob.type as fallback
        break;
}

Acceptez ou rejetez les téléchargements de fichiers à votre guise en fonction des types MIME attendus.


Démo

Voici une démo fonctionnelle pour les fichiers locaux et distants (j'ai dû contourner CORS juste pour cette démo). Ouvrez l'extrait de code, exécutez-le et vous devriez voir trois images distantes de types différents affichés. En haut, vous pouvez sélectionner une image locale ou un fichier de données, et la signature du fichier et / ou le type MIME seront affichés.

Notez que même si une image est renommée, son vrai type MIME peut être déterminé. Voir ci-dessous.

Capture d'écran

Sortie attendue de la démo


Drakes
la source
8
2 commentaires mineurs. (1) Ne serait-il pas préférable de découper le fichier selon ses 4 premiers octets avant la lecture? fileReader.readAsArrayBuffer(blob.slice(0,4))? (2) Afin de copier / coller des signatures de fichiers, l'en-tête ne devrait-il pas être construit avec des 0 en tête for(var i = 0; i < bytes.length; i++) { var byte = bytes[i]; fileSignature += (byte < 10 ? "0" : "") + byte.toString(16); }?
Matthew Madson
1
@Deadpool Voir ici . Il existe des formats JPEG plus nombreux, moins courants, de différents fabricants. Par exemple, FF D8 FF E2= CANNON EOS JPEG FILE, FF D8 FF E3= SAMSUNG D500 JPEG FILE. La partie clé de la signature JPEG n'est que de 2 octets, mais pour réduire les faux positifs, j'ai ajouté les signatures de 4 octets les plus courantes. J'espère que cela aide.
Drakes
24
La qualité de cette réponse est tout simplement incroyable.
Luca
2
Il n'est pas nécessaire de charger l'objet blob complet en tant qu'ArrayBuffer pour déterminer le mimeType. Vous pouvez simplement découper et passer les 4 premiers octets du blob comme ceci:fileReader.readAsArrayBuffer(blob.slice(0, 4))
codeVerine
2
Quelle devrait être la vérification pour autoriser uniquement le texte brut? Les 4 premiers octets des fichiers texte correspondent aux 4 premiers caractères du fichier texte.
MP Droid
19

Comme indiqué dans d'autres réponses, vous pouvez vérifier le type mime en vérifiant la signature du fichier dans les premiers octets du fichier.

Mais ce que font les autres réponses, c'est de charger le fichier entier en mémoire afin de vérifier la signature, ce qui est très coûteux et pourrait facilement geler votre navigateur si vous sélectionnez un gros fichier par accident ou non.

/**
 * Load the mime type based on the signature of the first bytes of the file
 * @param  {File}   file        A instance of File
 * @param  {Function} callback  Callback with the result
 * @author Victor www.vitim.us
 * @date   2017-03-23
 */
function loadMime(file, callback) {
    
    //List of known mimes
    var mimes = [
        {
            mime: 'image/jpeg',
            pattern: [0xFF, 0xD8, 0xFF],
            mask: [0xFF, 0xFF, 0xFF],
        },
        {
            mime: 'image/png',
            pattern: [0x89, 0x50, 0x4E, 0x47],
            mask: [0xFF, 0xFF, 0xFF, 0xFF],
        }
        // you can expand this list @see https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
    ];

    function check(bytes, mime) {
        for (var i = 0, l = mime.mask.length; i < l; ++i) {
            if ((bytes[i] & mime.mask[i]) - mime.pattern[i] !== 0) {
                return false;
            }
        }
        return true;
    }

    var blob = file.slice(0, 4); //read the first 4 bytes of the file

    var reader = new FileReader();
    reader.onloadend = function(e) {
        if (e.target.readyState === FileReader.DONE) {
            var bytes = new Uint8Array(e.target.result);

            for (var i=0, l = mimes.length; i<l; ++i) {
                if (check(bytes, mimes[i])) return callback("Mime: " + mimes[i].mime + " <br> Browser:" + file.type);
            }

            return callback("Mime: unknown <br> Browser:" + file.type);
        }
    };
    reader.readAsArrayBuffer(blob);
}


//when selecting a file on the input
fileInput.onchange = function() {
    loadMime(fileInput.files[0], function(mime) {

        //print the output to the screen
        output.innerHTML = mime;
    });
};
<input type="file" id="fileInput">
<div id="output"></div>

Vitim.us
la source
Je pense que ce readyStatesera toujours FileReader.DONEdans le gestionnaire d'événements ( spécification W3C ) même s'il y avait une erreur - la vérification ne devrait-elle pas être si à la (!e.target.error)place?
boycy
5

Pour tous ceux qui cherchent à ne pas l'implémenter eux-mêmes, Sindresorhus a créé un utilitaire qui fonctionne dans le navigateur et qui a les mappages d'en-tête-mime pour la plupart des documents que vous pourriez souhaiter.

https://github.com/sindresorhus/file-type

Vous pouvez combiner la suggestion de Vitim.us de ne lire que les X premiers octets pour éviter de tout charger en mémoire avec l'utilisation de cet utilitaire (exemple dans es6):

import fileType from 'file-type'; // or wherever you load the dependency

const blob = file.slice(0, fileType.minimumBytes);

const reader = new FileReader();
reader.onloadend = function(e) {
  if (e.target.readyState !== FileReader.DONE) {
    return;
  }

  const bytes = new Uint8Array(e.target.result);
  const { ext, mime } = fileType.fromBuffer(bytes);

  // ext is the desired extension and mime is the mimetype
};
reader.readAsArrayBuffer(blob);
Vinay
la source
Pour moi, la dernière version de la bibliothèque ne fonctionnait pas mais cela "file-type": "12.4.0"fonctionnait et je devais l'utiliserimport * as fileType from "file-type";
ssz
4

Si vous voulez juste vérifier si le fichier téléchargé est une image, vous pouvez simplement essayer de le charger dans la <img>balise pour vérifier tout rappel d'erreur.

Exemple:

var input = document.getElementsByTagName('input')[0];
var reader = new FileReader();

reader.onload = function (e) {
    imageExists(e.target.result, function(exists){
        if (exists) {

            // Do something with the image file.. 

        } else {

            // different file format

        }
    });
};

reader.readAsDataURL(input.files[0]);


function imageExists(url, callback) {
    var img = new Image();
    img.onload = function() { callback(true); };
    img.onerror = function() { callback(false); };
    img.src = url;
}
Roberto14
la source
1
Fonctionne très bien, j'ai essayé un hack de téléchargement de fichier .gif et il a jeté une erreur :)
pathfinder
4

C'est ce que tu dois faire

var fileVariable =document.getElementsById('fileId').files[0];

Si vous souhaitez vérifier les types de fichiers image,

if(fileVariable.type.match('image.*'))
{
 alert('its an image');
}
Kailas
la source
Ne fonctionne actuellement pas pour: Firefox pour Android, Opera pour Android et Safari sur iOS. developer.mozilla.org/en-US/docs/Web/API/File/type
Reid
3

Voici une implémentation de Typescript qui prend en charge webp. Ceci est basé sur la réponse JavaScript de Vitim.us.

interface Mime {
  mime: string;
  pattern: (number | undefined)[];
}

// tslint:disable number-literal-format
// tslint:disable no-magic-numbers
const imageMimes: Mime[] = [
  {
    mime: 'image/png',
    pattern: [0x89, 0x50, 0x4e, 0x47]
  },
  {
    mime: 'image/jpeg',
    pattern: [0xff, 0xd8, 0xff]
  },
  {
    mime: 'image/gif',
    pattern: [0x47, 0x49, 0x46, 0x38]
  },
  {
    mime: 'image/webp',
    pattern: [0x52, 0x49, 0x46, 0x46, undefined, undefined, undefined, undefined, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50],
  }
  // You can expand this list @see https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
];
// tslint:enable no-magic-numbers
// tslint:enable number-literal-format

function isMime(bytes: Uint8Array, mime: Mime): boolean {
  return mime.pattern.every((p, i) => !p || bytes[i] === p);
}

function validateImageMimeType(file: File, callback: (b: boolean) => void) {
  const numBytesNeeded = Math.max(...imageMimes.map(m => m.pattern.length));
  const blob = file.slice(0, numBytesNeeded); // Read the needed bytes of the file

  const fileReader = new FileReader();

  fileReader.onloadend = e => {
    if (!e || !fileReader.result) return;

    const bytes = new Uint8Array(fileReader.result as ArrayBuffer);

    const valid = imageMimes.some(mime => isMime(bytes, mime));

    callback(valid);
  };

  fileReader.readAsArrayBuffer(blob);
}

// When selecting a file on the input
fileInput.onchange = () => {
  const file = fileInput.files && fileInput.files[0];
  if (!file) return;

  validateImageMimeType(file, valid => {
    if (!valid) {
      alert('Not a valid image file.');
    }
  });
};

<input type="file" id="fileInput">

Eric Coulthard
la source
1

Comme Drake le déclare, cela pourrait être fait avec FileReader. Cependant, ce que je présente ici est une version fonctionnelle. Tenez compte du fait que le gros problème avec JavaScript est de réinitialiser le fichier d'entrée. Eh bien, cela se limite uniquement au JPG (pour les autres formats, vous devrez changer le type mime et le nombre magique ):

<form id="form-id">
  <input type="file" id="input-id" accept="image/jpeg"/>
</form>

<script type="text/javascript">
    $(function(){
        $("#input-id").on('change', function(event) {
            var file = event.target.files[0];
            if(file.size>=2*1024*1024) {
                alert("JPG images of maximum 2MB");
                $("#form-id").get(0).reset(); //the tricky part is to "empty" the input file here I reset the form.
                return;
            }

            if(!file.type.match('image/jp.*')) {
                alert("only JPG images");
                $("#form-id").get(0).reset(); //the tricky part is to "empty" the input file here I reset the form.
                return;
            }

            var fileReader = new FileReader();
            fileReader.onload = function(e) {
                var int32View = new Uint8Array(e.target.result);
                //verify the magic number
                // for JPG is 0xFF 0xD8 0xFF 0xE0 (see https://en.wikipedia.org/wiki/List_of_file_signatures)
                if(int32View.length>4 && int32View[0]==0xFF && int32View[1]==0xD8 && int32View[2]==0xFF && int32View[3]==0xE0) {
                    alert("ok!");
                } else {
                    alert("only valid JPG images");
                    $("#form-id").get(0).reset(); //the tricky part is to "empty" the input file here I reset the form.
                    return;
                }
            };
            fileReader.readAsArrayBuffer(file);
        });
    });
</script>

Tenez compte du fait que cela a été testé sur les dernières versions de Firefox et Chrome, et sur IExplore 10.

Pour une liste complète des types de mime, consultez Wikipedia .

Pour une liste complète des nombres magiques, consultez Wikipedia .

lmiguelmh
la source
Les liens Wikipedia ci-dessus ne sont plus valides.
Bob Quinn
@BobQuinn corrigé, merci
lmiguelmh
0

Voici une extension de la réponse de Roberto14 qui fait ce qui suit:

CECI N'AUTORISE QUE DES IMAGES

Vérifie si FileReader est disponible et revient à la vérification d'extension s'il n'est pas disponible.

Donne une alerte d'erreur si ce n'est pas une image

S'il s'agit d'une image, il charge un aperçu

** Vous devez toujours faire la validation côté serveur, c'est plus une commodité pour l'utilisateur final qu'autre chose. Mais c'est pratique!

<form id="myform">
    <input type="file" id="myimage" onchange="readURL(this)" />
    <img id="preview" src="#" alt="Image Preview" />
</form>

<script>
function readURL(input) {
    if (window.FileReader && window.Blob) {
        if (input.files && input.files[0]) {
            var reader = new FileReader();
            reader.onload = function (e) {
                var img = new Image();
                img.onload = function() {
                    var preview = document.getElementById('preview');
                    preview.src = e.target.result;
                    };
                img.onerror = function() { 
                    alert('error');
                    input.value = '';
                    };
                img.src = e.target.result;
                }
            reader.readAsDataURL(input.files[0]);
            }
        }
    else {
        var ext = input.value.split('.');
        ext = ext[ext.length-1].toLowerCase();      
        var arrayExtensions = ['jpg' , 'jpeg', 'png', 'bmp', 'gif'];
        if (arrayExtensions.lastIndexOf(ext) == -1) {
            alert('error');
            input.value = '';
            }
        else {
            var preview = document.getElementById('preview');
            preview.setAttribute('alt', 'Browser does not support preview.');
            }
        }
    }
</script>
éclaireur
la source
-1

La réponse courte est non.

Comme vous le constatez, les navigateurs dérivent typede l'extension de fichier. L'aperçu Mac semble également fonctionner avec l'extension. Je suppose que c'est parce qu'il lit plus rapidement le nom du fichier contenu dans le pointeur, plutôt que de rechercher et de lire le fichier sur le disque.

J'ai fait une copie d'un jpg renommé avec png.

J'ai pu obtenir systématiquement ce qui suit à partir des deux images dans Chrome (devrait fonctionner dans les navigateurs modernes).

ÿØÿàJFIFÿþ;CREATOR: gd-jpeg v1.0 (using IJG JPEG v62), quality = 90

Ce que vous pourriez pirater un contrôle String.indexOf ('jpeg') pour le type d'image.

Voici un violon à explorer http://jsfiddle.net/bamboo/jkZ2v/1/

La ligne ambiguë que j'ai oublié de commenter dans l'exemple

console.log( /^(.*)$/m.exec(window.atob( image.src.split(',')[1] )) );

  • Divise les données img encodées en base64, laissant sur l'image
  • Base64 décode l'image
  • Correspond uniquement à la première ligne des données d'image

Le code violon utilise le décodage base64 qui ne fonctionnera pas dans IE9, j'ai trouvé un bel exemple utilisant le script VB qui fonctionne dans IE http://blog.nihilogic.dk/2008/08/imageinfo-reading-image-metadata-with.html

Le code pour charger l'image a été pris de Joel Vardy, qui fait un redimensionnement de la toile d'image sympa côté client avant le téléchargement, ce qui peut être intéressant https://joelvardy.com/writing/javascript-image-upload

Lex
la source
1
Veuillez ne pas rechercher dans les fichiers JPEG la sous-chaîne "jpeg", c'est juste une coïncidence si vous l'avez trouvée dans un commentaire. Les fichiers JPEG n'ont pas besoin de le contenir (et si vous envisagez de rechercher à la JFIFplace, il APP0n'est pas nécessaire de contenir JFIF dans les fichiers EXIF-JPEG, donc c'est aussi le cas).
Kornel le
Voir en haut "La réponse courte est non".
Lex