2016-09-15 19:53:01 +00:00
|
|
|
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';
|
2016-09-15 19:53:01 +00:00
|
|
|
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-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.
|
2016-04-21 20:37:02 +00:00
|
|
|
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.
|
2016-09-15 19:53:01 +00:00
|
|
|
AngelBase app;
|
2016-04-18 03:27:23 +00:00
|
|
|
|
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`.');
|
|
|
|
}
|
|
|
|
|
|
|
|
ResponseContext(this.io, this.app);
|
2016-04-18 03:27:23 +00:00
|
|
|
|
|
|
|
/// Set this to true if you will manually close the response.
|
|
|
|
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. :)
|
|
|
|
void redirect(url, {bool absolute: true, int code: 301}) {
|
2016-12-19 01:38:23 +00:00
|
|
|
headers[HttpHeaders.LOCATION] =
|
|
|
|
url is String ? url : app.navigate(url, absolute: absolute);
|
|
|
|
statusCode = code ?? 301;
|
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 />
|
2016-07-04 18:06:31 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2016-05-02 22:28:14 +00:00
|
|
|
/// 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
|
|
|
|
2016-05-02 22:28:14 +00:00
|
|
|
if (matched != null) {
|
2016-10-22 20:41:36 +00:00
|
|
|
redirect(matched.makeUri(params), code: code);
|
|
|
|
return;
|
2016-05-02 22:28:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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]}'");
|
|
|
|
|
2016-09-15 19:53:01 +00:00
|
|
|
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-04-18 03:27:23 +00:00
|
|
|
/// Streams a file to this response as chunked data.
|
2016-10-22 20:41:36 +00:00
|
|
|
Future streamFile(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);
|
|
|
|
end();
|
|
|
|
buffer.add(await file.readAsBytes());
|
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
|
|
|
}
|