/**
 * http.js : utilitaires pour les requêtes HTTP émises depuis des scripts.
 *
 * Tiré du livre JavaScript — La référence, 5e édition,
 * de David Flanagan. Copyright 2006 Éditions O'Reilly (ISBN : 2-84177-415-5)
 */

// Vérifier que le module n'a pas déjà été chargé.
var HTTP;
if (HTTP && (typeof HTTP != "object" || HTTP.NAME))
    throw new Error("L'espace de noms 'HTTP' existe déjà");

// Créer notre espace de noms et quelques méta-informations.
HTTP = {};
HTTP.NAME = "HTTP";    // Nom de cet espace de noms.
HTTP.VERSION = 1.0;    // Version de cet espace de noms.

// Voici la liste des fonctions de création de XMLHttpRequest à essayer.
HTTP._fabriques = [
    function() { return new XMLHttpRequest(); },
    function() { return new ActiveXObject("Msxml2.XMLHTTP"); },
    function() { return new ActiveXObject("Microsoft.XMLHTTP"); }
];

// Lorsqu’une méthode de fabrique fonctionne, la stocker dans cette variable.
HTTP._fabrique = null;

// Créer et retourner un nouvel objet XMLHttpRequest.
// 
// Au premier appel, essayer la liste des fonctions de fabrique jusqu’à
// ce que l’une d’elles retourne une valeur non nulle et ne lève pas une
// exception. Lorsque nous avons trouvé une méthode qui fonctionne, la
// mémoriser pour son utilisation future.
//
HTTP.nouvelleRequete = function() {
    if (HTTP._fabrique != null) return HTTP._fabrique();

    for(var i = 0; i < HTTP._fabriques.length; i++) {
        try {
            var fabrique = HTTP._fabriques[i];
            var requete = fabrique();
            if (requete != null) {
                HTTP._fabrique = fabrique;
                return requete;
            }
        }
        catch(e) {
            continue;
        }
    }

    // Si nous arrivons ici, c’est qu’aucune des fonctions de fabrique
    // n’a réussit à créer un objet XMLHttpRequest. Nous levons donc une
    // exception pour cet appel et les suivants.
    HTTP._fabrique = function() {
        throw new Error("XMLHttpRequest non pris en charge");
    }
    HTTP._fabrique(); // Générer une erreur.
}

/**
 * Utiliser XMLHttpRequest pour obtenir le contenu de l’URL indiquée en
 * utilisant une requête HTTP GET. Lorsque la réponse arrive, elle est 
 * passée, sous forme de texte brut, à la fonction de rappel.
 * 
 * Cette fonction ne bloque pas et n’a pas de valeur de retour.
 */
HTTP.obtenirTexte = function(url, rappel) {
    var requete = HTTP.nouvelleRequete();
    requete.onreadystatechange = function() {
        if (requete.readyState == 4 && requete.status == 200)
            rappel(requete.responseText);
    }
    requete.open("GET", url);
    requete.send(null);
};

/**
 * Utiliser XMLHttpRequest pour obtenir le contenu de l’URL indiquée en
 * utilisant une requête HTTP GET. Lorsque la réponse arrive, elle est 
 * passée, sous forme d'objet Document XML, à la fonction de rappel.
 * 
 * Cette fonction ne bloque pas et n’a pas de valeur de retour.
 */
HTTP.obtenirXML = function(url, rappel) {
    var requete = HTTP.nouvelleRequete();
    requete.onreadystatechange = function() {
        if (requete.readyState == 4 && requete.status == 200)
            rappel(requete.responseXML);
    }
    requete.open("GET", url);
    requete.send(null);
};

/**
 * Utiliser une requête HTTP HEAD pour obtenir les en-têtes de l’URL
 * indiquée. Lorsque les en-têtes arrivent, ils sont analysés par
 * HTTP.analyserEntetes() et l’objet résultant est passé à la fonction
 * de rappel. Si le serveur renvoie un code d’erreur, la fonction 
 * gererErreur est appelée à la place. Si cette fonction n’est pas
 * appelée, null est passé à la fonction de rappel.
 */
HTTP.obtenirEntetes = function(url, rappel, gererErreur) {
    var requete = HTTP.nouvelleRequete();
    requete.onreadystatechange = function() {
        if (requete.readyState == 4) {
            if (requete.status == 200) {
                rappel(HTTP.analyserEntetes(requete));
            }
            else {
                if (gererErreur) gererErreur(requete.status,
                                             requete.statusText);
                else rappel(null);
            }
        }
    }
    requete.open("HEAD", url);
    requete.send(null);
};

