platform/lib/src/http/response_context.dart

254 lines
7.2 KiB
Dart
Raw Normal View History

library angel_framework.http.response_context;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
2016-10-22 20:41:36 +00:00
import 'package:angel_route/angel_route.dart';
import 'package:json_god/json_god.dart' as god;
import 'package:mime/mime.dart';
import '../extensible.dart';
import 'angel_base.dart';
import 'controller.dart';
2016-04-18 03:27:23 +00:00
2016-12-21 03:10:03 +00:00
final RegExp _contentType =
new RegExp(r'([^/\n]+)\/\s*([^;\n]+)\s*(;\s*charset=([^$;\n]+))?');
2016-11-28 00:49:27 +00:00
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
2016-04-18 03:27:23 +00:00
/// A convenience wrapper around an outgoing HTTP request.
class ResponseContext extends Extensible {
2016-10-22 20:41:36 +00:00
bool _isOpen = true;
2016-04-18 03:27:23 +00:00
/// The [Angel] instance that is sending a response.
AngelBase app;
2016-04-18 03:27:23 +00:00
2016-12-19 04:32:36 +00:00
/// Is `Transfer-Encoding` chunked?
bool chunked;
2016-12-19 01:38:23 +00:00
/// Any and all cookies to be sent to the user.
final List<Cookie> cookies = [];
/// Headers that will be sent to the user.
final Map<String, String> headers = {};
/// This response's status code.
int statusCode = 200;
2016-04-18 03:27:23 +00:00
/// Can we still write to this response?
2016-10-22 20:41:36 +00:00
bool get isOpen => _isOpen;
2016-04-18 03:27:23 +00:00
/// A set of UTF-8 encoded bytes that will be written to the response.
2016-10-22 20:41:36 +00:00
final BytesBuilder buffer = new BytesBuilder();
2016-04-18 03:27:23 +00:00
/// Sets the status code to be sent with this response.
2016-12-19 01:38:23 +00:00
@Deprecated('Please use `statusCode=` instead.')
2016-10-22 20:41:36 +00:00
void status(int code) {
2016-12-19 01:38:23 +00:00
statusCode = code;
2016-04-18 03:27:23 +00:00
}
/// The underlying [HttpResponse] under this instance.
2016-11-23 19:50:17 +00:00
final HttpResponse io;
2016-04-18 03:27:23 +00:00
2016-11-23 19:50:17 +00:00
@deprecated
HttpResponse get underlyingRequest {
throw new Exception(
'`ResponseContext#underlyingResponse` is deprecated. Please update your application to use the newer `ResponseContext#io`.');
}
2016-12-21 03:10:03 +00:00
/// Gets the Content-Type header.
ContentType get contentType {
if (!headers.containsKey(HttpHeaders.CONTENT_TYPE)) return null;
var header = headers[HttpHeaders.CONTENT_TYPE];
var match = _contentType.firstMatch(header);
if (match == null)
throw new Exception('Malformed Content-Type response header: "$header".');
if (match[4]?.isNotEmpty != true)
return new ContentType(match[1], match[2]);
else
return new ContentType(match[1], match[2], charset: match[4]);
}
/// Sets the Content-Type header.
void set contentType(ContentType contentType) {
headers[HttpHeaders.CONTENT_TYPE] = contentType.toString();
}
2016-11-23 19:50:17 +00:00
ResponseContext(this.io, this.app);
2016-04-18 03:27:23 +00:00
/// Set this to true if you will manually close the response.
2016-12-21 18:18:26 +00:00
///
/// If `true`, all response finalizers will be skipped.
2016-04-18 03:27:23 +00:00
bool willCloseItself = false;
/// Sends a download as a response.
2016-10-22 20:41:36 +00:00
download(File file, {String filename}) async {
2016-12-19 01:38:23 +00:00
headers["Content-Disposition"] =
'attachment; filename="${filename ?? file.path}"';
headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path);
headers[HttpHeaders.CONTENT_LENGTH] = file.lengthSync().toString();
2016-10-22 20:41:36 +00:00
buffer.add(await file.readAsBytes());
end();
2016-04-18 03:27:23 +00:00
}
/// Prevents more data from being written to the response.
2016-10-22 20:41:36 +00:00
void end() {
_isOpen = false;
}
2016-04-18 03:27:23 +00:00
/// Sets a response header to the given value, or retrieves its value.
2016-12-19 01:38:23 +00:00
@Deprecated('Please use `headers` instead.')
2016-04-18 03:27:23 +00:00
header(String key, [String value]) {
2016-10-22 20:41:36 +00:00
if (value == null)
2016-12-19 01:38:23 +00:00
return headers[key];
2016-10-22 20:41:36 +00:00
else
2016-12-19 01:38:23 +00:00
headers[key] = value;
2016-04-18 03:27:23 +00:00
}
/// Serializes JSON to the response.
2016-10-22 20:41:36 +00:00
void json(value) {
2016-04-18 03:27:23 +00:00
write(god.serialize(value));
2016-12-19 01:38:23 +00:00
headers[HttpHeaders.CONTENT_TYPE] = ContentType.JSON.toString();
2016-04-18 03:27:23 +00:00
end();
}
/// Returns a JSONP response.
2016-10-22 20:41:36 +00:00
void jsonp(value, {String callbackName: "callback"}) {
2016-04-18 03:27:23 +00:00
write("$callbackName(${god.serialize(value)})");
2016-12-19 01:38:23 +00:00
headers[HttpHeaders.CONTENT_TYPE] = "application/javascript";
2016-04-18 03:27:23 +00:00
end();
}
/// Renders a view to the response stream, and closes the response.
2016-04-22 02:40:37 +00:00
Future render(String view, [Map data]) async {
write(await app.viewGenerator(view, data));
2016-12-19 01:38:23 +00:00
headers[HttpHeaders.CONTENT_TYPE] = ContentType.HTML.toString();
2016-04-18 03:27:23 +00:00
end();
}
/// Redirects to user to the given URL.
2016-11-28 00:49:27 +00:00
///
/// [url] can be a `String`, or a `List`.
/// If it is a `List`, a URI will be constructed
/// based on the provided params.
///
/// See [Router]#navigate for more. :)
2016-12-23 01:49:30 +00:00
void redirect(url, {bool absolute: true, int code: 302}) {
2016-12-19 01:38:23 +00:00
headers[HttpHeaders.LOCATION] =
url is String ? url : app.navigate(url, absolute: absolute);
2016-12-23 01:49:30 +00:00
statusCode = code ?? 302;
2016-04-18 03:27:23 +00:00
write('''
<!DOCTYPE html>
<html>
<head>
<title>Redirecting...</title>
<meta http-equiv="refresh" content="0; url=$url">
</head>
<body>
<h1>Currently redirecting you...</h1>
<br />
Click <a href="$url">here</a> if you are not automatically redirected...
2016-04-18 03:27:23 +00:00
<script>
window.location = "$url";
</script>
</body>
</html>
''');
end();
}
/// Redirects to the given named [Route].
2016-10-22 20:41:36 +00:00
void redirectTo(String name, [Map params, int code]) {
2016-11-28 00:49:27 +00:00
Route _findRoute(Router r) {
for (Route route in r.routes) {
if (route is SymlinkRoute) {
final m = _findRoute(route.router);
2016-11-23 09:10:47 +00:00
2016-11-28 00:49:27 +00:00
if (m != null) return m;
} else if (route.name == name) return route;
2016-11-23 09:10:47 +00:00
}
2016-11-28 00:49:27 +00:00
return null;
2016-11-23 09:10:47 +00:00
}
2016-11-28 00:49:27 +00:00
Route matched = _findRoute(app);
2016-11-23 09:10:47 +00:00
if (matched != null) {
2016-10-22 20:41:36 +00:00
redirect(matched.makeUri(params), code: code);
return;
}
throw new ArgumentError.notNull('Route to redirect to ($name)');
}
2016-06-27 00:20:42 +00:00
/// Redirects to the given [Controller] action.
2016-10-22 20:41:36 +00:00
void redirectToAction(String action, [Map params, int code]) {
2016-06-27 00:20:42 +00:00
// UserController@show
List<String> split = action.split("@");
if (split.length < 2)
2016-10-22 20:41:36 +00:00
throw new Exception(
"Controller redirects must take the form of 'Controller@action'. You gave: $action");
2016-06-27 00:20:42 +00:00
2016-11-28 00:49:27 +00:00
Controller controller =
app.controller(split[0].replaceAll(_straySlashes, ''));
2016-06-27 00:20:42 +00:00
if (controller == null)
throw new Exception("Could not find a controller named '${split[0]}'");
Route matched = controller.routeMappings[split[1]];
2016-06-27 00:20:42 +00:00
if (matched == null)
2016-10-22 20:41:36 +00:00
throw new Exception(
"Controller '${split[0]}' does not contain any action named '${split[1]}'");
2016-06-27 00:20:42 +00:00
2016-11-28 00:49:27 +00:00
final head =
controller.exposeDecl.path.toString().replaceAll(_straySlashes, '');
final tail = matched.makeUri(params).replaceAll(_straySlashes, '');
redirect('$head/$tail'.replaceAll(_straySlashes, ''), code: code);
2016-06-27 00:20:42 +00:00
}
2016-12-21 18:18:26 +00:00
/// Copies a file's contents into the response buffer.
Future sendFile(File file,
2016-04-18 03:27:23 +00:00
{int chunkSize, int sleepMs: 0, bool resumable: true}) async {
if (!isOpen) return;
2016-12-19 01:38:23 +00:00
headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path);
buffer.add(await file.readAsBytes());
2016-12-21 18:18:26 +00:00
end();
}
/// Streams a file to this response.
///
/// You can optionally transform the file stream with a [codec].
Future streamFile(File file,
{int chunkSize,
int sleepMs: 0,
bool resumable: true,
Codec<List<int>, List<int>> codec}) async {
if (!isOpen) return;
headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path);
end();
willCloseItself = true;
var stream = codec != null
? file.openRead().transform(codec.encoder)
: file.openRead();
await stream.pipe(io);
2016-04-18 03:27:23 +00:00
}
/// Writes data to the response.
2016-10-22 20:41:36 +00:00
void write(value, {Encoding encoding: UTF8}) {
if (isOpen) {
if (value is List<int>)
buffer.add(value);
2016-11-23 09:10:47 +00:00
else
buffer.add(encoding.encode(value.toString()));
2016-10-22 20:41:36 +00:00
}
2016-04-18 03:27:23 +00:00
}
2016-10-22 20:41:36 +00:00
}