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()
 App        :: atom()
 Path       :: binary() | string()
 Path       :: binary() | string()
-Extra      :: [Etag | Mimetypes]
+Extra      :: [Charset | Etag | Mimetypes]
+
+Charset    :: {charset, module(), function()}
+            | {charset, binary()}
 
 
 Etag       :: {etag, module(), function()}
 Etag       :: {etag, module(), function()}
             | {etag, false}
             | {etag, false}
@@ -72,6 +75,20 @@ current directory.
 The extra options allow you to define how the etag should be
 The extra options allow you to define how the etag should be
 calculated and how the MIME type of files should be detected.
 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
 By default the static handler will generate an etag based
 on the size and modification time of the file. You may disable
 on the size and modification time of the file. You may disable
 the etag entirely with `{etag, false}` or provide a module
 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
 == Changelog
 
 
+* *2.6*: The `charset` extra option was added.
 * *1.0*: Handler introduced.
 * *1.0*: Handler introduced.
 
 
 == Examples
 == Examples

+ 18 - 0
src/cowboy_static.erl

@@ -19,11 +19,13 @@
 -export([malformed_request/2]).
 -export([malformed_request/2]).
 -export([forbidden/2]).
 -export([forbidden/2]).
 -export([content_types_provided/2]).
 -export([content_types_provided/2]).
+-export([charsets_provided/2]).
 -export([resource_exists/2]).
 -export([resource_exists/2]).
 -export([last_modified/2]).
 -export([last_modified/2]).
 -export([generate_etag/2]).
 -export([generate_etag/2]).
 -export([get_file/2]).
 -export([get_file/2]).
 
 
+-type extra_charset() :: {charset, module(), function()} | {charset, binary()}.
 -type extra_etag() :: {etag, module(), function()} | {etag, false}.
 -type extra_etag() :: {etag, module(), function()} | {etag, false}.
 -type extra_mimetypes() :: {mimetypes, module(), function()}
 -type extra_mimetypes() :: {mimetypes, module(), function()}
 	| {mimetypes, binary() | {binary(), binary(), [{binary(), binary()}]}}.
 	| {mimetypes, binary() | {binary(), binary(), [{binary(), binary()}]}}.
@@ -322,6 +324,22 @@ content_types_provided(Req, State={Path, _, Extra}) ->
 			{[{Type, get_file}], Req, State}
 			{[{Type, get_file}], Req, State}
 	end.
 	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.
 %% Assume the resource doesn't exist if it's not a regular file.
 
 
 -spec resource_exists(Req, State)
 -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">>}]}},
 			[{mimetypes, <<"application/vnd.ninenines.cowboy+xml;v=1">>}]}},
 		{"/mime/hardcode/tuple-form", cowboy_static, {priv_file, ct_helper, "static/file.cowboy",
 		{"/mime/hardcode/tuple-form", cowboy_static, {priv_file, ct_helper, "static/file.cowboy",
 			[{mimetypes, {<<"application">>, <<"vnd.ninenines.cowboy+xml">>, [{<<"v">>, <<"1">>}]}}]}},
 			[{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/custom", cowboy_static, {file, config(static_dir, Config) ++ "/style.css",
 			[{etag, ?MODULE, do_etag_custom}]}},
 			[{etag, ?MODULE, do_etag_custom}]}},
 		{"/etag/crash", cowboy_static, {file, config(static_dir, Config) ++ "/style.css",
 		{"/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/file/path", cowboy_static, {file, "/bad/path/style.css"}},
 		{"/bad/options", cowboy_static, {priv_file, ct_helper, "static/style.css", bad}},
 		{"/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/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}]}},
 		{"/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}]}},
 		{"/unknown/option", cowboy_static, {priv_file, ct_helper, "static/style.css", [{bad, option}]}},
 		{"/char/[...]", cowboy_static, {dir, config(char_dir, Config)}},
 		{"/char/[...]", cowboy_static, {dir, config(char_dir, Config)}},
@@ -162,6 +169,18 @@ init_dispatch(Config) ->
 
 
 %% Internal functions.
 %% 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().
 -spec do_etag_crash(_, _, _) -> no_return().
 do_etag_crash(_, _, _) ->
 do_etag_crash(_, _, _) ->
 	ct_helper_error_h:ignore(?MODULE, do_etag_crash, 3),
 	ct_helper_error_h:ignore(?MODULE, do_etag_crash, 3),
@@ -746,17 +765,46 @@ mime_custom_txt(Config) ->
 	ok.
 	ok.
 
 
 mime_hardcode_binary(Config) ->
 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),
 	{200, Headers, _} = do_get("/mime/hardcode/binary-form", Config),
 	{_, <<"application/vnd.ninenines.cowboy+xml;v=1">>} = lists:keyfind(<<"content-type">>, 1, Headers),
 	{_, <<"application/vnd.ninenines.cowboy+xml;v=1">>} = lists:keyfind(<<"content-type">>, 1, Headers),
 	ok.
 	ok.
 
 
 mime_hardcode_tuple(Config) ->
 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),
 	{200, Headers, _} = do_get("/mime/hardcode/tuple-form", Config),
 	{_, <<"application/vnd.ninenines.cowboy+xml;v=1">>} = lists:keyfind(<<"content-type">>, 1, Headers),
 	{_, <<"application/vnd.ninenines.cowboy+xml;v=1">>} = lists:keyfind(<<"content-type">>, 1, Headers),
 	ok.
 	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) ->
 priv_dir_in_ez_archive(Config) ->
 	doc("Get a file from a priv_dir stored in Erlang application .ez archive."),
 	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),
 	{200, Headers, <<"<h1>It works!</h1>\n">>} = do_get("/ez_priv_dir/index.html", Config),