How can I get the XMLHttpRequest back after the data loads? - javascript

I have a code in which the "onloadend" runs after the "get" task, and the code would work the right way if "get" ran first. So i need a way to switch them. I must use HttpClient and JavaScript, I cannot use jQuery or anything else.
Here is my full code in CodePen: https://codepen.io/vargaadam19/pen/BaabVbZ?editors=0010
Here is the onloadend:
export class HttpClient {
constructor(url) {
this.url = url;
this.xhr = new XMLHttpRequest();
this.xhr.onloadend = (event) => {
return this.xhr.result;
};
}
I set header here:
setHeader() {
this.xhr.setRequestHeader('Content-Type','application/json');
}
and here is the get function:
get(async, header) {
return new Promise((resolve, reject) => {
this.xhr.open('GET', this.url, async);
this.xhr.setRequestHeader(header.name, header.value);
this.xhr.send();
resolve(this.xhr.response);`enter code here`
});
}
And here is the function that's what I invite the get function
getAll() {
this.httpClient.get(true, this.header).then((result) => {
const data = !!result ? result : '[]';
const items = JSON.parse(data);
console.log('result:', result);
items.forEach(item => {
const todo = new Todo(item.id, item.name, item.status);
this.items.push(todo);
});
});
}
How can I solve this problem about the order of the tasks?

Related

Error loading(insert) images in CKeditor?

When I write my functions as below, you can see that there is a record as seen in the image. Where exactly is the problem?
Which is the function that records like in the image?
The image saves as I want, but it saves to the database as in the image.
I did according to the answer in How to use custom upload adapter on ASP.Net Core - CKEDITOR 5? but I can't solve the problem
!!How can I give this.url to src?
class MyUploadAdapter
{
constructor(loader) {
// The file loader instance to use during the upload.
this.loader = loader;
this.urls = '/tr/UnitType/DocUploadImage';
}
// Starts the upload process.
upload() {
return this.loader.file.then(file => new Promise((resolve, reject) => {
this._initRequest();
this._initListeners(resolve, reject, file);
this._sendRequest(file);
}));
}
// Aborts the upload process.
abort() {
if (this.xhr) {
this.xhr.abort();
}
}
_initRequest() {
const xhr = this.xhr = new XMLHttpRequest();
xhr.open('POST', this.urls, true);
xhr.responseType = 'json';
}
// Initializes XMLHttpRequest listeners.
_initListeners(resolve, reject, file) {
const xhr = this.xhr;
const loader = this.loader;
const genericErrorText = `Couldn't upload file: ${file.name}.`;
xhr.addEventListener('error', () => reject(genericErrorText));
xhr.addEventListener('abort', () => reject());
xhr.addEventListener('load', () => {
const response = xhr.response;
if (!response || response.error) {
return reject(response && response.error ? response.error.message : genericErrorText);
}
resolve({
default: response.urls
});
});
if (xhr.upload) {
xhr.upload.addEventListener('progress', evt => {
if (evt.lengthComputable) {
loader.uploadTotal = evt.total;
loader.uploaded = evt.loaded;
}
});
}
}
// Prepares the data and sends the request.
_sendRequest(file) {
// Prepare the form data.
const data = new FormData();
data.append('upload', file);
this.xhr.send(data);
}
}
function MyCustomUploadAdapterPlugin(editor) {
editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
return new MyUploadAdapter(loader);
};
}
public async Task<JsonResult> DocUploadImage()
{
try
{
var config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
var webRootPath = config["AppSettings:urunResimPath"].ToString();
var uploads = Path.Combine(webRootPath, "assets");
var filePath = Path.Combine(uploads, "rich-text");
var urls = new List<string>();
//If folder of new key is not exist, create the folder.
if (!Directory.Exists(filePath)) Directory.CreateDirectory(filePath);
foreach (var contentFile in Request.Form.Files)
{
if (contentFile != null && contentFile.Length > 0)
{
await contentFile.CopyToAsync(new FileStream($"{filePath}\\{contentFile.FileName}", FileMode.Create));
urls.Add($"{HttpContext.Request.Host}/rich-text/{contentFile.FileName}");
}
}
return Json(urls);
}
catch (Exception e)
{
return Json(new { error = new { message = e.Message } });
}
}

