Comment modifier la réponse XMLHttpRequest reçue par une autre fonction?

j'essaie de modifier la réponse reçue par une fonction que je ne peux pas modifier. Cette fonction crée une XMLHttpRequest à laquelle je peux m'attacher, mais j'ai été incapable de "envelopper" le responsabiletext d'une manière qui me permet de modifier le contenu avant que la fonction originale ne le reçoive.

Voici la fonction originale complète:

function Mj(a, b, c, d, e) {
    function k() {
        4 == (m && 'readyState' in m ? m.readyState : 0) && b && ff(b) (m)
    }
    var m = new XMLHttpRequest;
    'onloadend' in m ? m.addEventListener('loadend', k, !1)  : m.onreadystatechange = k;
    c = ('GET').toUpperCase();
    d = d || '';
    m.open(c, a, !0);
    m.send(d);
    return m
}
function ff(a) {
    return a && window ? function () {
        try {
            return a.apply(this, arguments)
        } catch(b) {
            throw jf(b),
                b;
        }
    } : a
}

j'ai aussi essayé de manipuler la fonction reiceiving k (); dans une tentative d'atteindre mon but, mais puisque cela ne dépend pas d'aucune donnée passant à la fonction (par exemple k(A. responsabiletext);) I had no success.

est-ce que je peux y arriver? Je ne souhaite pas utiliser les librairies js (comme jQuery);


modifier : je comprends que je ne peux pas changer .répondre directement puisqu'il est en lecture seule, mais j'essaie de trouver un moyen de changer le contenu entre la réponse et la fonction de réception.


EDIT2 : ajouté ci-dessous une des méthodes que j'ai essayé d'intercepter et de changer .réponse qui a été ajoutée d'ici: patch Monkey XMLHttpRequest.onreadystatechange

(function (open) {
XMLHttpRequest.prototype.open = function (method, url, async, user, pass) {
    if(/results/.test(url)) {
      console.log(this.onreadystatechange);
        this.addEventListener("readystatechange", function () {
            console.log('readystate: ' + this.readyState);
            if(this.responseText !== '') {
                this.responseText = this.responseText.split('&')[0];
            }
        }, false);
    }
    open.call(this, method, url, async, user, pass);
};
})(XMLHttpRequest.prototype.open);

EDIT3 : j'ai oublié d'inclure que les fonctions MJ et ff ne sont pas globalement disponibles, elles sont toutes les deux contenues dans un fonction anonyme (function(){fonctions sont ici})();


edit 4 : j'ai changé la réponse acceptée parce que AmmarCSE n'a aucun des problèmes et de la complexité liés à la réponse de jfriend00.

la meilleure réponse expliquée en résumé est la suivante:

écouter la demande que vous souhaitez modifier (assurez-vous que votre auditeur l'interceptera avant l'original fonction de la destination, sinon il est inutile de le modifier après la réponse a déjà été utilisé).

Enregistrer la réponse originale (si vous voulez la modifier) dans une variable temporaire

changez la propriété que vous voulez modifier en "writable: true", elle effacera la valeur qu'elle avait. Dans mon cas, j'utilise

Object.defineProperty(event, 'responseText', {
    writable: true
});

event est l'objet retourné en écoutant le load ou readystatechange événement de la demande de xhr

Maintenant vous pouvez définir tout ce que vous voulez pour votre réponse, si tout ce que vous vouliez était de modifier la réponse originale, alors vous pouvez utiliser ces données à partir de votre variable temporaire et ensuite enregistrer les modifications dans la réponse.

14
demandé sur Community 2014-10-19 08:34:09

7 réponses

une solution très simple est de changer le descripteur de propriété pour responseText lui-même

Object.defineProperty(wrapped, 'responseText', {
     writable: true
});

donc, vous pouvez étendre XMLHttpRequest comme

(function(proxied) {
    XMLHttpRequest = function() {
        //cannot use apply directly since we want a 'new' version
        var wrapped = new(Function.prototype.bind.apply(proxied, arguments));

        Object.defineProperty(wrapped, 'responseText', {
            writable: true
        });

        return wrapped;
    };
})(XMLHttpRequest);

Démo

8
répondu AmmarCSE 2016-05-30 19:31:36

Edit: Voir la deuxième option code ci-dessous (il est testé et fonctionne). La première a quelques limites.


