/**
 * Surcouche à l'objet XMLHttpRequest
 * Pour éviter des conflits lors d'appels multiples à l'objet, créez un objet par demande.
 * 
 * Notes : Dû à l'objet en lui même, toutes les informations sont envoyées avec le jeu de caractère utf-8.
 *         Côté serveur : - utilisez utf8_decode() pour retrouver les caractères spéciaux envoyé par l'objet xmlhttp
 *                        - utilisez utf8_encode() pour envoyer les réponses à l'objet xmlhttp
 *  
 * <CODE>
 * 
 *  // Initialisation de l'objet
 *  var client = new oXmlHttpRequest();
 *      
 *  // Création d'un gestionnaire suivant le statut de l'objet xmlHttp.
 *  // Trois évènements sont disponibles : onInit, onError, onCompleted
 *  var xmlHttpHandler = {
 *      
 *      onInit: function() {
 *          // Action après initialisation de l'objet        
 *      },
 *          
 *      onError: function(status, statusText) {
 *          // Action après erreur.
 *          // Status contient le code de l'erreur
 *          // StatusText contient le libellé de l'erreur
 *      },
 *          
 *      onCompleted: function (result) {
 *          // Action après retour du serveur. "result" contient les données renvoyées par le serveur
 *      }
 *  }
 *  
 *  with (client) {
 *      // Initialisation des propriétés de l'objet
 *      openMethod  = "GET";
 *      serverFile  = "fichier_serveur.php";
 *      userhandler = xmlHttpHandler; // La variable créée un peu plus tôt
 *      params      = new Array(); // Il est préférable de toujours réinitialiser les paramètres du tableau
 *      
 *      // Pour ajouter des paramètres, passez par la méthode "addParam"
 *      addParam("variable1", "valeur1");
 *      addParam("variable2", "valeur2");
 *          
 *      // Appel à la méthode d'envoie de la requête
 *      send();
 *  }
 * 
 * </CODE>
 * 
 * v1.0j 17/11/2008
 *
 * Historique :
 *      - v1.0j :
 *                * Modification du tableau des paramètres pour compatibilité prototype.js et consors...
 *      - v1.0i :
 *                * Prise en compte des entêtes renvoyés par le serveur contenant le type de charset utilisé (causant un bug dans la méthode analysant dynamiquement
 *                  le retour de valeur du serveur @see retrieveData()).
 *      - v1.0h :
 *                * Correction d'un bogue lançant 2 fois successivement l'appel à la fonction xmlHttpHandler.onInit().
 *                * Suppression du mode "debug", "alertParam", la méthode "reset" n'est gardé qu'au titre de compatbilité descendante.
 *      - v1.0g :
 *                * Interception de l'erreur NS_ERROR_NOT_AVAILABLE (cf. switchStatus()).
 *      - v1.0f :
 *                * Mise en place du pattern de Peter Michaux [http://peter.michaux.ca/article/3556] : évite de repasser dans le try/captch après un premier appel à la méthode init().
 *      - v1.0e :
 *                * ajout entête pour empêcher la mise en cache par le browser
 *                * ajout méthode JSON lors de la récupération des données
 *                * suppression de quelques fermetures (closures) et fuites mémoires (memory leak)
 *                * analyse des entêtes renvoyés par le serveur pour ne plus avoir à préciser la propriété "receiveDataMode". Liste des entêtes interprétés :
 *                      - 'application/json', text/json', 'text/javascript', 'application/javascript', 'application/x-javascript'
 *                      - 'text/html'
 *                      - 'text/xml'
 *                  Par défaut, le mode text/html est utilisé. L'analyse des entêtes peut être contournée (forcée donc) en renseignant la propriété "receiveDataMode" (JSON/TEXT/XML).
 *
 *      - v1.0d :
 *                * ajout de la méthode "reset"
 *                * le tableau des paramètres est réinitialisé à vide après un envoi.
 *
 *      - v1.0c :
 *                * changement de la fonction d'encodage des caractères dans la méthode buildUrl : escape() est remplacée par encodeURIComponent(). Voir ici pour
 *                  les explications : http://xkr.us/articles/javascript/encode-compare/
 * 
 *      - v1.0b :
 *                * correction bug mineur. La méthode buildUrl concaténait les propriétés serverFile et data. Or, la méthode prepare concaténait à nouveau ces variables.
 *                * La méthode buildUrl ne s'occupe désormait plus de l'association entre les propriétés serverFile et data.
 */


/** 
 * Constructeur
 *
 * @access  private
 * @param   none
 * @return  void
 */
function oXmlHttpRequest() {
    this.__construct();
}

