Skip to content

Commit

Permalink
capricorn86#526@minor: Continue Adds support for XMLHttpRequest (Sync…
Browse files Browse the repository at this point in the history
…) and remove outdated annotation, fixes some typo error.
  • Loading branch information
Mas0nShi committed Jul 3, 2022
1 parent a71819f commit 1ff5b9a
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 18 deletions.
2 changes: 1 addition & 1 deletion packages/happy-dom/src/event/IEventListener.ts
Expand Up @@ -7,7 +7,7 @@ export default interface IEventListener {
/**
* Handles event.
*
* @param type Event type.
* @param event
*/
handleEvent(event: Event): void;
}
10 changes: 8 additions & 2 deletions packages/happy-dom/src/fetch/FetchHandler.ts
Expand Up @@ -10,7 +10,7 @@ import NodeFetch from 'node-fetch';
*/
export default class FetchHandler {
/**
* Returns resource data asynchonously.
* Returns resource data asynchronously.
*
* @param document Document.
* @param url URL to resource.
Expand All @@ -20,7 +20,13 @@ export default class FetchHandler {
public static fetch(document: IDocument, url: string, init?: IRequestInit): Promise<IResponse> {
// We want to only load NodeFetch when it is needed to improve performance and not have direct dependencies to server side packages.
const taskManager = document.defaultView.happyDOM.asyncTaskManager;

// We need set referer to solve anti-hotlinking.
// And the browser will set the referer to the origin of the page.
if (init) {
if (!init.headers['referer']) {
init.headers['referer'] = document.defaultView.location.origin;
}
}
return new Promise((resolve, reject) => {
const taskID = taskManager.startTask();

Expand Down
4 changes: 2 additions & 2 deletions packages/happy-dom/src/fetch/ResourceFetchHandler.ts
Expand Up @@ -7,7 +7,7 @@ import IDocument from '../nodes/document/IDocument';
*/
export default class ResourceFetchHandler {
/**
* Returns resource data asynchonously.
* Returns resource data asynchronously.
*
* @param document Document.
* @param url URL.
Expand All @@ -24,7 +24,7 @@ export default class ResourceFetchHandler {
}

/**
* Returns resource data synchonously.
* Returns resource data synchronously.
*
* @param document Document.
* @param url URL.
Expand Down
2 changes: 0 additions & 2 deletions packages/happy-dom/src/nodes/document/Document.ts
Expand Up @@ -64,7 +64,6 @@ export default class Document extends Node implements IDocument {
/**
* Creates an instance of Document.
*
* @param defaultView Default view.
*/
constructor() {
super();
Expand Down Expand Up @@ -764,7 +763,6 @@ export default class Document extends Node implements IDocument {
*
* @param node Node to import.
* @param [deep=false] Set to "true" if the clone should be deep.
* @param Imported Node.
*/
public importNode(node: INode, deep = false): INode {
if (!(node instanceof Node)) {
Expand Down
4 changes: 2 additions & 2 deletions packages/happy-dom/src/window/IWindow.ts
Expand Up @@ -310,7 +310,7 @@ export default interface IWindow extends IEventTarget, NodeJS.Global {
* Creates a Base64-encoded ASCII string from a binary string (i.e., a string in which each character in the string is treated as a byte of binary data).
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/btoa
* @param data Binay data.
* @param data Binary data.
* @returns Base64-encoded string.
*/
btoa(data: unknown): string;
Expand All @@ -321,7 +321,7 @@ export default interface IWindow extends IEventTarget, NodeJS.Global {
* @see https://developer.mozilla.org/en-US/docs/Web/API/atob
* @see https://infra.spec.whatwg.org/#forgiving-base64-encode.
* @see Https://html.spec.whatwg.org/multipage/webappapis.html#btoa.
* @param data Binay string.
* @param data Binary string.
* @returns An ASCII string containing decoded data from encodedData.
*/
atob(data: unknown): string;
Expand Down
60 changes: 51 additions & 9 deletions packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts
Expand Up @@ -21,6 +21,8 @@ import {
MajorNodeVersion,
IXMLHttpRequestOptions
} from './XMLHttpReqeustUtility';
import { spawnSync } from "child_process";
const SyncWorkerFile = require.resolve ? require.resolve('./XMLHttpRequestSyncWorker') : null;

/**
* References: https://github.com/souldreamer/xhr2-cookies.
Expand All @@ -46,21 +48,23 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget {
public upload = new XMLHttpRequestUpload();
public responseUrl = '';
public withCredentials = false;
// TODO: another way to set proxy?

public nodejsHttpAgent: HttpAgent = http.globalAgent;
public nodejsHttpsAgent: HttpsAgent = https.globalAgent;

private readonly anonymous: boolean;
private method: string | null = null;
private url: URL | null = null;
private auth: string | null = null;
private body: string | Buffer | ArrayBuffer | ArrayBufferView;
private sync = false;
private headers: { [header: string]: string } = {};
private loweredHeaders: { [lowercaseHeader: string]: string } = {};
private mimeOverride: string | null = null; // TODO: is type right?
private _request: ClientRequest | null = null;
private _response: IncomingMessage | null = null;
// @ts-ignore
private _error: Error | null = null;
private _error: Error | string | null = null;
private responseParts: Buffer[] | null = null;
private responseHeaders: { [lowercaseHeader: string]: string } | null = null;
private loadedBytes = 0;
Expand Down Expand Up @@ -141,10 +145,11 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget {

this.method = method;
this.url = xhrUrl;
this.auth = `${this.url.username || ''}:${this.url.password || ''}`
this.sync = !async;
this.headers = {};
// this.headers = {};
this.loweredHeaders = {};
this.mimeOverride = null;
// this.mimeOverride = null;
this.setReadyState(XMLHttpRequest.OPENED);
this._request = null;
this._response = null;
Expand All @@ -155,6 +160,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget {
this.loadedBytes = 0;
this.totalBytes = 0;
this.lengthComputable = false;

}

/**
Expand Down Expand Up @@ -246,7 +252,8 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget {
if (
this.responseHeaders == null ||
name == null ||
this.readyState in [XMLHttpRequest.OPENED, XMLHttpRequest.UNSENT]
this.readyState === XMLHttpRequest.OPENED ||
this.readyState === XMLHttpRequest.UNSENT
) {
return null;
}
Expand All @@ -263,7 +270,8 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget {
public getAllResponseHeaders(): string {
if (
this.responseHeaders == null ||
this.readyState in [XMLHttpRequest.OPENED, XMLHttpRequest.UNSENT]
this.readyState === XMLHttpRequest.OPENED ||
this.readyState === XMLHttpRequest.UNSENT
) {
return '';
}
Expand All @@ -278,7 +286,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget {
* @param mimeType The MIME type to use.
*/
public overrideMimeType(mimeType: string): void {
if (this.readyState in [XMLHttpRequest.LOADING, XMLHttpRequest.DONE]) {
if (this.readyState === XMLHttpRequest.LOADING || this.readyState === XMLHttpRequest.DONE) {
throw new DOMException(
'overrideMimeType() not allowed in LOADING or DONE',
DOMExceptionNameEnum.invalidStateError
Expand Down Expand Up @@ -315,7 +323,12 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget {
*/
private sendHttp(data?: string | Buffer | ArrayBuffer | ArrayBufferView): void {
const { _defaultView } = XMLHttpRequest;
this.body = data;

if (this.sync) {
const params = this._serialParams();
const res = spawnSync(process.execPath, [SyncWorkerFile], { input: params, maxBuffer: Infinity });
res;
// TODO: sync not implemented.
throw new Error('Synchronous XHR processing not implemented');
}
Expand Down Expand Up @@ -351,7 +364,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget {
hostname: this.url.hostname,
port: +this.url.port,
path: this.url.pathname,
auth: `${this.url.username || ''}:${this.url.password || ''}`,
auth: this.auth,
method: this.method,
headers: this.headers,
agent
Expand Down Expand Up @@ -381,7 +394,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget {
Connection: 'keep-alive',
Host: this.url.host,
'User-Agent': _defaultView.navigator.userAgent,
...(this.anonymous ? { Referer: 'about:blank' } : {})
...(this.anonymous ? { Referer: 'about:blank' } : { Referer: _defaultView.location.origin })
};
this.upload.finalizeHeaders(this.headers, this.loweredHeaders);
}
Expand Down Expand Up @@ -651,4 +664,33 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget {
const charset = /;\s*charset=(.*)$/.exec(this.responseHeaders['content-type'] || '');
return Array.isArray(charset) ? charset[1] : 'utf-8';
}

public _syncGetError(): Error {
return <Error>this._error;
}
public _syncSetErrorString(error: string): void {
this._error = error;
}

private _serialParams(): string {
const { _defaultView } = XMLHttpRequest;
const serials = {
sync: this.sync,
withCredentials: this.withCredentials,
mimeType: this.mimeOverride,
username: this.url.username,
password: this.url.password,
auth: this.auth,
method: this.method,
responseType: this.responseType,
headers: this.headers,
uri: this.url.href,
timeout: this.timeout,
body: this.body,

cookie: _defaultView.document.cookie,
origin: _defaultView.location.href
};
return JSON.stringify(serials);
}
}
@@ -0,0 +1,73 @@
import Window from '../window/Window';
import * as util from 'util';

const window = new Window();
const xhr = new window.XMLHttpRequest();

const chunks = [];

process.stdin.on('data', (chunk) => {
chunks.push(chunk);
});

process.stdin.on('end', () => {
const buffer = Buffer.concat(chunks);

const serials = JSON.parse(buffer.toString());
if (serials.body && serials.body.type === 'Buffer' && serials.body.data) {
serials.body = Buffer.from(serials.body.data);
}
if (serials.origin) {
window.location.href = serials.origin;
}

if (serials.cookie) {
window.document.cookie = serials.cookie;
}

xhr.overrideMimeType(serials.mimeType);
xhr.open(serials.method, serials.uri, true, serials.user, serials.password);
if (serials.headers) {
Object.keys(serials.headers).forEach((key) => {
xhr.setRequestHeader(key, serials.headers[key]);
});
}


xhr.timeout = serials.timeout;

try {
xhr.addEventListener('loadend', () => {
if (xhr._syncGetError()) {
const err = xhr._syncGetError();
xhr._syncSetErrorString(err.stack || util.inspect(err));
}

process.stdout.write(
JSON.stringify({
responseURL: xhr.responseUrl,
responseText: xhr.responseText,
status: xhr.status,
statusText: xhr.statusText,
}),
() => {
process.exit(0);
}
);
});

xhr.send(serials.body);
} catch (error) {
// Properties.error += error.stack || util.inspect(error);
process.stdout.write(
JSON.stringify({
responseURL: xhr.responseUrl,
status: xhr.status,
statusText: xhr.statusText
}),
() => {
process.exit(0);
}
);
}
});

0 comments on commit 1ff5b9a

Please sign in to comment.