puisque vous ne pouvez modifier aucune de ces fonctions, il semble que vous devez aller après le prototype XMLHttpRequest. Voici une idée (non testée, mais vous pouvez voir la direction):

(function() {
    var open = XMLHttpRequest.prototype.open;

    XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
        var oldReady;
        if (async) {   
            oldReady = this.onreadystatechange;
            // override onReadyStateChange
            this.onreadystatechange = function() {
                if (this.readyState == 4) {
                    // this.responseText is the ajax result
                    // create a dummay ajax object so we can modify responseText
                    var self = this;
                    var dummy = {};
                    ["statusText", "status", "readyState", "responseType"].forEach(function(item) {
                        dummy[item] = self[item];
                    });
                    dummy.responseText = '{"msg": "Hello"}';
                    return oldReady.call(dummy);
                } else {
                    // call original onreadystatechange handler
                    return oldReady.apply(this, arguments);
                }
            }
        } 
        // call original open method
        return open.apply(this, arguments);
    }

})();

il s'agit d'un patch de singe pour la méthode XMLHttpRequest open() et ensuite quand cela est appelé pour un async demande de, il fait un singe patch pour le onReadyStateChange gestionnaire depuis que devrait déjà être réglé. Cette fonction patchée voit alors le responsabiletext avant que le handler original onReadyStateChange ne soit appelé afin qu'il puisse lui attribuer une valeur différente.

et, finalement parce que .responseText est prêt seulement, cela remplace un objet xmlhttpresponse factice avant d'appeler le handler onreadystatechange . Cela ne fonctionnerait pas dans tous les cas, mais fonctionnera si le onreadystatechange handler utilise this.responseText pour obtenir la réponse.


et, voici une tentative qui redéfinit L'objet XMLHttpRequest pour qu'il soit notre propre objet proxy. Parce que c'est notre propre objet proxy, nous pouvons définir la propriété responseText à ce que nous voulons. Pour toutes les autres propriétés sauf onreadystatechange , cet objet envoie simplement l'appel get, set ou function à L'objet real XMLHttpRequest.

(function() {
    // create XMLHttpRequest proxy object
    var oldXMLHttpRequest = XMLHttpRequest;

    // define constructor for my proxy object
    XMLHttpRequest = function() {
        var actual = new oldXMLHttpRequest();
        var self = this;

        this.onreadystatechange = null;

        // this is the actual handler on the real XMLHttpRequest object
        actual.onreadystatechange = function() {
            if (this.readyState == 4) {
                // actual.responseText is the ajax result

                // add your own code here to read the real ajax result
                // from actual.responseText and then put whatever result you want
                // the caller to see in self.responseText
                // this next line of code is a dummy line to be replaced
                self.responseText = '{"msg": "Hello"}';
            }
            if (self.onreadystatechange) {
                return self.onreadystatechange();
            }
        };

        // add all proxy getters
        ["status", "statusText", "responseType", "response",
         "readyState", "responseXML", "upload"].forEach(function(item) {
            Object.defineProperty(self, item, {
                get: function() {return actual[item];}
            });
        });

        // add all proxy getters/setters
        ["ontimeout, timeout", "withCredentials", "onload", "onerror", "onprogress"].forEach(function(item) {
            Object.defineProperty(self, item, {
                get: function() {return actual[item];},
                set: function(val) {actual[item] = val;}
            });
        });

        // add all pure proxy pass-through methods
        ["addEventListener", "send", "open", "abort", "getAllResponseHeaders",
         "getResponseHeader", "overrideMimeType", "setRequestHeader"].forEach(function(item) {
            Object.defineProperty(self, item, {
                value: function() {return actual[item].apply(actual, arguments);}
            });
        });
    }
})();

Démo de travail: http://jsfiddle.net/jfriend00/jws6g691 /

Je l'ai essayé dans les dernières versions D'IE, Firefox et Chrome et il a fonctionné avec une simple demande ajax.

Note: je n'ai pas regardé dans tous les moyens avancés que Ajax (comme les données binaires, uploads, etc...) peut être utilisé pour voir que ce proxy est suffisamment détaillée pour faire tous ces travaux (je suppose qu'il pourrait ne pas être encore sans plus de travail, mais il est de travail pour les requêtes de base de sorte qu'il ressemble le concept en est capable).


