| // Copyright (c) 2023, 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. |
| |
| import 'dart:async'; |
| import 'dart:js_interop'; |
| import 'dart:typed_data'; |
| |
| import '../dom.dart'; |
| import 'events/events.dart' show XHRGetters; |
| |
| /// A helper used to make it easier to operate over [XMLHttpRequest]s. |
| /// |
| /// The logic here was copied from `dart:html` to help bridge a functionality |
| /// gap missing in `package:web`. |
| /// |
| /// 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) { |
| /// 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. |
| /// |
| /// ## Other resources |
| /// |
| /// * [Fetch data dynamically](https://dart.dev/tutorials/web/fetch-data/), |
| /// a tutorial shows how to load data from a static file or from a server. |
| /// * [Dart article on using HttpRequests](https://dart.dev/guides/libraries/library-tour#using-http-resources-with-httprequest) |
| /// * [JS XMLHttpRequest](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest) |
| /// * [Using XMLHttpRequest](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest) |
| class HttpRequest { |
| // The lint is ignored because these are purposely made to match the old |
| // names used in `dart:html` |
| // ignore:constant_identifier_names |
| static const int DONE = 4; |
| // ignore:constant_identifier_names |
| static const int HEADERS_RECEIVED = 2; |
| // ignore:constant_identifier_names |
| static const int LOADING = 3; |
| // ignore:constant_identifier_names |
| static const int OPENED = 1; |
| // ignore:constant_identifier_names |
| static const int UNSENT = 0; |
| |
| /// Creates a GET request for the specified [url]. |
| /// |
| /// 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((String resp) { |
| /// // Do something with the response. |
| /// }); |
| /// |
| /// See also [request]. |
| static Future<String> getString(String url, |
| {bool? withCredentials, void Function(ProgressEvent)? onProgress}) => |
| request(url, withCredentials: withCredentials, onProgress: onProgress) |
| .then((r) => r.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<XMLHttpRequest> postFormData( |
| String url, Map<String, String> data, |
| {bool? withCredentials, |
| String? responseType, |
| Map<String, String>? requestHeaders, |
| void Function(ProgressEvent)? onProgress}) { |
| final parts = <String>[]; |
| data.forEach((key, value) { |
| parts.add('${Uri.encodeQueryComponent(key)}=' |
| '${Uri.encodeQueryComponent(value)}'); |
| }); |
| final formData = parts.join('&'); |
| 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'. |
| /// 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 |
| /// `getAllResponseHeaders`. |
| /// |
| /// 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][1]. |
| /// |
| /// [1]: http://en.wikipedia.org/wiki/Basic_access_authentication |
| static Future<XMLHttpRequest> request(String url, |
| {String? method, |
| bool? withCredentials, |
| String? responseType, |
| String? mimeType, |
| Map<String, String>? requestHeaders, |
| Object? sendData, |
| void Function(ProgressEvent)? onProgress}) { |
| final completer = Completer<XMLHttpRequest>(); |
| final xhr = XMLHttpRequest(); |
| method ??= 'GET'; |
| xhr.open(method, url, true); |
| |
| if (withCredentials != null) { |
| xhr.withCredentials = withCredentials; |
| } |
| |
| if (responseType != null) { |
| xhr.responseType = responseType; |
| } |
| |
| if (mimeType != null) { |
| xhr.overrideMimeType(mimeType); |
| } |
| |
| // ignore: unnecessary_lambdas |
| requestHeaders?.forEach((a, b) => xhr.setRequestHeader(a, b)); |
| |
| if (onProgress != null) { |
| xhr.onProgress.listen(onProgress); |
| } |
| |
| xhr.onLoad.listen((ProgressEvent e) { |
| final status = xhr.status; |
| final accepted = status >= 200 && status < 300; |
| final fileUri = status == 0; // file:// URIs have status of 0. |
| final notModified = 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. |
| final unknownRedirect = status > 307 && status < 400; |
| |
| if (accepted || fileUri || notModified || unknownRedirect) { |
| completer.complete(xhr); |
| } else { |
| completer.completeError(e); |
| } |
| }); |
| |
| xhr.onError.listen(completer.completeError); |
| |
| if (sendData != null) { |
| xhr.send(sendData is String ? sendData.toJS : sendData.jsify()); |
| } else { |
| xhr.send(); |
| } |
| |
| return completer.future; |
| } |
| } |