Как я могу изменить XMLHttpRequest responseetext, полученный другой функцией?

Я пытаюсь изменить responseText, полученный функцией, которую я не могу изменить. Эта функция создает XMLHttpRequest, к которому я могу подключиться, но я не смог "обернуть" responseText таким образом, чтобы я мог изменять содержимое до того, как его получит оригинальная функция.

Здесь полная оригинальная функция:

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
}

Я также пытался манипулировать функцией reiceiving k(); в попытке достичь моей цели, но поскольку он не зависит от каких-либо данных, передаваемых функции (например, k (a.responseText);), я не имел успеха.

Есть ли способ, которым я могу это достичь? Я не хочу использовать js-библиотеки (например, jQuery);


EDIT. Я понимаю, что я не могу изменить .responseText напрямую, поскольку он доступен только для чтения, но я пытаюсь найти способ изменить контент между ответом и функцией приема.


EDIT2. Добавлен ниже один из методов, которые я пытался перехватить и изменить .responseText, который был добавлен здесь: Патч обезьяны 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: я забыл включить, что функции Mj и ff недоступны глобально, они оба содержатся внутри анонимной функции (function() {функции здесь})();


EDIT4. Я изменил принятый ответ, потому что у AmmarCSE нет никаких проблем и сложностей, связанных с ответом jfriend00.

Лучший ответ, объясненный кратко, выглядит следующим образом:

Прослушайте какой-либо запрос, который вы хотите изменить (убедитесь, что ваш прослушиватель перехватит его до того, как будет выполняться исходная функция, иначе нет смысла изменять его после того, как ответ уже был использован).

Сохранить исходный ответ (если вы хотите его изменить) во временной переменной

Измените свойство, которое вы хотите изменить, на "writeable: true", оно удалит любое значение, которое оно имело. В моем случае я использую

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

Где event - объект, возвращенный при прослушивании события load или readystatechange запроса xhr

Теперь вы можете установить все, что хотите для своего ответа, если все, что вам нужно, это изменить исходный ответ, тогда вы можете использовать эти данные из своей временной переменной и затем сохранить изменения в ответе.

Ответ 1

Один очень простой способ - изменить дескриптор свойства для самого responseText

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

Итак, вы можете расширить XMLHttpRequest как

(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);

Демо

Ответ 2

Изменить: см. второй вариант кода ниже (он протестирован и работает). Первый имеет некоторые ограничения.


Поскольку вы не можете изменить какую-либо из этих функций, вам кажется, что вам нужно идти после прототипа XMLHttpRequest. Вот одна идея (непроверенная, но вы можете видеть направление):

(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);
    }

})();

Это делает патч обезьяны для метода XMLHttpRequest open(), а затем, когда он вызывается для запроса async, он выполняет патч обезьяны для обработчика onReadyStateChange, поскольку он уже должен быть установлен. Эта исправленная функция затем увидит ответText перед тем, как вызывается обработчик onReadyStateChange, чтобы он мог присвоить ему другое значение.

И, наконец, поскольку .responseText готов только, это заменяет объект фиктивного XMLHttpResponse перед вызовом обработчика onreadystatechange. Это не будет работать во всех случаях, но будет работать, если обработчик onreadystatechange использует this.responseText для получения ответа.


И вот попытка, которая переопределяет объект XMLHttpRequest как наш собственный прокси-объект. Поскольку это наш собственный прокси-объект, мы можем установить свойство responseText на все, что захотим. Для всех других свойств, кроме onreadystatechange, этот объект просто перенаправляет вызов get, set или function в реальный объект 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);}
            });
        });
    }
})();

Рабочая демонстрация: http://jsfiddle.net/jfriend00/jws6g691/

Я попробовал его в последних версиях IE, Firefox и Chrome, и он работал с простым ajax-запросом.

Примечание. Я не рассматривал все расширенные способы, с помощью которых Ajax (например, двоичные данные, закачки и т.д.) можно использовать, чтобы увидеть, что этот прокси достаточно тщательный, чтобы заставить все это работать (я бы предположил, не будет еще без какой-либо дальнейшей работы, но он работает для базовых запросов, поэтому похоже, что концепция способна).


Другие неудачные попытки:

  • Пробовал вывести объект XMLHttpRequest, а затем заменить конструктор на свой собственный, но это не сработало, потому что реальная функция XMLHttpRequest не позволит вам называть его как функцию для инициализации моего производного объекта.

  • Пробовал просто переопределить обработчик onreadystatechange и изменить .responseText, но это поле доступно только для чтения, поэтому вы не можете его изменить.

  • Попробовал создать фиктивный объект, который отправляется как объект this при вызове onreadystatechange, но много кода не ссылается на this, а скорее имеет фактический объект, сохраненный в локальной переменной в закрытии - таким образом, побеждая фиктивный объект.

Ответ 3

По запросу я включаю ниже примерный фрагмент, показывающий, как изменить ответ XMLHttpRequest, прежде чем исходная функция сможет его получить.

// 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();

Ответ 4

Вы можете обернуть getter для responseText в прототипе с помощью новой функции и внести туда изменения.

Вот простой пример, который добавляет комментарий html <!-- TEST --> к тексту ответа:

(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);

Вышеуказанная функция изменит текст ответа для всех запросов.

Если вы хотите внести изменения только в один запрос, не используйте вышеприведенную функцию, а просто укажите получателя по отдельному запросу:

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();

Ответ 5

Мне нужно было перехватить и изменить ответ запроса, поэтому я придумал немного кода. Я также обнаружил, что некоторые сайты любят использовать ответ, а также responseText, поэтому мой код изменяет оба.

Код

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);
   };
};

первый параметр функции intercept_response является регулярным выражением, которое соответствует URL-адресу запроса, а второй параметр - это функция, которая будет использоваться для ответа, чтобы изменить его.

Пример использования

intercept_response(/fruit\.json/i, function(response) {
   var new_response = response.replace('banana', 'apple');
   return new_response;
});

Ответ 6

Я столкнулся с той же проблемой, когда я делал расширение Chrome, чтобы разрешать вызовы API перекрестного происхождения. Это работало в Chrome. (Обновление: оно не работает в новейшей версии Chrome).

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

Фрагмент запускается внутри файла XMLHttpRequest.prototype.send, который перенаправляет запросы на фона extensions script и заменяет все свойства на ответ. Вот так:

// 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;

Это не работает в Firefox, но я нашел решение, которое сработало:

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

test.responseText = "Hey";

Это не работает в Chrome, но эта работа работает как в Chrome, так и в 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";

Последняя была скопирована из https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

Ни одно из решений не работает в Safari. Я попытался создать новый XMLHttpRequest с возможностью записи, но ему не разрешили открывать или отправлять его. Я также пробовал это решение: fooobar.com/questions/94139/.... К сожалению, в Safari появилась такая же ошибка:

TypeError: попытка настраиваемого атрибута неконфигурируемого свойства.

Ответ 7

Первичные функциональные переменные - замечательные вещи! function f() {a; b; c; } - это точно то же самое, что и var f = function () {a; b; c; } Это означает, что вы можете переопределить функции по мере необходимости. Вы хотите обернуть функцию Mj, чтобы вернуть измененный объект? Нет проблем. Тот факт, что поле responseText является только для чтения, является болью, но если это единственное поле, которое вам нужно...

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;
}

Теперь, когда ваш код страницы вызывает Mj(), он будет вызывать вашу обертку вместо этого (что, конечно, по-прежнему вызовет исходный Mj).