autres tentatives qui ont échoué:

  1. a essayé de dériver de L'objet XMLHttpRequest et puis de remplacer le constructeur par le mien, mais cela n'a pas fonctionné parce que la vraie fonction XMLHttpRequest ne vous laissera pas l'appeler comme une fonction pour initialiser mon objet dérivé.

  2. essayé juste remplacer le gestionnaire onreadystatechange et changer .responseText , mais ce champ est en lecture seule donc vous ne pouvez pas le changer.

  3. a essayé de créer un objet factice qui est envoyé comme l'objet this en appelant onreadystatechange , mais beaucoup de code ne fait pas référence à this , mais a plutôt l'objet réel enregistré dans une variable locale dans une fermeture - battant ainsi l'objet factice.

15
répondu jfriend00 2015-02-03 23:34:26

par requête j'inclus ci-dessous un exemple d'extrait montrant comment modifier la réponse d'une XMLHttpRequest avant que la fonction originale puisse la recevoir.

// In this example the sample response should be
// {"data_sample":"data has not been modified"}
// and we will change it into
// {"data_sample":"woops! All data has gone!"}

/*---BEGIN HACK---------------------------------------------------------------*/

// here we will modify the response
function modifyResponse(response) {

    var original_response, modified_response;

    if (this.readyState === 4) {

        // we need to store the original response before any modifications
        // because the next step will erase everything it had
        original_response = response.target.responseText;

        // here we "kill" the response property of this request
        // and we set it to writable
        Object.defineProperty(this, "responseText", {writable: true});

        // now we can make our modifications and save them in our new property
        modified_response = JSON.parse(original_response);
        modified_response.data_sample = "woops! All data has gone!";
        this.responseText = JSON.stringify(modified_response);

    }
}

// here we listen to all requests being opened
function openBypass(original_function) {

    return function(method, url, async) {

        // here we listen to the same request the "original" code made
        // before it can listen to it, this guarantees that
        // any response it receives will pass through our modifier
        // function before reaching the "original" code
        this.addEventListener("readystatechange", modifyResponse);

        // here we return everything original_function might
        // return so nothing breaks
        return original_function.apply(this, arguments);

    };

}

// here we override the default .open method so that
// we can listen and modify the request before the original function get its
XMLHttpRequest.prototype.open = openBypass(XMLHttpRequest.prototype.open);
// to see the original response just remove/comment the line above

/*---END HACK-----------------------------------------------------------------*/

// here we have the "original" code receiving the responses
// that we want to modify
function logResponse(response) {

    if (this.readyState === 4) {

        document.write(response.target.responseText);

    }

}

// here is a common request
var _request = new XMLHttpRequest();
_request.open("GET", "https://gist.githubusercontent.com/anonymous/c655b533b340791c5d49f67c373f53d2/raw/cb6159a19dca9b55a6c97d3a35a32979ee298085/data.json", true);
_request.addEventListener("readystatechange", logResponse);
_request.send();
3
répondu MT0 2017-01-10 10:01:20

vous pouvez envelopper le getter pour responseText dans le prototype avec une nouvelle fonction et faire les changements à la sortie là.

voici un exemple simple qui ajoute le commentaire html <!-- TEST --> au texte de réponse:

(function(http){
  var get = Object.getOwnPropertyDescriptor(
    http.prototype,
    'responseText'
  ).get;

  Object.defineProperty(
    http.prototype,
    "responseText",
    {
      get: function(){ return get.apply( this, arguments ) + "<!-- TEST -->"; }
    }
  );
})(self.XMLHttpRequest);

la fonction ci-dessus modifiera le texte de la réponse pour toutes les requêtes.

si vous voulez faire le changement à une seule requête alors n'utilisez pas la fonction ci-dessus, mais définissez simplement la getter sur la demande individuelle à la place:

var req = new XMLHttpRequest();
var get = Object.getOwnPropertyDescriptor(
  XMLHttpRequest.prototype,
  'responseText'
).get;
Object.defineProperty(
  req,
  "responseText", {
    get: function() {
      return get.apply(this, arguments) + "<!-- TEST -->";
    }
  }
);
var url = '/';
req.open('GET', url);
req.addEventListener(
  "load",
   function(){
     console.log(req.responseText);
   }
);
req.send();
3
répondu MT0 2017-01-12 09:37:19

