Browse Source

Add a charset option to cowboy_static

Loïc Hoguin 6 years ago
parent
commit
571719a164
3 changed files with 87 additions and 3 deletions
  1. 19 1
      doc/src/manual/cowboy_static.asciidoc
  2. 18 0
      src/cowboy_static.erl
  3. 50 2
      test/static_handler_SUITE.erl

+ 19 - 1
doc/src/manual/cowboy_static.asciidoc

@@ -27,7 +27,10 @@ opts() :: {priv_file, App, Path}
 
 App        :: atom()
 Path       :: binary() | string()
-Extra      :: [Etag | Mimetypes]
+Extra      :: [Charset | Etag | Mimetypes]
+
+Charset    :: {charset, module(), function()}
+            | {charset, binary()}
 
 Etag       :: {etag, module(), function()}
             | {etag, false}
@@ -72,6 +75,20 @@ current directory.
 The extra options allow you to define how the etag should be
 calculated and how the MIME type of files should be detected.
 
+By default the static handler will not send a charset with
+the response. You can provide a specific charset that will
+be used for all files using the text media type, or provide
+a module and function that will be called when needed:
+
+[source,erlang]
+----
+detect_charset(Path :: binary()) -> Charset :: binary()
+----
+
+A charset must always be returned even if it doesn't make
+sense considering the media type of the file. A good default
+is `<<"utf-8">>`.
+
 By default the static handler will generate an etag based
 on the size and modification time of the file. You may disable
 the etag entirely with `{etag, false}` or provide a module
@@ -112,6 +129,7 @@ when it fails to detect a file's MIME type.
 
 == Changelog
 
+* *2.6*: The `charset` extra option was added.
 * *1.0*: Handler introduced.
 
 == Examples

+ 18 - 0
src/cowboy_static.erl

@@ -19,11 +19,13 @@
 -export([malformed_request/2]).
 -export([forbidden/2]).
 -export([content_types_provided/2]).
+-export([charsets_provided/2]).
 -export([resource_exists/2]).
 -export([last_modified/2]).
 -export([generate_etag/2]).
 -export([get_file/2]).
 
+-type extra_charset() :: {charset, module(), function()} | {charset, binary()}.
 -type extra_etag() :: {etag, module(), function()} | {etag, false}.
 -type extra_mimetypes() :: {mimetypes, module(), function()}
 	| {mimetypes, binary() | {binary(), binary(), [{binary(), binary()}]}}.
@@ -322,6 +324,22 @@ content_types_provided(Req, State={Path, _, Extra}) ->
 			{[{Type, get_file}], Req, State}
 	end.
 
+%% Detect the charset of the file.
+
+-spec charsets_provided(Req, State)
+	-> {[binary()], Req, State}
+	when State::state().
+charsets_provided(Req, State={Path, _, Extra}) ->
+	case lists:keyfind(charset, 1, Extra) of
+		%% We simulate the callback not being exported.
+		false ->
+			no_call;
+		{charset, Module, Function} ->
+			{[Module:Function(Path)], Req, State};
+		{charset, Charset} ->
+			{[Charset], Req, State}
+	end.
+
 %% Assume the resource doesn't exist if it's not a regular file.
 
 -spec resource_exists(Req, State)

+ 50 - 2
test/static_handler_SUITE.erl

@@ -132,6 +132,12 @@ init_dispatch(Config) ->
 			[{mimetypes, <<"application/vnd.ninenines.cowboy+xml;v=1">>}]}},
 		{"/mime/hardcode/tuple-form", cowboy_static, {priv_file, ct_helper, "static/file.cowboy",
 			[{mimetypes, {<<"application">>, <<"vnd.ninenines.cowboy+xml">>, [{<<"v">>, <<"1">>}]}}]}},
