From 571719a164326eebdc792b43170fe27f123aac0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Fri, 2 Nov 2018 15:31:54 +0100 Subject: Add a charset option to cowboy_static --- doc/src/manual/cowboy_static.asciidoc | 20 +++++++++++++- src/cowboy_static.erl | 18 ++++++++++++ test/static_handler_SUITE.erl | 52 +++++++++++++++++++++++++++++++++-- 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/doc/src/manual/cowboy_static.asciidoc b/doc/src/manual/cowboy_static.asciidoc index a0069da..0e131dd 100644 --- a/doc/src/manual/cowboy_static.asciidoc +++ b/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 diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl index ae4e7c0..67cc08f 100644 --- a/src/cowboy_static.erl +++ b/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) diff --git a/test/static_handler_SUITE.erl b/test/static_handler_SUITE.erl index 9b7a1dc..55c3d2a 100644 --- a/test/static_handler_SUITE.erl +++ b/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, <<"

It works!

\n">>} = do_get("/ez_priv_dir/index.html", Config), -- cgit v1.2.3