// Analyser les en-têtes de la réponse fournie par un objet XMLHttpRequest
// et retourner les noms et les valeurs des en-têtes comme noms et valeurs
// de propriétés d’un nouvel objet.
HTTP.analyserEntetes = function(requete) {
    var texteEntete = requete.getAllResponseHeaders();   // Texte venant
                                                         // du serveur.
    var entetes = {}; // Notre valeur de retour.
    var ed = /^\s*/;  // Expression régulière pour les espaces de début.
    var ef = /\s*$/;  // Expression régulière pour les espaces de fin.

    // Transformer les en-têtes en lignes.
    var lignes = texteEntete.split("\n");
    // Parcourir les lignes.
    for(var i = 0; i < lignes.length; i++) {
        var ligne = lignes[i];
        if (ligne.length == 0) continue;  // Sauter les lignes vides.
        // Couper chaque ligne sur le premier caractère deux-points et
        // supprimer les espaces de début et de fin.
        var pos = ligne.indexOf(':');     
        var nom = ligne.substring(0, pos).replace(ed, "").replace(ef, "");
        var valeur = ligne.substring(pos+1).replace(ed, "").replace(ef, "");
        // Stocker la paire nom/valeur de l’en-tête dans un objet JavaScript.
        entetes[nom] = valeur;
    }
    return entetes;
};

/**
 * Envoyer une requête HTTP POST à l’URL indiquée. Les noms et les valeurs
 * des propriétés de l’objet valeurs constituent le corps de la requête.
 * La réponse du serveur est analysée en fonction de son type et la valeur
 * résultante est passée à la fonction de rappel. Si une erreur HTTP se
 * produit, la fonction indiquée par gererErreur est invoquée ou, si cette
 * fonction n’est pas définie, null est passé à la fonction de rappel.
 **/
HTTP.post = function(url, valeurs, rappel, gererErreur) {
    var requete = HTTP.nouvelleRequete();
    requete.onreadystatechange = function() {
        if (requete.readyState == 4) {
            if (requete.status == 200) {
                rappel(HTTP._obtenirReponse(requete));
            }
            else {
                if (gererErreur) gererErreur(requete.status,
                                             requete.statusText);
                else rappel(null);
            }
        }
    }

    requete.open("POST", url);
    // Cette en-tête indique au serveur comment interpréter le corps
    // de la requête.
    requete.setRequestHeader("Content-Type",
                             "application/x-www-form-urlencoded");
    // Encoder les propriétés de l’objet valeurs et les envoyer
    // dans le corps de la requête.
    requete.send(HTTP.coderDonneesFormulaire(valeurs));
};

/**
 * Encoder les paires nom/valeur des propriétés d’un objet comme
 * si elles provenaient d’un formulaire HTML, en utilisant un 
 * format application/x-www-form-urlencoded.
 */
HTTP.encoderDonneesFormulaire = function(donnees) {
    var paires = [];
    var regexp = /%20/g; // Expression régulière qui correspond
                         // à un espace encodé.

    for(var nom in donnees) {
        var valeur = donnees[nom].toString();
        // Créer une paire nom/valeur, en commençant par encoder le nom et
        // la valeur. La fonction globale encodeURIComponent effectue cette 
        // opération, mais elle convertit les espaces en %20 et non en "+",
        // contrairement à ce que nous souhaitons. Nous corrigeons cela grâce
        // à la fonction String.replace().
        var paire = encodeURIComponent(nom).replace(regexp,"+") + '=' +
                    encodeURIComponent(valeur).replace(regexp,"+");
        paires.push(paire);
    }

    // Concaténer toutes les paires nom/valeur, en les séparant par "&".
    return paires.join('&');
};

/**
 * Analyser une réponse HTTP en fonction de son en-tête Content-Type et
 * retourner l'objet obtenu.
 */
HTTP._obtenirReponse = function(requete) {
    // Tester le type du contenu retourné par le serveur.
    switch(requete.getResponseHeader("Content-Type")) {
    case "text/xml":
        // S’il s’agit d’un document XML, utilisez l’objet Document créé.
        return requete.responseXML;

    case "text/json":
    case "text/javascript":
    case "application/javascript":
    case "application/x-javascript":
        // Si la réponse contient du code JavaScript ou une valeur au 
        // format JSON, appeler eval() sur le texte afin de le convertir
        // en valeur JavaScript. Note : n’effectuer cette opération que
        // si le code JavaScript provient d’un serveur de confiance !
        return eval(requete.responseText);

    default:
        // Sinon, traiter la réponse comme du texte brut et la retourner
        // sous forme d’une chaîne de caractères.
        return requete.responseText;
    }
};