+		{"/charset/custom/[...]", cowboy_static, {priv_dir, ct_helper, "static",
+			[{charset, ?MODULE, do_charset_custom}]}},
+		{"/charset/crash/[...]", cowboy_static, {priv_dir, ct_helper, "static",
+			[{charset, ?MODULE, do_charset_crash}]}},
+		{"/charset/hardcode/[...]", cowboy_static, {priv_file, ct_helper, "static/index.html",
+			[{charset, <<"utf-8">>}]}},
 		{"/etag/custom", cowboy_static, {file, config(static_dir, Config) ++ "/style.css",
 			[{etag, ?MODULE, do_etag_custom}]}},
 		{"/etag/crash", cowboy_static, {file, config(static_dir, Config) ++ "/style.css",
@@ -151,6 +157,7 @@ init_dispatch(Config) ->
 		{"/bad/file/path", cowboy_static, {file, "/bad/path/style.css"}},
 		{"/bad/options", cowboy_static, {priv_file, ct_helper, "static/style.css", bad}},
 		{"/bad/options/mime", cowboy_static, {priv_file, ct_helper, "static/style.css", [{mimetypes, bad}]}},
+		{"/bad/options/charset", cowboy_static, {priv_file, ct_helper, "static/style.css", [{charset, bad}]}},
 		{"/bad/options/etag", cowboy_static, {priv_file, ct_helper, "static/style.css", [{etag, true}]}},
 		{"/unknown/option", cowboy_static, {priv_file, ct_helper, "static/style.css", [{bad, option}]}},
 		{"/char/[...]", cowboy_static, {dir, config(char_dir, Config)}},
@@ -162,6 +169,18 @@ init_dispatch(Config) ->
 
 %% Internal functions.
 
+-spec do_charset_crash(_) -> no_return().
+do_charset_crash(_) ->
+	ct_helper_error_h:ignore(?MODULE, do_charset_crash, 1),
+	exit(crash).
+
+do_charset_custom(Path) ->
+	case filename:extension(Path) of
+		<<".cowboy">> -> <<"utf-32">>;
+		<<".html">> -> <<"utf-16">>;
+		_ -> <<"utf-8">>
+	end.
+
 -spec do_etag_crash(_, _, _) -> no_return().
 do_etag_crash(_, _, _) ->
 	ct_helper_error_h:ignore(?MODULE, do_etag_crash, 3),
@@ -746,17 +765,46 @@ mime_custom_txt(Config) ->
 	ok.
 
 mime_hardcode_binary(Config) ->
-	doc("Get a .cowboy file with hardcoded route."),
+	doc("Get a .cowboy file with hardcoded route and media type in binary form."),
 	{200, Headers, _} = do_get("/mime/hardcode/binary-form", Config),
 	{_, <<"application/vnd.ninenines.cowboy+xml;v=1">>} = lists:keyfind(<<"content-type">>, 1, Headers),
 	ok.
 
 mime_hardcode_tuple(Config) ->
-	doc("Get a .cowboy file with hardcoded route."),
+	doc("Get a .cowboy file with hardcoded route and media type in tuple form."),
 	{200, Headers, _} = do_get("/mime/hardcode/tuple-form", Config),
 	{_, <<"application/vnd.ninenines.cowboy+xml;v=1">>} = lists:keyfind(<<"content-type">>, 1, Headers),
 	ok.
 
+charset_crash(Config) ->
+	doc("Get a file with a crashing charset function."),
+	{500, _, _} = do_get("/charset/crash/style.css", Config),
+	ok.
+
+charset_custom_cowboy(Config) ->
+	doc("Get a .cowboy file."),
+	{200, Headers, _} = do_get("/charset/custom/file.cowboy", Config),
+	{_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
+	ok.
+
+charset_custom_css(Config) ->
+	doc("Get a .css file."),
+	{200, Headers, _} = do_get("/charset/custom/style.css", Config),
+	{_, <<"text/css; charset=utf-8">>} = lists:keyfind(<<"content-type">>, 1, Headers),
+	ok.
+
+charset_custom_html(Config) ->
+	doc("Get a .html file."),
+	{200, Headers, _} = do_get("/charset/custom/index.html", Config),
+	{_, <<"text/html; charset=utf-16">>} = lists:keyfind(<<"content-type">>, 1, Headers),
+	ok.
+
+charset_hardcode_binary(Config) ->
+	doc("Get a .html file with hardcoded route and charset."),
+	{200, Headers, _} = do_get("/charset/hardcode", Config),
+	{_, <<"text/html; charset=utf-8">>} = lists:keyfind(<<"content-type">>, 1, Headers),
+	ok.
+
 priv_dir_in_ez_archive(Config) ->
 	doc("Get a file from a priv_dir stored in Erlang application .ez archive."),
 	{200, Headers, <<"<h1>It works!</h1>\n">>} = do_get("/ez_priv_dir/index.html", Config),