j'ai eu besoin d'intercepter et de modifier une réponse de requête donc j'ai trouvé un peu de code. J'ai également constaté que certains sites Web aiment à utiliser la réponse ainsi que le responsabiletext qui est la raison pour laquelle mon code modifie les deux.

Le Code

var open_prototype = XMLHttpRequest.prototype.open,
intercept_response = function(urlpattern, callback) {
   XMLHttpRequest.prototype.open = function() {
      arguments['1'].match(urlpattern) && this.addEventListener('readystatechange', function(event) {
         if ( this.readyState === 4 ) {
            var response = callback(event.target.responseText);
            Object.defineProperty(this, 'response',     {writable: true});
            Object.defineProperty(this, 'responseText', {writable: true});
            this.response = this.responseText = response;
         }
      });
      return open_prototype.apply(this, arguments);
   };
};

le premier paramètre de la intercept_response fonction est une expression régulière pour correspondre à l'url de la requête et le deuxième paramètre est la fonction à utiliser sur la réponse à de le modifier.

Exemple D'Usage

intercept_response(/fruit\.json/i, function(response) {
   var new_response = response.replace('banana', 'apple');
   return new_response;
});
3
répondu Jake 2017-03-31 15:33:51

j'ai rencontré le même problème lorsque je faisais une extension Chrome pour permettre les appels D'API cross origin. Cela a fonctionné en Chrome. (Mise à jour: Il ne fonctionne pas dans la dernière version de Chrome).

delete _this.responseText;
_this.responseText = "Anything you want";

le snippet fonctionne à l'intérieur d'une XMLHttpRequest monkeypatched.prototype.Envoyer qui redirige les requêtes vers le script d'arrière-plan des extensions et remplacer toutes les propriétés sur response. Comme ceci:

// Delete removes the read only restriction
delete _this.response;
_this.response = event.data.response.xhr.response;
delete _this.responseText;
_this.responseText = event.data.response.xhr.responseText;
delete _this.status;
_this.status = event.data.response.xhr.status;
delete _this.statusText;
_this.statusText = event.data.response.xhr.statusText;
delete _this.readyState;
_this.readyState = event.data.response.xhr.readyState;

qui n'a pas fonctionné dans Firefox, mais j'ai trouvé une solution qui a fonctionné:

var test = new XMLHttpRequest();
Object.defineProperty(test, 'responseText', {
  configurable: true,
  writable: true,
});

test.responseText = "Hey";

qui ne fonctionne pas dans le Chrome, mais ce travail à la fois dans le Chrome et Firefox:

var test = new XMLHttpRequest();
var aValue;
Object.defineProperty(test, 'responseText', {
  get: function() { return aValue; },
  set: function(newValue) { aValue = newValue; },
  enumerable: true,
  configurable: true
});

test.responseText = "Hey";

Le dernier été de la copie passé de https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

aucune des solutions ne fonctionne en Safari. J'ai essayé de faire une nouvelle XMLHttpRequest avec des propriétés inscriptibles, mais ce n'était pas autorisé à appeler ouvert ou envoyer à partir de lui. J'ai aussi essayé cette solution: https://stackoverflow.com/a/28513219/3717718 . Malheureusement, il a produit la même erreur dans Safari:

TypeError: Attempting to configurable attribut of unconfigurable property.

2
répondu user3717718 2017-05-23 12:02:48

les variables de fonction de première classe sont des choses merveilleuses! function f() {a; b; c; } est exactement la même chose que var f = function () {a; b; c; } cela signifie que vous pouvez redéfinir les fonctions si nécessaire. Vous souhaitez intégrer la fonction Mj pour retourner un objet modifié? Pas de problème. Le fait que le champ responseText soit en lecture seule est une douleur, mais si c'est le seul champ dont vous avez besoin...

var Mj_backup = Mj; // Keep a copy of it, unless you want to re-implement it (which you could do)
Mj = function (a, b, c, d, e) { // To wrap the old Mj function, we need its args
    var retval = Mj_backup(a,b,c,d,e); // Call the original function, and store its ret value
    var retmod; // This is the value you'll actually return. Not a true XHR, just mimics one
    retmod.responseText = retval.responseText; // Repeat for any other required properties
    return retmod;
}

maintenant, quand votre code de page appelle Mj (), il va invoquer votre wrapper à la place (qui appellera encore l'original de Mj en interne, bien sûr).

1
répondu CBHacking 2014-10-19 05:29:38