/deps/webmachine/src/webmachine_util.erl

https://code.google.com/p/zotonic/ · Erlang · 303 lines · 242 code · 22 blank · 39 comment · 12 complexity · 89eb7016d5b0efae8fa4fb111fb77c26 MD5 · raw file

  1. %% @author Justin Sheehy <justin@basho.com>
  2. %% @author Andy Gross <andy@basho.com>
  3. %% @copyright 2007-2008 Basho Technologies
  4. %% (guess_mime/1 derived from code copyright 2007 Mochi Media, Inc.)
  5. %%
  6. %% Licensed under the Apache License, Version 2.0 (the "License");
  7. %% you may not use this file except in compliance with the License.
  8. %% You may obtain a copy of the License at
  9. %%
  10. %% http://www.apache.org/licenses/LICENSE-2.0
  11. %%
  12. %% Unless required by applicable law or agreed to in writing, software
  13. %% distributed under the License is distributed on an "AS IS" BASIS,
  14. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. %% See the License for the specific language governing permissions and
  16. %% limitations under the License.
  17. %% @doc Utilities for parsing, quoting, and negotiation.
  18. -module(webmachine_util).
  19. -export([guess_mime/1]).
  20. -export([convert_request_date/1, compare_ims_dates/2]).
  21. -export([choose_media_type/2]).
  22. -export([choose_charset/2]).
  23. -export([choose_encoding/2]).
  24. -export([unquote_header/1]).
  25. -export([now_diff_milliseconds/2]).
  26. -export([media_type_to_detail/1]).
  27. -export([test/0]).
  28. convert_request_date(Date) ->
  29. try
  30. case httpd_util:convert_request_date(Date) of
  31. ReqDate -> ReqDate
  32. end
  33. catch
  34. error:_ -> bad_date
  35. end.
  36. %% returns true if D1 > D2
  37. compare_ims_dates(D1, D2) ->
  38. GD1 = calendar:datetime_to_gregorian_seconds(D1),
  39. GD2 = calendar:datetime_to_gregorian_seconds(D2),
  40. GD1 > GD2.
  41. %% @spec guess_mime(string()) -> string()
  42. %% @doc Guess the mime type of a file by the extension of its filename.
  43. guess_mime(File) ->
  44. case filename:extension(File) of
  45. ".html" ->
  46. "text/html";
  47. ".xhtml" ->
  48. "application/xhtml+xml";
  49. ".xml" ->
  50. "application/xml";
  51. ".css" ->
  52. "text/css";
  53. ".js" ->
  54. "application/x-javascript";
  55. ".jpg" ->
  56. "image/jpeg";
  57. ".jpeg" ->
  58. "image/jpeg";
  59. ".gif" ->
  60. "image/gif";
  61. ".png" ->
  62. "image/png";
  63. ".ico" ->
  64. "image/x-icon";
  65. ".swf" ->
  66. "application/x-shockwave-flash";
  67. ".zip" ->
  68. "application/zip";
  69. ".bz2" ->
  70. "application/x-bzip2";
  71. ".gz" ->
  72. "application/x-gzip";
  73. ".tar" ->
  74. "application/x-tar";
  75. ".tgz" ->
  76. "application/x-gzip";
  77. ".htc" ->
  78. "text/x-component";
  79. ".manifest" ->
  80. "text/cache-manifest";
  81. ".svg" ->
  82. "image/svg+xml";
  83. _ ->
  84. "text/plain"
  85. end.
  86. choose_media_type(Provided,AcceptHead) ->
  87. % Return the Content-Type we will serve for a request.
  88. % If there is no acceptable/available match, return the atom "none".
  89. % AcceptHead is the value of the request's Accept header
  90. % Provided is a list of media types the resource can provide.
  91. % each is either a string e.g. -- "text/html"
  92. % or a string and parameters e.g. -- {"text/html",[{level,1}]}
  93. % (the plain string case with no parameters is much more common)
  94. Requested = accept_header_to_media_types(AcceptHead),
  95. Prov1 = normalize_provided(Provided),
  96. choose_media_type1(Prov1,Requested).
  97. choose_media_type1(_Provided,[]) ->
  98. none;
  99. choose_media_type1(Provided,[H|T]) ->
  100. {_Pri,Type,Params} = H,
  101. case media_match({Type,Params}, Provided) of
  102. [] -> choose_media_type1(Provided,T);
  103. [{CT_T,CT_P}|_] -> format_content_type(CT_T,CT_P)
  104. end.
  105. media_match(_,[]) -> [];
  106. media_match({"*/*",[]},[H|_]) -> [H];
  107. media_match({Type,Params},Provided) ->
  108. [{T1,P1} || {T1,P1} <- Provided,
  109. media_type_match(Type,T1), media_params_match(Params,P1)].
  110. media_type_match(Req,Prov) ->
  111. case Req of
  112. "*" -> % might as well not break for lame (Gomez) clients
  113. true;
  114. "*/*" ->
  115. true;
  116. Prov ->
  117. true;
  118. _ ->
  119. [R1|R2] = string:tokens(Req,"/"),
  120. [P1,_P2] = string:tokens(Prov,"/"),
  121. case R2 of
  122. ["*"] ->
  123. case R1 of
  124. P1 -> true;
  125. _ -> false
  126. end;
  127. _ -> false
  128. end
  129. end.
  130. media_params_match(Req,Prov) ->
  131. lists:sort(Req) =:= lists:sort(Prov).
  132. prioritize_media(TyParam) ->
  133. {Type, Params} = TyParam,
  134. prioritize_media(Type,Params,[]).
  135. prioritize_media(Type,Params,Acc) ->
  136. case Params of
  137. [] ->
  138. {1, Type, Acc};
  139. _ ->
  140. [{Tok,Val}|Rest] = Params,
  141. case Tok of
  142. "q" ->
  143. QVal = case Val of
  144. "1" ->
  145. 1;
  146. [$.|_] -> list_to_float([$0|Val]);
  147. _ -> list_to_float(Val)
  148. end,
  149. {QVal, Type, Rest ++ Acc};
  150. _ ->
  151. prioritize_media(Type,Rest,[{Tok,Val}|Acc])
  152. end
  153. end.
  154. media_type_to_detail(MType) ->
  155. [CType|Params] = string:tokens(MType, ";"),
  156. MParams = [list_to_tuple([string:strip(KV) || KV <- string:tokens(X,"=")])
  157. || X <- Params],
  158. {CType, MParams}.
  159. accept_header_to_media_types(HeadVal) ->
  160. % given the value of an accept header, produce an ordered list
  161. % based on the q-values. Results are [{Type,Params}] with the
  162. % head of the list being the highest-priority requested type.
  163. try
  164. lists:reverse(lists:keysort(1,
  165. [prioritize_media(media_type_to_detail(MType)) ||
  166. MType <- [string:strip(X) || X <- string:tokens(HeadVal, ",")]]))
  167. catch _:_ -> []
  168. end.
  169. normalize_provided(Provided) ->
  170. [normalize_provided1(X) || X <- Provided].
  171. normalize_provided1(Type) when is_list(Type) -> {Type, []};
  172. normalize_provided1({Type,Params}) -> {Type, Params}.
  173. format_content_type(Type,[]) -> Type;
  174. format_content_type(Type,[H|T]) -> format_content_type(Type ++ "; " ++ H, T).
  175. choose_charset(CSets, AccCharHdr) -> do_choose(CSets, AccCharHdr, "ISO-8859-1").
  176. choose_encoding(Encs, AccEncHdr) -> do_choose(Encs, AccEncHdr, "identity").
  177. do_choose(Choices, Header, Default) ->
  178. Accepted = build_conneg_list(string:tokens(Header, ",")),
  179. DefaultPrio = [P || {P,C} <- Accepted, C =:= Default],
  180. StarPrio = [P || {P,C} <- Accepted, C =:= "*"],
  181. DefaultOkay = case DefaultPrio of
  182. [] ->
  183. case StarPrio of
  184. [0.0] -> no;
  185. _ -> yes
  186. end;
  187. [0.0] -> no;
  188. _ -> yes
  189. end,
  190. AnyOkay = case StarPrio of
  191. [] -> no;
  192. [0.0] -> no;
  193. _ -> yes
  194. end,
  195. do_choose(Default, DefaultOkay, AnyOkay, Choices, Accepted).
  196. do_choose(_Default, _DefaultOkay, _AnyOkay, [], []) ->
  197. none;
  198. do_choose(_Default, _DefaultOkay, _AnyOkay, [], _Accepted) ->
  199. none;
  200. do_choose(Default, DefaultOkay, AnyOkay, Choices, []) ->
  201. case AnyOkay of
  202. yes -> hd(Choices);
  203. no ->
  204. case DefaultOkay of
  205. yes ->
  206. case lists:member(Default, Choices) of
  207. true -> Default;
  208. _ -> none
  209. end;
  210. no -> none
  211. end
  212. end;
  213. do_choose(Default, DefaultOkay, AnyOkay, Choices, [AccPair|AccRest]) ->
  214. {Prio, Acc} = AccPair,
  215. case Prio of
  216. 0.0 ->
  217. do_choose(Default, DefaultOkay, AnyOkay,
  218. lists:delete(Acc, Choices), AccRest);
  219. _ ->
  220. LAcc = string:to_lower(Acc),
  221. LChoices = [string:to_lower(X) || X <- Choices],
  222. % doing this a little more work than needed in
  223. % order to be easily insensitive but preserving
  224. case lists:member(LAcc, LChoices) of
  225. true ->
  226. hd([X || X <- Choices,
  227. string:to_lower(X) =:= LAcc]);
  228. false -> do_choose(Default, DefaultOkay, AnyOkay,
  229. Choices, AccRest)
  230. end
  231. end.
  232. build_conneg_list(AccList) ->
  233. build_conneg_list(AccList, []).
  234. build_conneg_list([], Result) -> lists:reverse(lists:sort(Result));
  235. build_conneg_list([Acc|AccRest], Result) ->
  236. XPair = list_to_tuple([string:strip(X) || X <- string:tokens(Acc, ";")]),
  237. Pair = case XPair of
  238. {Choice, "q=" ++ PrioStr} ->
  239. case PrioStr of
  240. "0" -> {0.0, Choice};
  241. "1" -> {1.0, Choice};
  242. [$.|_] -> {list_to_float([$0|PrioStr]), Choice};
  243. _ -> {list_to_float(PrioStr), Choice}
  244. end;
  245. {Choice} ->
  246. {1.0, Choice}
  247. end,
  248. build_conneg_list(AccRest,[Pair|Result]).
  249. % (unquote_header copied from mochiweb_util since they don't export it)
  250. unquote_header("\"" ++ Rest) ->
  251. unquote_header(Rest, []);
  252. unquote_header(S) ->
  253. S.
  254. unquote_header("", Acc) ->
  255. lists:reverse(Acc);
  256. unquote_header("\"", Acc) ->
  257. lists:reverse(Acc);
  258. unquote_header([$\\, C | Rest], Acc) ->
  259. unquote_header(Rest, [C | Acc]);
  260. unquote_header([C | Rest], Acc) ->
  261. unquote_header(Rest, [C | Acc]).
  262. %% @type now() = {MegaSecs, Secs, MicroSecs}
  263. %% This is faster than timer:now_diff() because it does not use bignums.
  264. %% But it returns *milliseconds* (timer:now_diff returns microseconds.)
  265. %% From http://www.erlang.org/ml-archive/erlang-questions/200205/msg00027.html
  266. %% @doc Compute the difference between two now() tuples, in milliseconds.
  267. %% @spec now_diff_milliseconds(now(), now()) -> integer()
  268. now_diff_milliseconds({M,S,U}, {M,S1,U1}) ->
  269. ((S-S1) * 1000) + ((U-U1) div 1000);
  270. now_diff_milliseconds({M,S,U}, {M1,S1,U1}) ->
  271. ((M-M1)*1000000+(S-S1))*1000 + ((U-U1) div 1000).
  272. test() ->
  273. test_choose_media_type(),
  274. ok.
  275. test_choose_media_type() ->
  276. Provided = "text/html",
  277. ShouldMatch = ["*", "*/*", "text/*", "text/html"],
  278. WantNone = ["foo", "text/xml", "application/*", "foo/bar/baz"],
  279. [ Provided = choose_media_type([Provided], I) || I <- ShouldMatch ],
  280. [ none = choose_media_type([Provided], I) || I <- WantNone ],
  281. ok.