blob: 35683fc5c0d3c8fea86b8ab99fb5556c999b0da4 [file] [log] [blame]
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of $LIBRARYNAME;
/**
* A task specification for HTTP requests.
*
* This specification is not available when an HTTP request is sent through
* direct use of [HttpRequest.send]. See [HttpRequestSendTaskSpecification].
*
* A task created from this specification is a `Future<HttpRequest>`.
*
* *Experimental*. This class may disappear without notice.
*/
class HttpRequestTaskSpecification extends TaskSpecification {
/// The URL of the request.
final String url;
/// The HTTP request method.
///
/// By default (when `null`) this is a `"GET"` request. Alternatively, the
/// method can be `"POST"`, `"PUT"`, `"DELETE"`, etc.
final String method;
/// Whether the request should send credentials. Credentials are only useful
/// for cross-origin requests.
///
/// See [HttpRequest.request] for more information.
final bool withCredentials;
/// The desired response format.
///
/// Supported types are:
/// - `""`: (same as `"text"`),
/// - `"arraybuffer"`,
/// - `"blob"`,
/// - `"document"`,
/// - `"json"`,
/// - `"text"`
///
/// When no value is provided (when equal to `null`) defaults to `""`.
final String responseType;
/// The desired MIME type.
///
/// This overrides the default MIME type which is set up to transfer textual
/// data.
final String mimeType;
/// The request headers that should be sent with the request.
final Map<String, String> requestHeaders;
/// The data that is sent with the request.
///
/// When data is provided (the value is not `null`), it must be a
/// [ByteBuffer], [Blob], [Document], [String], or [FormData].
final dynamic sendData;
/// The function that is invoked on progress updates. This function is
/// registered as an event listener on the created [HttpRequest] object, and
/// thus has its own task. Further invocations of the progress function do
/// *not* use the HTTP request task as task object.
///
/// Creating an HTTP request automatically registers the on-progress listener.
final ZoneUnaryCallback<dynamic, ProgressEvent> onProgress;
HttpRequestTaskSpecification(this.url,
{String this.method, bool this.withCredentials, String this.responseType,
String this.mimeType, Map<String, String> this.requestHeaders,
this.sendData,
void this.onProgress(ProgressEvent e)});
String get name => "dart.html.http-request";
bool get isOneShot => true;
}
/**
* A task specification for HTTP requests that are initiated through a direct
* invocation of [HttpRequest.send].
*
* This specification serves as signal to zones that an HTTP request has been
* initiated. The created task is the [request] object itself, and
* no callback is ever executed in this task.
*
* Note that event listeners on the HTTP request are also registered in the
* zone (although with their own task creations), and that a zone can thus
* detect when the HTTP request returns.
*
* HTTP requests that are initiated through `request` methods don't use
* this class but use [HttpRequestTaskSpecification].
*
* *Experimental*. This class may disappear without notice.
*/
class HttpRequestSendTaskSpecification extends TaskSpecification {
final HttpRequest request;
final dynamic sendData;
HttpRequestSendTaskSpecification(this.request, this.sendData);
String get name => "dart.html.http-request-send";
/**
* No callback is ever executed in an HTTP request send task.
*/
bool get isOneShot => false;
}
/**
* A client-side XHR request for getting data from a URL,
* formally known as XMLHttpRequest.
*
* HttpRequest can be used to obtain data from HTTP and FTP protocols,
* and is useful for AJAX-style page updates.
*
* The simplest way to get the contents of a text file, such as a
* JSON-formatted file, is with [getString].
* For example, the following code gets the contents of a JSON file
* and prints its length:
*
* var path = 'myData.json';
* HttpRequest.getString(path)
* .then((String fileContents) {
* print(fileContents.length);
* })
* .catchError((Error error) {
* print(error.toString());
* });
*
* ## Fetching data from other servers
*
* For security reasons, browsers impose restrictions on requests
* made by embedded apps.
* With the default behavior of this class,
* the code making the request must be served from the same origin
* (domain name, port, and application layer protocol)
* as the requested resource.
* In the example above, the myData.json file must be co-located with the
* app that uses it.
* You might be able to
* [get around this restriction](http://www.dartlang.org/articles/json-web-service/#a-note-on-cors-and-httprequest)
* by using CORS headers or JSONP.
*
* ## Other resources
*
* * [Fetch Data Dynamically](https://www.dartlang.org/docs/tutorials/fetchdata/),
* a tutorial from _A Game of Darts_,
* shows two different ways to use HttpRequest to get a JSON file.
* * [Get Input from a Form](https://www.dartlang.org/docs/tutorials/forms/),
* another tutorial from _A Game of Darts_,
* shows using HttpRequest with a custom server.
* * [Dart article on using HttpRequests](http://www.dartlang.org/articles/json-web-service/#getting-data)
* * [JS XMLHttpRequest](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest)
* * [Using XMLHttpRequest](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest)
*/
$(ANNOTATIONS)$(NATIVESPEC)$(CLASS_MODIFIERS)class $CLASSNAME$EXTENDS$IMPLEMENTS {
/**
* Creates a GET request for the specified [url].
*
* The server response must be a `text/` mime type for this request to
* succeed.
*
* This is similar to [request] but specialized for HTTP GET requests which
* return text content.
*
* To add query parameters, append them to the [url] following a `?`,
* joining each key to its value with `=` and separating key-value pairs with
* `&`.
*
* var name = Uri.encodeQueryComponent('John');
* var id = Uri.encodeQueryComponent('42');
* HttpRequest.getString('users.json?name=$name&id=$id')
* .then((HttpRequest resp) {
* // Do something with the response.
* });
*
* See also:
*
* * [request]
*/
static Future<String> getString(String url,
{bool withCredentials, void onProgress(ProgressEvent e)}) {
return request(url, withCredentials: withCredentials,
onProgress: onProgress).then((HttpRequest xhr) => xhr.responseText);
}
/**
* Makes a server POST request with the specified data encoded as form data.
*
* This is roughly the POST equivalent of getString. This method is similar
* to sending a FormData object with broader browser support but limited to
* String values.
*
* If [data] is supplied, the key/value pairs are URI encoded with
* [Uri.encodeQueryComponent] and converted into an HTTP query string.
*
* Unless otherwise specified, this method appends the following header:
*
* Content-Type: application/x-www-form-urlencoded; charset=UTF-8
*
* Here's an example of using this method:
*
* var data = { 'firstName' : 'John', 'lastName' : 'Doe' };
* HttpRequest.postFormData('/send', data).then((HttpRequest resp) {
* // Do something with the response.
* });
*
* See also:
*
* * [request]
*/
static Future<HttpRequest> postFormData(String url, Map<String, String> data,
{bool withCredentials, String responseType,
Map<String, String> requestHeaders,
void onProgress(ProgressEvent e)}) {
var parts = [];
data.forEach((key, value) {
parts.add('${Uri.encodeQueryComponent(key)}='
'${Uri.encodeQueryComponent(value)}');
});
var formData = parts.join('&');
if (requestHeaders == null) {
requestHeaders = <String, String>{};
}
requestHeaders.putIfAbsent('Content-Type',
() => 'application/x-www-form-urlencoded; charset=UTF-8');
return request(url, method: 'POST', withCredentials: withCredentials,
responseType: responseType,
requestHeaders: requestHeaders, sendData: formData,
onProgress: onProgress);
}
/**
* Creates and sends a URL request for the specified [url].
*
* By default `request` will perform an HTTP GET request, but a different
* method (`POST`, `PUT`, `DELETE`, etc) can be used by specifying the
* [method] parameter. (See also [HttpRequest.postFormData] for `POST`
* requests only.
*
* The Future is completed when the response is available.
*
* If specified, `sendData` will send data in the form of a [ByteBuffer],
* [Blob], [Document], [String], or [FormData] along with the HttpRequest.
*
* If specified, [responseType] sets the desired response format for the
* request. By default it is [String], but can also be 'arraybuffer', 'blob',
* 'document', 'json', or 'text'. See also [HttpRequest.responseType]
* for more information.
*
* The [withCredentials] parameter specified that credentials such as a cookie
* (already) set in the header or
* [authorization headers](http://tools.ietf.org/html/rfc1945#section-10.2)
* should be specified for the request. Details to keep in mind when using
* credentials:
*
* * Using credentials is only useful for cross-origin requests.
* * The `Access-Control-Allow-Origin` header of `url` cannot contain a wildcard (*).
* * The `Access-Control-Allow-Credentials` header of `url` must be set to true.
* * If `Access-Control-Expose-Headers` has not been set to true, only a subset of all the response headers will be returned when calling [getAllRequestHeaders].
*
* The following is equivalent to the [getString] sample above:
*
* var name = Uri.encodeQueryComponent('John');
* var id = Uri.encodeQueryComponent('42');
* HttpRequest.request('users.json?name=$name&id=$id')
* .then((HttpRequest resp) {
* // Do something with the response.
* });
*
* Here's an example of submitting an entire form with [FormData].
*
* var myForm = querySelector('form#myForm');
* var data = new FormData(myForm);
* HttpRequest.request('/submit', method: 'POST', sendData: data)
* .then((HttpRequest resp) {
* // Do something with the response.
* });
*
* Note that requests for file:// URIs are only supported by Chrome extensions
* with appropriate permissions in their manifest. Requests to file:// URIs
* will also never fail- the Future will always complete successfully, even
* when the file cannot be found.
*
* See also: [authorization headers](http://en.wikipedia.org/wiki/Basic_access_authentication).
*/
static Future<HttpRequest> request(String url,
{String method, bool withCredentials, String responseType,
String mimeType, Map<String, String> requestHeaders, sendData,
void onProgress(ProgressEvent e)}) {
var spec = new HttpRequestTaskSpecification(
url, method: method,
withCredentials: withCredentials,
responseType: responseType,
mimeType: mimeType,
requestHeaders: requestHeaders,
sendData: sendData,
onProgress: onProgress);
if (identical(Zone.current, Zone.ROOT)) {
return _createHttpRequestTask(spec, null);
}
return Zone.current.createTask(_createHttpRequestTask, spec);
}
static Future<HttpRequest> _createHttpRequestTask(
HttpRequestTaskSpecification spec, Zone zone) {
String url = spec.url;
String method = spec.method;
bool withCredentials = spec.withCredentials;
String responseType = spec.responseType;
String mimeType = spec.mimeType;
Map<String, String> requestHeaders = spec.requestHeaders;
var sendData = spec.sendData;
var onProgress = spec.onProgress;
var completer = new Completer<HttpRequest>();
var task = completer.future;
var xhr = new HttpRequest();
if (method == null) {
method = 'GET';
}
xhr.open(method, url, async: true);
if (withCredentials != null) {
xhr.withCredentials = withCredentials;
}
if (responseType != null) {
xhr.responseType = responseType;
}
if (mimeType != null) {
xhr.overrideMimeType(mimeType);
}
if (requestHeaders != null) {
requestHeaders.forEach((header, value) {
xhr.setRequestHeader(header, value);
});
}
if (onProgress != null) {
xhr.onProgress.listen(onProgress);
}
xhr.onLoad.listen((e) {
var accepted = xhr.status >= 200 && xhr.status < 300;
var fileUri = xhr.status == 0; // file:// URIs have status of 0.
var notModified = xhr.status == 304;
// Redirect status is specified up to 307, but others have been used in
// practice. Notably Google Drive uses 308 Resume Incomplete for
// resumable uploads, and it's also been used as a redirect. The
// redirect case will be handled by the browser before it gets to us,
// so if we see it we should pass it through to the user.
var unknownRedirect = xhr.status > 307 && xhr.status < 400;
var isSuccessful = accepted || fileUri || notModified || unknownRedirect;
if (zone == null && isSuccessful) {
completer.complete(xhr);
} else if (zone == null) {
completer.completeError(e);
} else if (isSuccessful) {
zone.runTask((task, value) {
completer.complete(value);
}, task, xhr);
} else {
zone.runTask((task, error) {
completer.completeError(error);
}, task, e);
}
});
if (zone == null) {
xhr.onError.listen(completer.completeError);
} else {
xhr.onError.listen((error) {
zone.runTask((task, error) {
completer.completeError(error);
}, task, error);
});
}
if (sendData != null) {
// TODO(floitsch): should we go through 'send()' and have nested tasks?
xhr._send(sendData);
} else {
xhr._send();
}
return task;
}
/**
* Checks to see if the Progress event is supported on the current platform.
*/
static bool get supportsProgressEvent {
$if DART2JS
var xhr = new HttpRequest();
return JS('bool', '("onprogress" in #)', xhr);
$else
return true;
$endif
}
/**
* Checks to see if the current platform supports making cross origin
* requests.
*
* Note that even if cross origin requests are supported, they still may fail
* if the destination server does not support CORS requests.
*/
static bool get supportsCrossOrigin {
$if DART2JS
var xhr = new HttpRequest();
return JS('bool', '("withCredentials" in #)', xhr);
$else
return true;
$endif
}
/**
* Checks to see if the LoadEnd event is supported on the current platform.
*/
static bool get supportsLoadEndEvent {
$if DART2JS
var xhr = new HttpRequest();
return JS('bool', '("onloadend" in #)', xhr);
$else
return true;
$endif
}
/**
* Checks to see if the overrideMimeType method is supported on the current
* platform.
*/
static bool get supportsOverrideMimeType {
$if DART2JS
var xhr = new HttpRequest();
return JS('bool', '("overrideMimeType" in #)', xhr);
$else
return true;
$endif
}
/**
* Makes a cross-origin request to the specified URL.
*
* This API provides a subset of [request] which works on IE9. If IE9
* cross-origin support is not required then [request] should be used instead.
*/
@Experimental()
static Future<String> requestCrossOrigin(String url,
{String method, String sendData}) {
if (supportsCrossOrigin) {
return request(url, method: method, sendData: sendData).then((xhr) {
return xhr.responseText;
});
}
// TODO(floitsch): the following code doesn't go through task zones.
// Since 'XDomainRequest' is an IE9 feature we should probably just remove
// it.
$if DART2JS
var completer = new Completer<String>();
if (method == null) {
method = 'GET';
}
var xhr = JS('var', 'new XDomainRequest()');
JS('', '#.open(#, #)', xhr, method, url);
JS('', '#.onload = #', xhr, convertDartClosureToJS((e) {
var response = JS('String', '#.responseText', xhr);
completer.complete(response);
}, 1));
JS('', '#.onerror = #', xhr, convertDartClosureToJS((e) {
completer.completeError(e);
}, 1));
// IE9 RTM - XDomainRequest issued requests may abort if all event handlers
// not specified
// http://social.msdn.microsoft.com/Forums/ie/en-US/30ef3add-767c-4436-b8a9-f1ca19b4812e/ie9-rtm-xdomainrequest-issued-requests-may-abort-if-all-event-handlers-not-specified
JS('', '#.onprogress = {}', xhr);
JS('', '#.ontimeout = {}', xhr);
JS('', '#.timeout = Number.MAX_VALUE', xhr);
if (sendData != null) {
JS('', '#.send(#)', xhr, sendData);
} else {
JS('', '#.send()', xhr);
}
return completer.future;
$endif
}
/**
* Returns all response headers as a key-value map.
*
* Multiple values for the same header key can be combined into one,
* separated by a comma and a space.
*
* See: http://www.w3.org/TR/XMLHttpRequest/#the-getresponseheader()-method
*/
Map<String, String> get responseHeaders {
// from Closure's goog.net.Xhrio.getResponseHeaders.
var headers = <String, String>{};
var headersString = this.getAllResponseHeaders();
if (headersString == null) {
return headers;
}
var headersList = headersString.split('\r\n');
for (var header in headersList) {
if (header.isEmpty) {
continue;
}
var splitIdx = header.indexOf(': ');
if (splitIdx == -1) {
continue;
}
var key = header.substring(0, splitIdx).toLowerCase();
var value = header.substring(splitIdx + 2);
if (headers.containsKey(key)) {
headers[key] = '${headers[key]}, $value';
} else {
headers[key] = value;
}
}
return headers;
}
/**
* Specify the desired `url`, and `method` to use in making the request.
*
* By default the request is done asyncronously, with no user or password
* authentication information. If `async` is false, the request will be send
* synchronously.
*
* Calling `open` again on a currently active request is equivalent to
* calling `abort`.
*
* Note: Most simple HTTP requests can be accomplished using the [getString],
* [request], [requestCrossOrigin], or [postFormData] methods. Use of this
* `open` method is intended only for more complex HTTP requests where
* finer-grained control is needed.
*/
@DomName('XMLHttpRequest.open')
@DocsEditable()
$if JSINTEROP
void open(String method, String url, {bool async, String user, String password}) {
if (async == null && user == null && password == null) {
_blink.BlinkXMLHttpRequest.instance.open_Callback_2_(this, method, url);
} else {
_blink.BlinkXMLHttpRequest.instance.open_Callback_5_(this, method, url, async, user, password);
}
}
$else
void open(String method, String url, {bool async, String user, String password}) native;
$endif
/**
* Sends the request with any given `data`.
*
* Note: Most simple HTTP requests can be accomplished using the [getString],
* [request], [requestCrossOrigin], or [postFormData] methods. Use of this
* `send` method is intended only for more complex HTTP requests where
* finer-grained control is needed.
*
* ## Other resources
*
* * [XMLHttpRequest.send](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#send%28%29)
* from MDN.
*/
@DomName('XMLHttpRequest.send')
@DocsEditable()
void send([body_OR_data]) {
if (identical(Zone.current, Zone.ROOT)) {
_send(body_OR_data);
} else {
Zone.current.createTask(_createHttpRequestSendTask,
new HttpRequestSendTaskSpecification(this, body_OR_data));
}
}
static HttpRequest _createHttpRequestSendTask(
HttpRequestSendTaskSpecification spec, Zone zone) {
spec.request._send(spec.sendData);
return spec.request;
}
$!MEMBERS
}