/**
 * Envoyer une requête HTTP GET pour l’URL indiquée. En cas de succès, la
 * réponse est convertie en un objet dépendant de l’en-tête Content-Type,
 * passé ensuite à la fonction de rappel indiquée. D’autres arguments
 * peuvent être passés comme des propriétés de l’objet options.
 *
 * Si la réponse signale une erreur (par exemple, une erreur 404 Not Found),
 * le code et le message d’état sont passés à la fonction options.gererErreur.
 * Si le gestionnaire d’erreur n’est pas précisé, la fonction de rappel est
 * appelée avec l’argument null.
 * 
 * Si l’objet options.parametres est défini, ses propriétés sont traitées
 * comme les noms et les valeurs des paramètres de la requête. Elles sont
 * converties en une chaîne d’URL avec HTTP.coderDonneesFormulaire() et
 * ajoutées à l’URL après un '?'.
 * 
 * Si une fonction options.gestionnaireProgression est indiquée, elle est
 * appelée chaque fois que la propriété readyState prend une valeur
 * inférieure à 4. Chaque appel de la fonction de gestion de la progression
 * reçoit un entier indiquant le nombre de fois qu’elle a été appelée.
 *
 * Si une valeur options.temporisation est spécifiée, la requête est
 * annulée si elle ne s’est pas terminée avant l’échéance de la durée 
 * indiquée en millisecondes. Si le délai d’attente est écoulé et si
 * options.gestionnaireTemporisation est définie, cette fonction est 
 * appelée avec l’URL demandée en argument.
 **/
HTTP.get = function(url, rappel, options) {
    var requete = HTTP.nouvelleRequete();
    var n = 0;
    var minuterie;
    if (options.temporisation)
        minuterie = setTimeout(
                           function() {
                               requete.abort();
                               if (options.gestionnaireTemporisation)
                                   options.gestionnaireTemporisation(url);
                           },
                           options.temporisation);

    requete.onreadystatechange = function() {
        if (requete.readyState == 4) {
            if (minuterie) clearTimeout(minuterie);
            if (requete.status == 200) {
                rappel(HTTP._obtenirReponse(requete));
            }
            else {
                if (options.gererErreur)
                    options.gererErreur(requete.status,
                                        requete.statusText);
                else rappel(null);
            }
        }
        else if (options.gestionnaireProgression) {
            options.gestionnaireProgression(++n);
        }
    }

    var cible = url;
    if (options.parametres)
        cible += "?" + HTTP.coderDonneesFormulaire(options.parametres)
    requete.open("GET", cible);
    requete.send(null);
};

HTTP.obtenirTexteAvecScript = function(url, rappel) {
    // Créer un nouvel élément <script> et l’ajouter au document.
    var script = document.createElement("script");
    document.body.appendChild(script);

    // Obtenir un nom de fonction unique.
    var nomFonction = "fonction" + HTTP.obtenirTexteAvecScript.compteur++;

    // Définir une fonction de ce nom, dont l’espace de noms est la fonction
    // courante. Le script généré sur le serveur invoque cette fonction.
    HTTP.obtenirTexteAvecScript[nomFonction] = function(texte) {
        // Passer le texte à la fonction de rappel.
        rappel(texte);

        // Effacer la balise <script> et la fonction générée.
        document.body.removeChild(script);
        delete HTTP.obtenirTexteAvecScript[nomFonction];
    }

    // Coder l’URL à charger et le nom de la fonction et placer le résultat
    // dans les arguments du script coté serveur jsquoter.php. Fixer la
    // propriété src de la balise <script> afin de charger l’URL.
    script.src = "jsquoter.php" +
                 "?url=" + encodeURIComponent(url) + "&fonction=" +
                 encodeURIComponent("HTTP.obtenirTexteAvecScript." +
                                    nomFonction);
}

// Nous utilisons cette variable pour générer des fonctions de rappel
// aux noms uniques, pour le cas où plusieurs requêtes seraient émises 
// en même temps.
HTTP.obtenirTexteAvecScript.compteur = 0;
