TouchDevelop/lib/WebRequest.ts

410 строки
18 KiB
TypeScript

///<reference path='refs.ts'/>
module TDev.RT {
export interface IProxyResponse {
response: WebResponse;
statusCode: number;
}
//? An HTTP web request
//@ stem("request") ctx(general,gckey)
export class WebRequest
extends RTValue {
private _method: string = undefined;
private _url: string = undefined;
private _showNotifications: boolean = true;
private _proxyResponseType: string = undefined;
constructor () {
super()
}
private _headers: StringMap = new StringMap();
private _content: any = undefined;
private _credentialsName: string = undefined;
private _credentialsPassword: string = undefined;
private _responseReceived: RT.Event_ = new RT.Event_();
static mk(url: string, proxyResponseType: string): WebRequest {
var wr = new WebRequest();
wr._url = url;
wr._method = "GET";
wr._proxyResponseType = proxyResponseType;
return wr;
}
//? Indicates if program notifications should be shown to the user. Default is true.
public show_notifications(visible: boolean) {
this._showNotifications = visible;
}
public proxyResponseType(): string {
return this._proxyResponseType;
}
//? Gets whether it was a 'get' or a 'post'.
public method(): string { return this._method; }
//? Sets the method. Default value is 'get'.
//@ [method].deflStrings("post", "put", "get", "delete")
public set_method(method: string): void { this._method = method; }
//? Gets the url of the request
public url(): string { return this._url; }
//? Sets the url of the request. Must be a valid internet address.
public set_url(url: string): void { this._url = url; }
//? Gets the value of a given header
//@ readsMutable
public header(name: string): string { return this._headers.at(name); }
//? Sets an HTML header value. Empty string clears the value
public set_header(name: string, value: string): void {
if (!value)
this._headers.remove(name);
else
this._headers.set_at(name, value);
}
//? Indicates if both requests are the same instance.
public equals(other: WebRequest): boolean {
return this == other;
}
public toString(): string {
return this.method() + " " + this.url();
}
//? Sets the Accept header type ('text/xml' for xml, 'application/json' for json).
//@ [type].deflStrings('application/json', 'text/xml')
public set_accept(type: string) {
this._headers.set_at("Accept", type);
}
//? Displays the request to the wall
public post_to_wall(s: IStackFrame): void {
var rt = s.rt;
if (this._content && this._content.length)
rt.postBoxedText("Content-Length: " + this._content.length, s.pc);
var keys = this._headers.keys();
for (var i = 0; i < keys.count(); ++i) {
var key = keys.at(i);
rt.postBoxedText(keys.at(i) + ": " + this._headers.at(key), s.pc);
}
if (this._credentialsName || this._credentialsPassword)
rt.postBoxedText("credentials: " + this._credentialsName, s.pc);
rt.postBoxedText(this.toString(), s.pc);
}
public serializeForProxy() {
var credentials = undefined;
if (this._credentialsName || this._credentialsPassword) {
credentials = {
name: this._credentialsName || "",
password: this._credentialsPassword || ""
};
}
var headers = this._headers.keys().a.map(k => {
return { name: k, value: this._headers.at(k) };
});
return {
url: this._url,
method: this._method,
contentText: typeof this._content == "string" ? this._content : undefined,
content: this._content instanceof Uint8Array ? Util.base64EncodeBytes(this._content) : undefined,
responseType: this._proxyResponseType,
headers: headers,
credentials: credentials
}
}
public set_content_type(contentType: string) { this._headers.set_at("Content-Type", contentType); }
public debuggerChildren(): any {
var r = {
'method': this._method,
'url': this._url,
'headers': this._headers,
'content': this._content,
'user name': this._credentialsName,
'password': this._credentialsPassword,
'notifications': this._showNotifications
};
return r;
}
private mkProxyCrash(proxyResponse: WebResponse): IProxyResponse {
return {
statusCode: proxyResponse.status_code(),
response : WebResponse.mkCrash(this)
};
}
private sendViaProxyAsync(): Promise { // ProxyResponse
if (!Util.check(!!this._proxyResponseType)) return Promise.as(<IProxyResponse>{ statusCode: 0, response: WebResponse.mkCrash(this) });
var proxy = WebRequest.mk(Cloud.getPrivateApiUrl("runtime/web/request"), undefined);
proxy.set_method("POST");
proxy.set_content(JSON.stringify(this.serializeForProxy()))
return proxy.sendAsync().then(proxyResponse => {
switch (proxyResponse.status_code()) {
case 502:
if (this._showNotifications)
HTML.showProxyNotification("Proxy Error: Could not perform web request. " + Cloud.onlineInfo(), this._url);
return this.mkProxyCrash(proxyResponse);
case 503:
if (this._showNotifications)
HTML.showProxyNotification("Proxy Error: Could not perform web request. Did you transfer a lot of data recently? (code 503)", this._url);
return this.mkProxyCrash(proxyResponse);
case 403:
Cloud.accessTokenExpired();
if (this._showNotifications)
HTML.showProxyNotification("Proxy Error: Could not perform web request; access denied; your access token might have expired.", this._url);
return this.mkProxyCrash(proxyResponse);
case 504:
if (this._showNotifications)
HTML.showProxyNotification("Proxy Error: Could not perform web request. Response too big. (code 504)", this._url);
return this.mkProxyCrash(proxyResponse);
case 400:
if (this._showNotifications)
HTML.showProxyNotification("Proxy Error: Malformed inputs: " + Util.decodeErrorMessage(proxyResponse.header("ErrorMessage")), this._url);
return this.mkProxyCrash(proxyResponse);
default:
return <IProxyResponse>{
statusCode: proxyResponse.status_code(),
response: WebResponse.mkProxy(this, JSON.parse(proxyResponse.content()))
};
}
});
}
private prepareAndSend(client: XMLHttpRequest) {
if (this._credentialsName || this._credentialsPassword) {
client.open(this.method().toUpperCase(), this.url(), true, this._credentialsName || "", this._credentialsPassword || "");
client.withCredentials = true;
}
else
client.open(this.method().toUpperCase(), this.url(), true);
// for some reason WebWorkers don't have FormData
var isForms = !isWebWorker && !!this._content && this._content instanceof FormData;
var keys = this._headers.keys();
for (var i = 0; i < keys.count(); ++i) {
var header = keys.at(i);
if (isForms && /^content-type$/i.test(header)) continue; // content-type set by browser when sending form
var headerValue = this._headers.at(header);
client.setRequestHeader(header, headerValue);
if (/^accept$/i.test(header) && /^image\/|^audio\//i.test(headerValue)) {
client.responseType = 'arraybuffer';
}
}
Time.log(this.toString());
client.send(this._content);
}
public sendAsync(): Promise {
return this.sendCoreAsync();
}
public sendCoreAsync(): Promise {
var request = this;
return new Promise((onSuccess: (v: any) => any, onError: (v: any) => any, onProgress: (v: any) => any) => {
// quick check for connectivity
if (Cloud.isOffline()) {
if (request._showNotifications)
HTML.showNotificationText('Web request failed, please connect to Internet.');
onSuccess(WebResponse.mkCrash(request));
return;
}
var onCORS = function () {
if (request._proxyResponseType) // CORS exception happened, and we are allowed to proxy
{
Cloud.authenticateAsync(lf("web request proxying"))
.done((authenticated) => {
if (!authenticated) onSuccess(WebResponse.mkCrash(request));
else request.sendViaProxyAsync().then((r: IProxyResponse) => {
// expired token?
if (r.statusCode == 403) {
// try to regresh token...
Cloud.authenticateAsync(lf("web request proxying"))
.done((authenticated) => {
if (!authenticated) onSuccess(WebResponse.mkCrash(request));
else request.sendViaProxyAsync().then((r: IProxyResponse) => onSuccess(r.response), e => onError(e));
});
} else onSuccess(r.response)
}, e => onError(e));
});
}
else {
if (request._showNotifications)
HTML.showCorsNotification(request.url());
onSuccess(WebResponse.mkCrash(request));
}
}
// calls from HTTPS to HTTP never work and don't call "onerror" in Chrome 38+
if (Web.proxy(this.url()) != this.url() &&
/^https:\//i.test(document.URL) && /^http:\//.test(this.url())) {
onCORS();
return
}
try {
var client: XMLHttpRequest = new XMLHttpRequest();
client.onerror = (e: ErrorEvent) => {
Time.log('error with ' + this.toString());
}
client.onreadystatechange = () => {
if (client.readyState == (XMLHttpRequest.DONE || 4)) {
if (client.status == 0)
onCORS();
else {
var r = WebResponse.mk(request, client);
onSuccess(<any>r);
}
}
};
request.prepareAndSend(client);
} catch (e) {
onCORS();
}
});
}
public testCORSAsync(): Promise {
if (Web.proxy(this._url) == this._url) return Promise.as(false);
var request = this;
return new Promise((onSuccess: (v: any) => any, onError: (v: any) => any, onProgress: (v: any) => any) =>
{
try {
var client: XMLHttpRequest = new XMLHttpRequest();
client.onerror = (e: ErrorEvent) => Time.log('error with ' + this.toString());
client.onreadystatechange = () =>
{
if (client.readyState == (XMLHttpRequest.DONE || 4)) {
onSuccess(client.status == 0);
}
};
request.prepareAndSend(client);
} catch (e) {
onSuccess(true);
}
});
}
//? User ``send`` instead
//@ cap(network) flow(SinkWeb) hidden
public send_async(s: IStackFrame) {
this.sendAsync().then((response: WebResponse) =>
{
if (this._responseReceived && this._responseReceived.handlers)
s.rt.queueLocalEvent(this._responseReceived, [response]);
}, (e) =>
{
var r = WebResponse.mkCrash(this);
if (this._responseReceived && this._responseReceived.handlers)
s.rt.queueLocalEvent(this._responseReceived, [r]);
});
}
//? Use ``send`` instead
//@ ignoreReturnValue hidden
public on_response_received(handler:WebResponseAction) : EventBinding {
return this._responseReceived.addHandler(handler);
}
//? Performs the request synchronously
//@ async cap(network) flow(SinkWeb) returns(WebResponse)
public send(r: ResumeCtx) {
this.sendAsync().then((response: WebResponse) =>
{
r.resumeVal(response);
}, e => {
r.resumeVal(WebResponse.mkCrash(this));
});
}
//? Sets the content of a 'post' request
public set_content(content: string): void {
this._content = content;
this.set_content_type("text/plain; charset=utf-8");
}
//? Sets the content of a 'post' request as the JSON tree
public set_content_as_json(json: JsonObject): void {
this.set_content(json.toString());
this.set_content_type("application/json; charset=utf-8");
}
//? Sets the content of a 'post' request as a binary buffer
public set_content_as_buffer(bytes: Buffer): void {
this._content = bytes.buffer;
this.set_content_type("application/octet-stream");
}
//? Sets the content as multipart/form-data.
public set_content_as_form(form: FormBuilder): void {
this._content = form.data();
// set by browser
// this.set_content_type("multipart/form-data");
if (/^get$/i.test(this.method()))
this.set_method("post");
}
//? Sets the content of a 'post' request as a JPEG encoded image. Quality from 0 (worse) to 1 (best).
//@ [quality].defl(0.85) picAsync
public set_content_as_picture(pic: Picture, quality: number, r:ResumeCtx): void {
pic.loadFirst(r, () => {
this.setContentAsPictureInternal(pic, quality);
})
}
public setContentAsSoundInternal(snd : Sound) : void {
var url = snd.getDataUri();
if (url) {
var mimeType = Sound.dataUriMimeType(url);
var bytes = Util.decodeDataURL(url, mimeType);
if (bytes) {
this._content = bytes;
this.set_content_type(mimeType);
}
}
}
public setContentAsPictureInternal(pic: Picture, quality: number, forceJpeg = false): void
{
quality = Math_.normalize(quality);
var mimeType = (quality >= 1 && !forceJpeg) ? "image/png" : "image/jpeg";
var jpegUrl = pic.getDataUri(quality, -1, forceJpeg);
var bytes = Util.decodeDataURL(jpegUrl, mimeType);
if (bytes) {
this._content = bytes;
this.set_content_type(mimeType);
}
}
//? Sets the content of a 'post' request as the XML tree
//@ import("npm", "xmldom", "0.1.*")
public set_content_as_xml(xml: XmlObject): void {
this.set_content(xml.toString());
this.set_content_type("text/xml; charset=utf-8");
}
//? Sets the name and password for basic authentication. Requires an HTTPS URL, empty string clears.
public set_credentials(name: string, password: string): void {
if (!this.url().match(/^https:\/\//i))
Util.userError(lf("Web Request->set credentials requires a secure HTTP url (https)"));
this._credentialsName = name;
this._credentialsPassword = password;
}
//? Gets the names of the headers
//@ readsMutable
public header_names(): Collection<string> {
return this._headers.keys();
}
//? Compresses the request content with gzip and sets the Content-Encoding header
//@ [value].defl(true)
//@ obsolete
public set_compress(value: boolean): void { }
}
}