oXmlHttpRequest.prototype = {
    
    // @access public
    openMethod      : "POST",       // Mode de transmission des données ("POST" || "GET")
    serverFile      : "",           // url du fichier auquel on souhaite transmettre les données
    username        : null,         // Nom d'utilisateur pour identification si nécessaire
    password        : null,         // Mot de passe pour identification si necessaire
    async           : true,         // Mode de transmission des données synchrones ou asynchrones
    userhandler     : null,         // Fonction de callback (Object Initializers)
    receiveDataMode : null,         // Mode de retour des données ("XML" || "TEXT" || "JSON")
    forceParam      : false,        // Ignore la regexp vérifiant les paramètres (méthode addParam()).
    
    // @access private (doivent être positionnées à leurs valeurs par défaut dans la méthode "__destruct()")
    data            : '',           // Données à transmettre sous la forme var1=value1&var2=value2...
    params          : [],           // Tableau des données à transmettre. La structure, dû à des problèmes avec les composants étendant les objets comme [prototype.js] a été modifié comme suit:
                                    // params[0][0] => nom du paramètre
                                    // params[0][1] => valeur du paramètre
    onInitFlag      : false,        // Cf. stateChangeCallback().

    
    
    /** 
     * Création de l'objet XMLHttpRequest
     * L'objet est accessible via "this.xmlhttp"
     * 
     * @access  private 
     * @param   none    
     * @return  bool    True en cas de succès
     */
    __construct : function() {
        
        try {
            
            // Mozilla / Safari / IE7
            this.xmlhttp = new XMLHttpRequest();
            
            // Les explications sont ici : http://www.dustindiaz.com/faster-ajax/
            oXmlHttpRequest.prototype.init = function() {
                this.xmlhttp = new XMLHttpRequest();
                return true;
            };
            
        } catch (e) {
            
            // IE 5.x et 6.x
            var MSXML_XMLHTTP_PROGIDS = new Array(
                'MSXML2.XMLHTTP.5.0',
                'MSXML2.XMLHTTP.4.0',
                'MSXML2.XMLHTTP.3.0',
                'MSXML2.XMLHTTP',
                'Microsoft.XMLHTTP'
            );
            
            var success = false;
            
            for (var i=0;i < MSXML_XMLHTTP_PROGIDS.length && !success; i++) {
                try {
                    this.xmlhttp = new ActiveXObject(MSXML_XMLHTTP_PROGIDS[i]);
                    success      = true;
                } catch (e) {}
            }
            
            if ( !success ) {
                throw new Error("Création de l'objet XmlHttpRequest impossible.");
                return false;
            }
        }
        
        return true;
        
    }, // Fin méthode Init
    
    
    
    /** 
     * Ajoute un paramètre au tableau "this.params".
     * "this.params" contiendra l'ensemble des paramètres que l'on souhaite passer au serveur.
     * 
     * @access  public
     * @param   string  name    Nom de la nouvelle variable
     * @param   string  value   Valeur de la variable
     * @return  void
     */
    addParam: function(name, value) {
        
        if (this.forceParam == true) {
            this.params.push(new array(name, value));
        }
        else {        
            var illegal = /[\W]/;
        
            if (!illegal.test(name) ) this.params.push(new Array(name, value));
            else                      alert('Paramètre invalide ' + name);
        }
        
    }, // Fin méthode addParam
    
    
    
    /** 
     * Construction de la chaîne des données en fonction des valeurs contenues dans la propriété 'this.params'
     * 
     * @access  private
     * @param   none
     * @return  void
     */
    buildUrl: function() {
        
        var sep = this.data = '';
        
        for (var i=0; i<this.params.length; i++) {
        
            var item = this.params[i];
            var paramName  = item[0];
            var paramValue = item[1];
            
            if (encodeURIComponent) paramValue = encodeURIComponent(paramValue);
            else if (escape)        paramValue = escape(paramValue);
            
            // Tjrs passer par encodeURIComponent (sinon certains symbole comme le "+" rendent le composant xmlHttpRequest inactif)
            this.data += sep + paramName + '=' + paramValue;
            sep = '&';
        }
        
        // En méthode "get", il faut prendre garde à la terminaison de la variable serverFile qui peut déjà contenir des valeurs.
        if (this.data.length != '' && this.openMethod == 'GET') {
            if ( this.serverFile.lastIndexOf('?') == -1 ) this.data = '?' + this.data;
            else                                          this.data = '&' + this.data;
        };
        
    }, // Fin méthode builUrl
    
    
    
    /** 
     * Initialisation d'une requête
     * 
     * @access  private
     * @param   none
     * @return  void
     */
    prepare: function() {
        
        this.buildUrl();
        
        /** 
         * Arguments: 
         *  method: The HTTP method, for example "POST" or "GET". Ignored if the URL is not a HTTP(S) URL. 
         *  url: The URL to which to send the request. 
         *  async: Whether the request is synchronous or asynchronous i.e. whether send returns only after the response is received or if it returns immediately after sending the request. In the latter case, notification of completion is sent through the event listeners. This argument must be true if the multipart attribute has been set to true, or an exception will be thrown. 
         *  user: A username for authentication if necessary. 
         *  password: A password for authentication if necessary
         */
        if (this.openMethod == 'GET') this.xmlhttp.open("GET", this.serverFile + this.data, this.async, this.username, this.password);
        else                          this.xmlhttp.open("POST", this.serverFile, this.async, this.username, this.password);
        
    }, // Fin méthode prepare
    
    
    
    /** 
     * Méthode appelée sur l'évènement onreadystatechange de l'objet xmlhttp.
     * 
     * @access  private
     * @return  void
     */ 
    stateChangeCallback: function() {
        
        switch (this.xmlhttp.readyState) {
            
            // open() n'a pas encore été appelé.
            case 0 : 
                // Ne rien faire
            break;
            
            // open() a été appelé, mais send() n'a pas encore été appelé.
            case 1 :
                
                /**
                 * onInitFlag sert à corriger un bogue (des navigateurs) exécutant deux fois l'évènement onreadystatechange avec le même code "1"
                 * => ce qui a pour effet d'exécuter 2 fois la méthode utilisateur "xmlHttpHandler.onInit()".
                 * 
                 * Cf. http://www.quirksmode.org/blog/archives/2005/09/xmlhttp_notes_r_2.html :
                 * xmlhttp.onreadystatechange = checkData;
                 * xmlhttp.open("GET","somepage.xml",true);
                 * xmlhttp.send(null);
                 * Results:
                 *      Explorer: 1-1-2-3-4
                 *      Mozilla: 1-1-2-3-4
                 *      Safari: 1-2-3-4 (correct)
                 *      Opera: 1-3-4
                 * 
                 * All browsers now add an extra readyState of 1 at the start of their sequence. Explorer and Mozilla give a "1" alert twice, and that's odd, because the event name is clearly readystatechange and a change from 1 to 1 is no change.
                 */
                if (typeof this.userhandler.onInit == "function" && this.onInitFlag == false) {
                    this.userhandler.onInit();
                }
                
                this.onInitFlag = true;
                
            break;

            case 2 : // send() a été appelé, mais le serveur n'a pas encore répondu.
            case 3 : // Les données sont en cours de réception sur le serveur. (readyState 3 differs somewhat in Firefox and Internet Explorer)
                // Pas supporté par IE
            break;

            // Le serveur a terminé d'envoyer la réponse.
            case 4 :
                this.switchStatus.apply(this, arguments);
            break;
            
        }
    },
    
    
    
    /**
     * Méthode appélée en interne par la méthode "stateChangeCallback" si la propriété "onreadysatechange"
     * de l'objet xmlhttprequest vaut "4" (completed).
     * 
     * En gros, si ça se passe bien on envoie les données au client en fonction de la propriété "receiveDataMode" à la méthode "onCompleted"
     * de l'objet "userhandler",
     * Sinon on affiche le message retourné par l'objet client à la méthode "onError" de l'objet "userhandler"
     * 
     * Liste des valeurs possible pour "status" dispo ici : "http://msdn.microsoft.com/library/default.asp?url=/library/en-us/xmlsdk/html/xmprostatusserverxmlhttp.asp"
     * 
     * @access private
     * @return void
     */
    switchStatus: function () {
        
        /**
         * NS_ERROR_NOT_AVAILABLE
         * This error took a little investigation on my part. So I did a search on bugzilla and came up with this link:https://bugzilla.mozilla.org/show_bug.cgi?id=238559#c0
         * So you do not have to search through the whole page I posted the problem with Mozilla and why it causes the error:
         * <quote from https://bugzilla.mozilla.org/show_bug.cgi?id=238559#c0>
         *   Mozilla calls onload() for all HTTP transactions that succeeded. The only time it calls onerror() is when a network error happened. Inside the onerror
         *   handler, accessing the status attribute results in this exception:
         * 
         *    Error: [Exception... "Component returned failure code: 0x80040111
         *    (NS_ERROR_NOT_AVAILABLE) [nsIXMLHttpRequest.status]"  nsresult: "0x80040111
         *    (NS_ERROR_NOT_AVAILABLE)" {...}
         * </quote>
         */
        try {
            statusCode = this.xmlhttp.status;
        } catch(e) {
            this.__destruct();
            return false;
        }
        
        switch (statusCode) {
        
            // OK
            case 200 :
            
                if (typeof(this.userhandler.onCompleted) == "function") {
                    this.userhandler.onCompleted(this.retrieveData());
                }
                
                this.__destruct();
                
            break;
            
            // Cas spécial pour IE sur les requêtes ayant échouées
            case 0 :
                // Ne rien faire
            break;
            
            // On envoie le message d'erreur
            default :
                
                if (typeof(this.userhandler.onError) == "function") {
                    this.userhandler.onError(this.xmlhttp.status, this.xmlhttp.statusText);
                }
                
            break;
        }
        
    }, // Fin méthode switchStatus
    

    
    /**
     * Intercepte les données renvoyées par le serveur.
     * 
     * @access  private
     * @return  string
     */
    retrieveData: function() {
        
        // Emploi de la fonction split() pour le cas où "Content-Type" est formatté avec le jeu de caractère (ex : [text/json; charset:utf-8])
        var responseHeader = this.xmlhttp.getResponseHeader('Content-Type').split(';')[0].toLowerCase();
        
        // Le mode de retour est forcé par l'utilisateur, on ne tient pas compte des entêtes renvoyés par le serveur
        if (this.receiveDataMode != null) {
        
                 if (this.receiveDataMode == "TEXT") responseHeader = 'text/html';
            else if (this.receiveDataMode == "JSON") responseHeader = 'application/json';
            else                                     responseHeader = 'text/xml';
        }
        
        // Analyse des entêtes retournés par le serveur pour entreprendre les bonnes actions.
        switch(responseHeader) {
            
            case 'text/xml' :
                return this.xmlhttp.responseXML;
            break;
            
            case 'application/json' :
            case 'text/json' :
            case 'text/javascript' :
            case 'application/javascript' :
            case 'application/x-javascript' :
                return eval('(' + this.xmlhttp.responseText + ')');
            break;
            
            default:
                return this.xmlhttp.responseText;
            break;
        }
        
    }, // Fin méthode retrieveData
    
    
    
    /**
     * Initialisation de la fonction de callBack pour l'évènement "onreadystatechange".
     * Lors d'un changement d'état, la méthode "stateChangeCallback" sera appelée.
     * 
     * @access  private
     * @param   none
     * @return  void
     */
    initOnReadyStateChange: function() {
        
        // Seul la méthode async permet d'utiliser l'évènement onreadystatechange.
        if (this.async === true) {
            
            var self = this;
            
            this.xmlhttp.onreadystatechange = function() {
                self.stateChangeCallback();
            }
        }
    },
    
    
    
    /**
     * Lance les procédures d'initialisation de l'objet, de préparation des donneés et d'envoi de la requête.
     *
     * @access  public
     * @param   none
     * @return  void
     */
    send: function() {
        
        // Vérification des paramètres
        try {
            this.checkParams();
        }
        catch(e) {
            throw new Error(e);
            return false;
        }
        
        // Si l'objet n'existe pas encore, il serait temps de le créer.
        if (!this.xmlhttp) {
            this.__construct();
        }
        
        this.initOnReadyStateChange();
        
        // Initialisation de la requête
        this.prepare();
        
        // Annuler le cache du navigateur
        this.xmlhttp.setRequestHeader('If-Modified-Since', 'Thu, 06 Apr 2006 00:00:00 GMT');
        
        if (this.openMethod == "GET") {
            // Les données sont contenues directement dans l'url (propriété serverFile). Envoi direct.
            this.xmlhttp.send(null);
        }
        else {
            // Modification des entêtes
            this.xmlhttp.setRequestHeader('Content-Type'  , 'application/x-www-form-urlencoded');
            this.xmlhttp.setRequestHeader('Content-Length', this.data.length);
            
            // Envoi
            this.xmlhttp.send(this.data);
        }
        
    }, // Fin méthode send;
    
    
    
    /**
     * Vérification des propriétés de l'objet
     * 
     * @access  private
     * @param   none
     * @return  void
     */
    checkParams: function() {
        
        if (this.openMethod !== "GET" && this.openMethod !== "POST") {
            throw new Error("Propriété [openMethod] invalide");
        }
        
        if (this.serverFile == "") {
            throw "Propriété [serverFile] invalide";
        }
        
        if (this.async !== true && this.async !== false) {
            throw new Error("Propriété [async] invalide");
        }
        
        if (this.receiveDataMode !== null && this.receiveDataMode !== "TEXT" && this.receiveDataMode !== "XML" && this.receiveDataMode !== "JSON") {
            throw new Error("Propriété [receiveDataMode] invalide");
        }
        
    }, // Fin méthode checkParams
    
    
    
    /**
     * Reset de l'objet => déprécié
     */
    reset: function() {
        // Laissé pour compatibilité. Ne sert à rien.
    }, // Fin méthode reset
    
    
    
    /**
     * Déréférence l'objet xmlHttpRequest
     * Réinitialisation des valeurs par défaut (propriétés privées uniquement)
     * 
     * @access  private
     * @return  void
     */
    __destruct: function() {
        
        this.data = '';
        this.params = [];
        this.onInitFlag = false;
        
        if (typeof(this) == 'object') {
            this.xmlhttp.abort();
            this.xmlhttp = null;
        }
        
    } // Fin méthode __destruct
}