How to track upload progress to S3 using aws-sdk V3 for browser (javascript)

I can find a lot of resources online on how to track upload progress to S3 using aws-sdk V2, listening to the event like:
.on('httpUploadProgress', event => {}
But since I updated the aws-sdk to V3, there are no listeners anymore. I believe I have to use the middleware function now, but I've tried a few things and it didn't work. I've also went deep into the API reference docs and the github repository without success.
My current code is like this:
import { S3Client, PutObjectCommand } from '#aws-sdk/client-s3';
export const UploadToS3 = (credentials, fileData) => {
const s3 = new S3Client({
region: credentials.region,
credentials: {
accessKeyId: credentials.access_key,
secretAccessKey: credentials.secret_key,
sessionToken: credentials.session_token,
}
});
return new Promise((resolve) => {
s3.send(new PutObjectCommand({
Bucket: credentials.bucket,
Key: credentials.file,
Body: fileData,
}));
});
};
Any help would be appreciated
I had exactly the same problem (switched from aws-sdk v2 to v3) and found out that it is because the library uses the Fetch API for all HTTP Requests and Fetch does not (yet) support tracking upload progress
To solve that problem I exchanged Fetch by good old XMLHttpRequest at least for PUT requests, which you can accomplish by providing a custom requestHandler when initializing the S3Client.
import { S3Client } from '#aws-sdk/client-s3';
const myHttpHandler = new MyHttpHandler();
myHttpHandler.onProgress$.subscribe(progress => {
const percentComplete = progress.progressEvent.loaded / progress.progressEvent.total * 100;
console.log('upload progress', percentComplete);
});
const myClient = new S3Client({
endpoint: this.configService.s3Api,
region: 'eu',
credentials: { ... },
requestHandler: myHttpHandler
});
The custom request handler simply extends the FetchHttpHandler from #aws-sdk/fetch-http-handler. If the method is PUT and there is a body (so we want to upload something), it uses a custom XHR handler - otherwise it just uses the Fetch handler from it's super class.
And in the XHR handler you can bind something to the progress event of the XHR handler - in my case I emit a rxjs Subject which I can consume outside the custom handler.
import { FetchHttpHandler, FetchHttpHandlerOptions } from '#aws-sdk/fetch-http-handler';
import { HeaderBag, HttpHandlerOptions } from '#aws-sdk/types';
import { buildQueryString } from '#aws-sdk/querystring-builder';
import { HttpResponse, HttpRequest } from '#aws-sdk/protocol-http';
import { Subject } from 'rxjs';
class MyHttpHandler extends FetchHttpHandler {
private myRequestTimeout;
onProgress$: Subject<{ path: string, progressEvent: ProgressEvent }> = new Subject();
constructor({ requestTimeout }: FetchHttpHandlerOptions = {}) {
super({ requestTimeout });
this.myRequestTimeout = requestTimeout;
}
handle(request: HttpRequest, { abortSignal }: HttpHandlerOptions = {}): Promise<{ response: HttpResponse }> {
// we let XHR only handle PUT requests with body (as we want to have progress events here), the rest by fetch
if (request.method === 'PUT' && request.body) {
return this.handleByXhr(request, { abortSignal });
}
return super.handle(request, { abortSignal });
}
/**
* handles a request by XHR instead of fetch
* this is a copy the `handle` method of the `FetchHttpHandler` class of #aws-sdk/fetch-http-handler
* replacing the `Fetch`part with XHR
*/
private handleByXhr(request: HttpRequest, { abortSignal }: HttpHandlerOptions = {}): Promise<{ response: HttpResponse}> {
const requestTimeoutInMs = this.myRequestTimeout;
// if the request was already aborted, prevent doing extra work
if (abortSignal?.aborted) {
const abortError = new Error('Request aborted');
abortError.name = 'AbortError';
return Promise.reject(abortError);
}
let path = request.path;
if (request.query) {
const queryString = buildQueryString(request.query);
if (queryString) {
path += `?${queryString}`;
}
}
const { port, method } = request;
const url = `${request.protocol}//${request.hostname}${port ? `:${port}` : ''}${path}`;
// Request constructor doesn't allow GET/HEAD request with body
// ref: https://github.com/whatwg/fetch/issues/551
const body = method === 'GET' || method === 'HEAD' ? undefined : request.body;
const requestOptions: RequestInit = {
body,
headers: new Headers(request.headers),
method,
};
const myXHR = new XMLHttpRequest();
const xhrPromise = new Promise<{headers: string[], body: Blob, status: number}>((resolve, reject) => {
try {
myXHR.responseType = 'blob';
// bind the events
myXHR.onload = progressEvent => {
resolve({
body: myXHR.response,
headers: myXHR.getAllResponseHeaders().split('\n'),
status: myXHR.status
});
};
myXHR.onerror = progressEvent => reject(new Error(myXHR.responseText));
myXHR.onabort = progressEvent => {
const abortError = new Error('Request aborted');
abortError.name = 'AbortError';
reject(abortError);
};
// progress event musst be bound to the `upload` property
if (myXHR.upload) {
myXHR.upload.onprogress = progressEvent => this.onProgress$.next({ path, progressEvent });
}
myXHR.open(requestOptions.method, url);
// append headers
if (requestOptions.headers) {
(requestOptions.headers as Headers).forEach((headerVal, headerKey, headers) => {
if (['host', 'content-length'].indexOf(headerKey.toLowerCase()) >= 0) {
// avoid "refused to set unsafe header" error message
return;
}
myXHR.setRequestHeader(headerKey, headerVal);
});
}
myXHR.send(requestOptions.body);
} catch (e) {
console.error('S3 XHRHandler error', e);
reject(e);
}
});
const raceOfPromises = [
xhrPromise.then((response) => {
const fetchHeaders = response.headers;
const transformedHeaders: HeaderBag = {};
fetchHeaders.forEach(header => {
const name = header.substr(0, header.indexOf(':') + 1);
const val = header.substr(header.indexOf(':') + 1);
if (name && val) {
transformedHeaders[name] = val;
}
});
const hasReadableStream = response.body !== undefined;
// Return the response with buffered body
if (!hasReadableStream) {
return response.body.text().then(body => ({
response: new HttpResponse({
headers: transformedHeaders,
statusCode: response.status,
body,
}),
}));
}
// Return the response with streaming body
return {
response: new HttpResponse({
headers: transformedHeaders,
statusCode: response.status,
body: response.body,
}),
};
}),
this.requestTimeoutFn(requestTimeoutInMs),
];
if (abortSignal) {
raceOfPromises.push(
new Promise<never>((resolve, reject) => {
abortSignal.onabort = () => {
myXHR.abort();
};
})
);
}
return Promise.race(raceOfPromises);
}
private requestTimeoutFn(timeoutInMs = 0): Promise<never> {
return new Promise((resolve, reject) => {
if (timeoutInMs) {
setTimeout(() => {
const timeoutError = new Error(`Request did not complete within ${timeoutInMs} ms`);
timeoutError.name = 'TimeoutError';
reject(timeoutError);
}, timeoutInMs);
}
});
}
}
Looking through the github issues I've just found that #aws-sdk/client-s3 don't support upload progress tracking, since it uses fetchHttpHandler under the covers. The recommended way is to use #aws-sdk/lib-storage which I have not tried yet, but looks promising!
I have also faced the same issue that is i have to upgrade aws-sdk from v2 to v3 but file upload progress feature is missing in v3.The reason for that is in JS SDK v2 for S3 file upload they uses XHR for browser network requests, which has a good interface for progress tracking.
Whereas JS SDK v3 uses fetch for the same, which offers some other advantages but one downside is that it does not seem to support upload progress yet, and progress on that feature is extremely slow.
But now JS SDK v3 has a new package called #aws-sdk/xhr-http-handler which can be used in place of #aws-sdk/fetch-http-handler in order to get the fine grained file upload progress as we are getting in v2.
You can find out the code for that on the link https://github.com/aws/aws-sdk-js-v3/tree/main/packages/xhr-http-handler
import { S3Client } from '#aws-sdk/client-s3';
import { XhrHttpHandler } from '#aws-sdk/xhr-http-handler';
import { Upload } from '#aws-sdk/lib-storage';
const s3Client = new S3Client({
requestHandler: new XhrHttpHandler({}),
});
const upload = new Upload({
client:s3Client,
params: {
bucket,
key,
},
});
upload.on("httpUploadProgress", (progress) => {
console.log(
progress.loaded, // Bytes uploaded so far.
progress.total // Total bytes. Divide these two for progress percentage.
);
});
await upload.done();

Access IndexDB when Promise gets query error

I am doing a Progressive Web App and I want to access the data I have stored on IndexedDB when the ajax reponse fails. The data gets filled when the query is successful. Below is one of the attempts. In comments is the original format before I inserted the offline part (I already had base code).
getAudits: (startOfMonth, endOfMonth) => {
return new Promise((resolve, reject) => {
request
.get(url + "api/?method=Audit.data")
.query({
pageLength: "all",
format: "json",
where: startOfMonth
? Utils.hashQuery(
"a.date",
"between",
[startOfMonth, endOfMonth].toString()
)
: "",
ts: new Date().getTime()
})
.end((error, response) => {
var res;
if (error) {
res = getCacheAudits(startOfMonth, endOfMonth);
//reject(error);
} else {
let _audits = [];
const audits = Utils.toArray(JSON.parse(response.text).rows);
audits.forEach(audit => {
_audits.push(audit);
//console.log(audit);
});
res = _audits;
getServerAudits(_audits);
//resolve(res);
}
resolve(res);
});
});
}
Does anyone have any idea about how to handle this correctly? Thanks in advance.
Not sure why you copy Utils.toArray(JSON.parse(response.text).rows to audits, _audits and res, they are all the same.
Also not sure what getServerAudits is supposed to do because it doesn't return anything and you're not doing anything with it's return value (usually a bad design sign to call functions and ignore their return)
You could probably do something like this:
getAudits: (startOfMonth, endOfMonth) => {
return new Promise((resolve, reject) => {
request
.get(url + "api/?method=Audit.data")
.query({
pageLength: "all",
format: "json",
where: startOfMonth
? Utils.hashQuery(
"a.date",
"between",
[startOfMonth, endOfMonth].toString()
)
: "",
ts: new Date().getTime()
})
.end((error, response) =>
(error)
? reject([error,startOfMonth, endOfMonth])
: resolve(Utils.toArray(JSON.parse(response.text).rows))
)
})
.catch(
([err,startOfMonth, endOfMonth]) =>
getCacheAudits(startOfMonth, endOfMonth)
)
.then(
audits => {
//not sure what this does, you don't seem to care about it's
// result because your code doesn't do anything with it
getServerAudits(audits);
//probably want to save audits to local storage here
return Promise.all([
audits,
saveCacheAudits(startOfMonth,endOfMonth,audits)
]);
}
)
.then(
//resolve audits
([audits])=>audits
)
}
Lets make this easy with indexedb library jsstore -
var con= new JsStore.Instance(<db_name>);
getCacheAudits: (startOfMonth, endOfMonth)=>{
return con.select({
From: "Table_Name",
Where:
{
startOfMonth:startOfMonth,
endOfMonth: endOfMonth
}
})
}
getAudits: (startOfMonth, endOfMonth) => {
return new Promise((resolve, reject) => {
request
.get(url + "api/?method=Audit.data")
.query({
pageLength: "all",
format: "json",
where: startOfMonth
? Utils.hashQuery(
"a.date",
"between",
[startOfMonth, endOfMonth].toString()
)
: "",
ts: new Date().getTime()
})
.end((error, response) => {
var res;
if (error) {
//get the data from indexeddb
getCacheAudits.
then(function (results){
resolve(results);
})
.catch(function (error) {
reject(error._message);
});
} else {
let _audits = [];
const audits = Utils.toArray(JSON.parse(response.text).rows);
audits.forEach(audit => {
_audits.push(audit);
//console.log(audit);
});
res = _audits;
getServerAudits(_audits);
resolve(res);
}
//resolve(res);
});
});
}
You can also use where (http://jsstore.net/tutorial/where) and all others options to query the data.
If you are using angular to create the app, check out this article - http://ujjwalguptaofficial.blogspot.in/2017/10/angular4-crud-operation-in-indexeddb.html
Hope this will help you.
Just solved the problem. It wasn't actually just in this part, but in the getCacheAudits function too.
The getCacheAudits call became:
getCacheAudits(startOfMonth, endOfMonth, function (res) {
resolve(res);
})
And in the getCacheAudits function was added a callback
function getCacheAudits(startOfMonth, endOfMonth, callback){
let initOpenReq = indexedDB.open("AuditsDB");
let agregate = [];
initOpenReq.onsuccess = function() {
var db = initOpenReq.result;
var transaction = db.transaction("audits","readonly");
var obj_store = transaction.objectStore("audits");
obj_store.openCursor().onsuccess = function (event){
let cursor = event.target.result;
if(cursor){
agregate.push(cursor.value);
cursor.continue();
}else{
callback(agregate);
}
};
}
There are a few more bells a whistles in the function, but this made it work properly.
Sorry for not including the function right from the beginning.

Angular 2 Synchronous File Upload

I am trying to upload a file to web api which takes the file as byte array using angular 2 application.
I am not able to pass the byte array from angular 2 page to web api. It looks like the File Reader read method is asynchronous. How do I make this as synchronous call or wait for the file content to be loaded before executing the next line of code?
Below is my code
//attachment on browse - when the browse button is clicked
//It only assign the file to a local variable (attachment)
fileChange = (event) => {
var files = event.target.files;
if (files.length > 0) {
this.attachment = files[0];
}
}
//when the submit button is clicked
onSubmit = () => {
//Read the content of the file and store it in local variable (fileData)
let fr = new FileReader();
let data = new Blob([this.attachment]);
fr.readAsArrayBuffer(data);
fr.onloadend = () => {
this.fileData = fr.result; //Note : This always "undefined"
};
//build the attachment object which will be sent to Web API
let attachment: Attachment = {
AttachmentId: '0',
FileName: this.form.controls["attachmentName"].value,
FileData: this.fileData
}
//build the purchase order object
let order: UpdatePurchaseOrder = {
SendEmail: true,
PurchaseOrderNumber: this.form.controls["purchaseOrderNumber"].value,
Attachment: attachment
}
//call the web api and pass the purchaseorder object
this.updatePoService
.updatePurchaseOrder(this.form.controls["purchaseOrderRequestId"].value, order)
.subscribe(data => {
if (data) {
this.saveSuccess = true;
}
else {
this.saveSuccess = false;
}
},
error => this.errors = error,
() => this.res = 'Completed'
);
}
Any hint would be useful.
regards,
-Alan-
You cannot make this async call synchronous. But you can take advantage of the observables to wait for the files to be read:
//when the submit button is clicked
onSubmit = () => {
let file = Observable.create((observer) => {
let fr = new FileReader();
let data = new Blob([this.attachment]);
fr.readAsArrayBuffer(data);
fr.onloadend = () => {
observer.next(fr.result);
observer.complete()
};
fr.onerror = (err) => {
observer.error(err)
}
fr.onabort = () => {
observer.error("aborted")
}
});
file.map((fileData) => {
//build the attachment object which will be sent to Web API
let attachment: Attachment = {
AttachmentId: '0',
FileName: this.form.controls["attachmentName"].value,
FileData: fileData
}
//build the purchase order object
let order: UpdatePurchaseOrder = {
SendEmail: true,
PurchaseOrderNumber: this.form.controls["purchaseOrderNumber"].value,
Attachment: attachment
}
return order;
})
.switchMap(order => this.updatePoService.updatePurchaseOrder(this.form.controls["purchaseOrderRequestId"].value, order))
.subscribe(data => {
if (data) {
this.saveSuccess = true;
} else {
this.saveSuccess = false;
}
},
error => this.errors = error,
() => this.res = 'Completed'
);
}
I arrived here looking for a solution for a similar issue. I'm performing requests to an endpoint which can response a binary blob if anything goes well or a JSON file in event of error.
this.httpClient.post(urlService, bodyRequest,
{responseType: 'blob', headers: headers})
.pipe(map((response: Response) => response),
catchError((err: Error | HttpErrorResponse) => {
if (err instanceof HttpErrorResponse) {
// here, err.error is a BLOB containing a JSON String with the error message
} else {
return throwError(ErrorDataService.overLoadError(err, message));
}
}));
As FileReaderSync apparently doesn't work in Angular6 I took n00dl3's solution (above) to throw the error after parsing the Blob content:
return this.httpClient.post(urlService, bodyRequest,
{responseType: 'blob', headers: headers})
.pipe(map((response: Response) => response),
catchError((err: Error | HttpErrorResponse) => {
const message = `In TtsService.getTts(${locale},${outputFormat}). ${err.message}`;
if (err instanceof HttpErrorResponse) {
const $errBlobReader: Observable<HttpErrorResponse> = Observable.create((observer) => {
const fr = new FileReader();
const errorBlob = err.error;
fr.readAsText(errorBlob, 'utf8');
fr.onloadend = () => {
const errMsg = JSON.parse(fr.result).message;
const msg = `In TtsService.getTts(${locale},${outputFormat}). ${errMsg}`;
observer.error(ErrorDataService.overLoadError(err, msg));
};
fr.onerror = (blobReadError) => {
observer.error(blobReadError);
};
fr.onabort = () => {
observer.error('aborted');
};
});
return $errBlobReader;
} else {
return throwError(ErrorDataService.overLoadError(err, message));
}
}));
Thanks! You really saved my day!

How can I intercept XMLHttpRequests from a Greasemonkey script?

I would like to capture the contents of AJAX requests using Greasemonkey.
Does anybody know how to do this?
The accepted answer is almost correct, but it could use a slight improvement:
(function(open) {
XMLHttpRequest.prototype.open = function() {
this.addEventListener("readystatechange", function() {
console.log(this.readyState);
}, false);
open.apply(this, arguments);
};
})(XMLHttpRequest.prototype.open);
Prefer using apply + arguments over call because then you don't have to explicitly know all the arguments being given to open which could change!
How about modifying the XMLHttpRequest.prototype.open or send methods with replacements which set up their own callbacks and call the original methods? The callback can do its thing and then call the callback the original code specified.
In other words:
XMLHttpRequest.prototype.realOpen = XMLHttpRequest.prototype.open;
var myOpen = function(method, url, async, user, password) {
//do whatever mucking around you want here, e.g.
//changing the onload callback to your own version
//call original
this.realOpen (method, url, async, user, password);
}
//ensure all XMLHttpRequests use our custom open method
XMLHttpRequest.prototype.open = myOpen ;
Tested in Chrome 55 and Firefox 50.1.0
In my case I wanted to modify the responseText, which in Firefox was a read-only property, so I had to wrap the whole XMLHttpRequest object. I haven't implemented the whole API (particular the responseType), but it was good enough to use for all of the libraries I have.
Usage:
XHRProxy.addInterceptor(function(method, url, responseText, status) {
if (url.endsWith('.html') || url.endsWith('.htm')) {
return "<!-- HTML! -->" + responseText;
}
});
Code:
(function(window) {
var OriginalXHR = XMLHttpRequest;
var XHRProxy = function() {
this.xhr = new OriginalXHR();
function delegate(prop) {
Object.defineProperty(this, prop, {
get: function() {
return this.xhr[prop];
},
set: function(value) {
this.xhr.timeout = value;
}
});
}
delegate.call(this, 'timeout');
delegate.call(this, 'responseType');
delegate.call(this, 'withCredentials');
delegate.call(this, 'onerror');
delegate.call(this, 'onabort');
delegate.call(this, 'onloadstart');
delegate.call(this, 'onloadend');
delegate.call(this, 'onprogress');
};
XHRProxy.prototype.open = function(method, url, async, username, password) {
var ctx = this;
function applyInterceptors(src) {
ctx.responseText = ctx.xhr.responseText;
for (var i=0; i < XHRProxy.interceptors.length; i++) {
var applied = XHRProxy.interceptors[i](method, url, ctx.responseText, ctx.xhr.status);
if (applied !== undefined) {
ctx.responseText = applied;
}
}
}
function setProps() {
ctx.readyState = ctx.xhr.readyState;
ctx.responseText = ctx.xhr.responseText;
ctx.responseURL = ctx.xhr.responseURL;
ctx.responseXML = ctx.xhr.responseXML;
ctx.status = ctx.xhr.status;
ctx.statusText = ctx.xhr.statusText;
}
this.xhr.open(method, url, async, username, password);
this.xhr.onload = function(evt) {
if (ctx.onload) {
setProps();
if (ctx.xhr.readyState === 4) {
applyInterceptors();
}
return ctx.onload(evt);
}
};
this.xhr.onreadystatechange = function (evt) {
if (ctx.onreadystatechange) {
setProps();
if (ctx.xhr.readyState === 4) {
applyInterceptors();
}
return ctx.onreadystatechange(evt);
}
};
};
XHRProxy.prototype.addEventListener = function(event, fn) {
return this.xhr.addEventListener(event, fn);
};
XHRProxy.prototype.send = function(data) {
return this.xhr.send(data);
};
XHRProxy.prototype.abort = function() {
return this.xhr.abort();
};
XHRProxy.prototype.getAllResponseHeaders = function() {
return this.xhr.getAllResponseHeaders();
};
XHRProxy.prototype.getResponseHeader = function(header) {
return this.xhr.getResponseHeader(header);
};
XHRProxy.prototype.setRequestHeader = function(header, value) {
return this.xhr.setRequestHeader(header, value);
};
XHRProxy.prototype.overrideMimeType = function(mimetype) {
return this.xhr.overrideMimeType(mimetype);
};
XHRProxy.interceptors = [];
XHRProxy.addInterceptor = function(fn) {
this.interceptors.push(fn);
};
window.XMLHttpRequest = XHRProxy;
})(window);
You can replace the unsafeWindow.XMLHttpRequest object in the document with a wrapper. A little code (not tested):
var oldFunction = unsafeWindow.XMLHttpRequest;
unsafeWindow.XMLHttpRequest = function() {
alert("Hijacked! XHR was constructed.");
var xhr = oldFunction();
return {
open: function(method, url, async, user, password) {
alert("Hijacked! xhr.open().");
return xhr.open(method, url, async, user, password);
}
// TODO: include other xhr methods and properties
};
};
But this has one little problem: Greasemonkey scripts execute after a page loads, so the page can use or store the original XMLHttpRequest object during it's load sequence, so requests made before your script executes, or with the real XMLHttpRequest object wouldn't be tracked by your script. No way that I can see to work around this limitation.
I spent quite some time figuring out how to do this.
At first I was just overriding window.fetch but that stopped working for some reason - I believe it has to do with Tampermonkey trying to sandbox window (??) and I also tried unsafeWindow with the same results.
So. I started looking into overriding the requests at a lower level. The XMLHttpRequest (also that class name upper case lower case ew...)
Sean's answer was helpful to get started but didn't show how to override the responses after interception. The below does that:
let interceptors = [];
/*
* Add a interceptor.
*/
export const addInterceptor = (interceptor) => {
interceptors.push(interceptor);
};
/*
* Clear interceptors
*/
export const clearInterceptors = () => {
interceptors = [];
};
/*
* XML HTPP requests can be intercepted with interceptors.
* Takes a regex to match against requests made and a callback to process the response.
*/
const createXmlHttpOverride = (
open
) => {
return function (
method: string,
url,
async,
username,
password
) {
this.addEventListener(
"readystatechange",
function () {
if (this.readyState === 4) {
// Override `onreadystatechange` handler, there's no where else this can go.
// Basically replace the client's with our override for interception.
this.onreadystatechange = (function (
originalOnreadystatechange
) {
return function (ev) {
// Only intercept JSON requests.
const contentType = this.getResponseHeader("content-type");
if (!contentType || !contentType.includes("application/json")) {
return (
originalOnreadystatechange &&
originalOnreadystatechange.call(this, ev)
);
}
// Read data from response.
(async function () {
let success = false;
let data;
try {
data =
this.responseType === "blob"
? JSON.parse(await this.response.text())
: JSON.parse(this.responseText);
success = true;
} catch (e) {
console.error("Unable to parse response.");
}
if (!success) {
return (
originalOnreadystatechange &&
originalOnreadystatechange.call(this, ev)
);
}
for (const i in interceptors) {
const { regex, override, callback } = interceptors[i];
// Override.
const match = regex.exec(url);
if (match) {
if (override) {
try {
data = await callback(data);
} catch (e) {
logger.error(`Interceptor '${regex}' failed. ${e}`);
}
}
}
}
// Override the response text.
Object.defineProperty(this, "responseText", {
get() {
return JSON.stringify(data);
},
});
// Tell the client callback that we're done.
return (
originalOnreadystatechange &&
originalOnreadystatechange.call(this, ev)
);
}.call(this));
};
})(this.onreadystatechange);
}
},
false
);
open.call(this, method, url, async, username, password);
};
};
const main = () => {
const urlRegex = /providers/; // Match any url with "providers" in the url.
addInterceptor({
urlRegex,
callback: async (_data) => {
// Replace response data.
return JSON.parse({ hello: 'world' });
},
override: true
});
XMLHttpRequest.prototype.open = createXmlHttpOverride(
XMLHttpRequest.prototype.open
);
};
main();
Based on proposed solution I implemented 'xhr-extensions.ts' file which can be used in typescript solutions.
How to use:
Add file with code to your solution
Import like this
import { XhrSubscription, subscribToXhr } from "your-path/xhr-extensions";
Subscribe like this
const subscription = subscribeToXhr(xhr => {
if (xhr.status != 200) return;
... do something here.
});
Unsubscribe when you don't need subscription anymore
subscription.unsubscribe();
Content of 'xhr-extensions.ts' file
export class XhrSubscription {
constructor(
private callback: (xhr: XMLHttpRequest) => void
) { }
next(xhr: XMLHttpRequest): void {
return this.callback(xhr);
}
unsubscribe(): void {
subscriptions = subscriptions.filter(s => s != this);
}
}
let subscriptions: XhrSubscription[] = [];
export function subscribeToXhr(callback: (xhr: XMLHttpRequest) => void): XhrSubscription {
const subscription = new XhrSubscription(callback);
subscriptions.push(subscription);
return subscription;
}
(function (open) {
XMLHttpRequest.prototype.open = function () {
this.addEventListener("readystatechange", () => {
subscriptions.forEach(s => s.next(this));
}, false);
return open.apply(this, arguments);
};
})(XMLHttpRequest.prototype.open);
Not sure if you can do it with greasemonkey, but if you create an extension then you can use the observer service and the http-on-examine-response observer.

Categories

Resources