/src/support/z_context.erl

https://code.google.com/p/zotonic/ · Erlang · 911 lines · 593 code · 158 blank · 160 comment · 16 complexity · 9a906247f36b2b403a46b162569609d3 MD5 · raw file

  1. %% @author Marc Worrell <marc@worrell.nl>
  2. %% @copyright 2009 Marc Worrell
  3. %% @doc Request context for zophenic request evaluation.
  4. %% Copyright 2009 Marc Worrell
  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. -module(z_context).
  18. -author("Marc Worrell <marc@worrell.nl>").
  19. -export([
  20. new/1,
  21. new/2,
  22. new_tests/0,
  23. site/1,
  24. hostname/1,
  25. hostname_port/1,
  26. is_request/1,
  27. prune_for_async/1,
  28. prune_for_template/1,
  29. prune_for_database/1,
  30. prune_for_scomp/2,
  31. output/2,
  32. abs_url/2,
  33. pickle/1,
  34. depickle/1,
  35. combine_results/2,
  36. continue_session/1,
  37. has_session/1,
  38. ensure_all/1,
  39. ensure_session/1,
  40. ensure_page_session/1,
  41. ensure_qs/1,
  42. get_reqdata/1,
  43. set_reqdata/2,
  44. get_resource_module/1,
  45. set_resource_module/2,
  46. get_q/2,
  47. get_q/3,
  48. get_q_all/1,
  49. get_q_all/2,
  50. get_q_all_noz/1,
  51. get_q_validated/2,
  52. add_script_session/1,
  53. add_script_page/1,
  54. add_script_session/2,
  55. add_script_page/2,
  56. spawn_link_session/4,
  57. spawn_link_page/4,
  58. get_value/2,
  59. set_session/3,
  60. get_session/2,
  61. incr_session/3,
  62. set_page/3,
  63. get_page/2,
  64. incr_page/3,
  65. persistent_id/1,
  66. set_persistent/3,
  67. get_persistent/2,
  68. set/3,
  69. set/2,
  70. get/2,
  71. get/3,
  72. incr/3,
  73. get_all/1,
  74. language/1,
  75. set_language/2,
  76. merge_scripts/2,
  77. copy_scripts/2,
  78. clean_scripts/1,
  79. set_resp_header/3,
  80. get_resp_header/2,
  81. get_req_header/2,
  82. get_req_path/1,
  83. cookie_domain/1,
  84. document_domain/1,
  85. streamhost/1
  86. ]).
  87. -include_lib("zotonic.hrl").
  88. %% @doc Return a new empty context, no request is initialized.
  89. %% @spec new(HostDescr) -> Context2
  90. %% HostDescr = Context | atom() | ReqData
  91. new(#context{} = C) ->
  92. #context{
  93. host=C#context.host,
  94. language=C#context.language,
  95. depcache=C#context.depcache,
  96. notifier=C#context.notifier,
  97. session_manager=C#context.session_manager,
  98. dispatcher=C#context.dispatcher,
  99. template_server=C#context.template_server,
  100. scomp_server=C#context.scomp_server,
  101. dropbox_server=C#context.dropbox_server,
  102. pivot_server=C#context.pivot_server,
  103. module_indexer=C#context.module_indexer,
  104. translation_table=C#context.translation_table
  105. };
  106. new(undefined) ->
  107. case z_sites_dispatcher:get_fallback_site() of
  108. undefined -> throw({error, no_site_enabled});
  109. Site -> new(Site)
  110. end;
  111. new(Host) when is_atom(Host) ->
  112. Context = set_server_names(#context{host=Host}),
  113. Context#context{language=z_trans:default_language(Context)};
  114. new(ReqData) ->
  115. %% This is the requesting thread, enable simple memo functionality.
  116. z_memo:enable(),
  117. z_depcache:in_process(true),
  118. Context = set_server_names(#context{wm_reqdata=ReqData, host=site(ReqData)}),
  119. set_dispatch_from_path(Context#context{language=z_trans:default_language(Context)}).
  120. %% @doc Create a new context record for a host with a certain language.
  121. new(Host, Lang) when is_atom(Host), is_atom(Lang) ->
  122. Context = set_server_names(#context{host=Host}),
  123. Context#context{language=Lang};
  124. %% @doc Create a new context record for the current request and resource module
  125. new(ReqData, Module) ->
  126. %% This is the requesting thread, enable simple memo functionality.
  127. z_memo:enable(),
  128. z_depcache:in_process(true),
  129. Context = set_server_names(#context{wm_reqdata=ReqData, resource_module=Module, host=site(ReqData)}),
  130. set_dispatch_from_path(Context#context{language=z_trans:default_language(Context)}).
  131. % @doc Create a new context used when testing parts of zotonic
  132. new_tests() ->
  133. z_trans_server:set_context_table(#context{host=test, language=en, notifier='z_notifier$test'}).
  134. %% @doc Set the dispatch rule for this request to the context var 'zotonic_dispatch'
  135. set_dispatch_from_path(Context) ->
  136. case dict:find(zotonic_dispatch, wrq:path_info(Context#context.wm_reqdata)) of
  137. {ok, Dispatch} -> set(zotonic_dispatch, Dispatch, Context);
  138. error -> Context
  139. end.
  140. %% @doc Set all server names for the given host.
  141. %% @spec set_server_names(Context1) -> Context2
  142. set_server_names(#context{host=Host} = Context) ->
  143. HostAsList = [$$ | atom_to_list(Host)],
  144. Context#context{
  145. depcache=list_to_atom("z_depcache"++HostAsList),
  146. notifier=list_to_atom("z_notifier"++HostAsList),
  147. session_manager=list_to_atom("z_session_manager"++HostAsList),
  148. dispatcher=list_to_atom("z_dispatcher"++HostAsList),
  149. template_server=list_to_atom("z_template"++HostAsList),
  150. scomp_server=list_to_atom("z_scomp"++HostAsList),
  151. dropbox_server=list_to_atom("z_dropbox"++HostAsList),
  152. pivot_server=list_to_atom("z_pivot_rsc"++HostAsList),
  153. module_indexer=list_to_atom("z_module_indexer"++HostAsList),
  154. translation_table=z_trans_server:table(Host)
  155. }.
  156. %% @doc Maps the host in the request to a site in the sites folder.
  157. %% @spec site(wm_reqdata) -> atom()
  158. site(#context{host=Host}) ->
  159. Host;
  160. %% @spec site(wm_reqdata) -> atom()
  161. site(ReqData = #wm_reqdata{}) ->
  162. PathInfo = wrq:path_info(ReqData),
  163. case dict:find(zotonic_host, PathInfo) of
  164. {ok, Host} -> Host;
  165. error -> z_sites_dispatcher:get_fallback_site()
  166. end.
  167. %% @doc Return the preferred hostname from the site configuration
  168. %% @spec hostname(Context) -> string()
  169. hostname(Context) ->
  170. case z_dispatcher:hostname(Context) of
  171. Empty when Empty == undefined; Empty == []; Empty == <<>> ->
  172. "localhost";
  173. Hostname ->
  174. Hostname
  175. end.
  176. %% @doc Return the preferred hostname, including port, from the site configuration
  177. %% @spec hostname_port(Context) -> string()
  178. hostname_port(Context) ->
  179. case z_dispatcher:hostname_port(Context) of
  180. Empty when Empty == undefined; Empty == [] ->
  181. "localhost";
  182. Hostname ->
  183. Hostname
  184. end.
  185. %% @doc Check if the current context is a request context
  186. is_request(#context{wm_reqdata=undefined}) -> false;
  187. is_request(_Context) -> true.
  188. %% @doc Make the context safe to use in a async message. This removes buffers and the db transaction.
  189. prune_for_async(#context{} = Context) ->
  190. #context{
  191. wm_reqdata=Context#context.wm_reqdata,
  192. host=Context#context.host,
  193. user_id=Context#context.user_id,
  194. session_pid=Context#context.session_pid,
  195. page_pid=Context#context.page_pid,
  196. acl=Context#context.acl,
  197. props=Context#context.props,
  198. depcache=Context#context.depcache,
  199. notifier=Context#context.notifier,
  200. session_manager=Context#context.session_manager,
  201. dispatcher=Context#context.dispatcher,
  202. template_server=Context#context.template_server,
  203. scomp_server=Context#context.scomp_server,
  204. dropbox_server=Context#context.dropbox_server,
  205. pivot_server=Context#context.pivot_server,
  206. module_indexer=Context#context.module_indexer,
  207. translation_table=Context#context.translation_table,
  208. language=Context#context.language
  209. }.
  210. %% @doc Cleanup a context for the output stream
  211. prune_for_template(#context{}=Context) ->
  212. #context{
  213. wm_reqdata=undefined,
  214. props=undefined,
  215. updates=Context#context.updates,
  216. actions=Context#context.actions,
  217. content_scripts=Context#context.content_scripts,
  218. scripts=Context#context.scripts,
  219. wire=Context#context.wire,
  220. validators=Context#context.validators,
  221. render=Context#context.render
  222. };
  223. prune_for_template(Output) -> Output.
  224. %% @doc Cleanup a context so that it can be used exclusively for database connections
  225. prune_for_database(Context) ->
  226. #context{
  227. host=Context#context.host,
  228. dbc=Context#context.dbc,
  229. depcache=Context#context.depcache,
  230. notifier=Context#context.notifier,
  231. session_manager=Context#context.session_manager,
  232. dispatcher=Context#context.dispatcher,
  233. template_server=Context#context.template_server,
  234. scomp_server=Context#context.scomp_server,
  235. dropbox_server=Context#context.dropbox_server,
  236. pivot_server=Context#context.pivot_server,
  237. module_indexer=Context#context.module_indexer
  238. }.
  239. %% @doc Cleanup a context for cacheable scomp handling. Resets most of the accumulators to prevent duplicating
  240. %% between different (cached) renderings.
  241. prune_for_scomp(VisibleFor, Context) ->
  242. z_acl:set_visible_for(VisibleFor, Context#context{
  243. dbc=undefined,
  244. wm_reqdata=undefined,
  245. updates=[],
  246. actions=[],
  247. content_scripts=[],
  248. scripts=[],
  249. wire=[],
  250. validators=[],
  251. render=[]
  252. }).
  253. %% @doc Make the url an absolute url by prepending the hostname.
  254. %% @spec abs_url(string(), Context) -> string()
  255. abs_url(Url, Context) when is_binary(Url) ->
  256. abs_url(binary_to_list(Url), Context);
  257. abs_url(Url, Context) ->
  258. case has_url_protocol(Url) of
  259. true ->
  260. Url;
  261. false ->
  262. ["http://", hostname_port(Context), Url]
  263. end.
  264. has_url_protocol([]) ->
  265. false;
  266. has_url_protocol([H|T]) when is_integer($a) andalso H >= $a andalso H =< $z ->
  267. has_url_protocol(T);
  268. has_url_protocol([$:|_]) ->
  269. true;
  270. has_url_protocol(_) ->
  271. false.
  272. %% @doc Pickle a context for storing in the database
  273. %% @todo pickle/depickle the visitor id (when any)
  274. %% @spec pickle(Context) -> tuple()
  275. pickle(Context) ->
  276. {pickled_context, Context#context.host, Context#context.user_id, Context#context.language, undefined}.
  277. %% @doc Depickle a context for restoring from a database
  278. %% @todo pickle/depickle the visitor id (when any)
  279. depickle({pickled_context, Host, UserId, Language, _VisitorId}) ->
  280. Context = set_server_names(#context{host=Host, language=Language}),
  281. case UserId of
  282. undefined -> Context;
  283. _ -> z_acl:logon(UserId, Context)
  284. end.
  285. %% @spec output(list(), Context) -> {io_list(), Context}
  286. %% @doc Replace the contexts in the output with their rendered content and collect all scripts
  287. output(<<>>, Context) ->
  288. {[], Context};
  289. output(B, Context) when is_binary(B) ->
  290. {B, Context};
  291. output(List, Context) ->
  292. output1(List, Context, []).
  293. %% @doc Recursively walk through the output, replacing all context placeholders with their rendered output
  294. output1(B, Context, Acc) when is_binary(B) ->
  295. {[lists:reverse(Acc),B], Context};
  296. output1([], Context, Acc) ->
  297. {lists:reverse(Acc), Context};
  298. output1([#context{}=C|Rest], Context, Acc) ->
  299. {Rendered, Context1} = output1(C#context.render, Context, []),
  300. output1(Rest, merge_scripts(C, Context1), [Rendered|Acc]);
  301. output1([{script, Args}|Rest], Context, Acc) ->
  302. output1(Rest, Context, [render_script(Args, Context)|Acc]);
  303. output1([List|Rest], Context, Acc) when is_list(List) ->
  304. {Rendered, Context1} = output1(List, Context, []),
  305. output1(Rest, Context1, [Rendered|Acc]);
  306. output1([undefined|Rest], Context, Acc) ->
  307. output1(Rest, Context, Acc);
  308. output1([C|Rest], Context, Acc) when is_atom(C) ->
  309. output1(Rest, Context, [list_to_binary(atom_to_list(C))|Acc]);
  310. output1([{trans, _} = Trans|Rest], Context, Acc) ->
  311. output1(Rest, Context, [z_trans:lookup_fallback(Trans, Context)|Acc]);
  312. output1([{{_,_,_},{_,_,_}} = D|Rest], Context, Acc) ->
  313. output1([filter_date:date(D, "Y-m-d H:i:s", Context)|Rest], Context, Acc);
  314. output1([T|Rest], Context, Acc) when is_tuple(T) ->
  315. output1([iolist_to_binary(io_lib:format("~p", [T]))|Rest], Context, Acc);
  316. output1([C|Rest], Context, Acc) ->
  317. output1(Rest, Context, [C|Acc]).
  318. render_script(Args, Context) ->
  319. NoStartup = z_convert:to_bool(proplists:get_value(nostartup, Args, false)),
  320. Extra = [ S || S <- z_notifier:map({scomp_script_render, NoStartup, Args}, Context), S /= undefined ],
  321. Script = case NoStartup of
  322. false ->
  323. [ z_script:get_page_startup_script(Context),
  324. Extra,
  325. z_script:get_script(Context) ];
  326. true ->
  327. [z_script:get_script(Context), Extra]
  328. end,
  329. case proplists:get_value(format, Args, "html") of
  330. "html" ->
  331. [ <<"\n\n<script type='text/javascript'>\n$(function() {\n">>, Script, <<"\n});\n</script>\n">> ];
  332. "escapejs" ->
  333. z_utils:js_escape(Script)
  334. end.
  335. %% @spec combine_results(Context1, Context2) -> Context
  336. %% @doc Merge the scripts and the rendered content of two contexts into Context1
  337. combine_results(C1, C2) ->
  338. Merged = merge_scripts(C2, C1),
  339. Merged#context{
  340. render=combine(C1#context.render, C2#context.render)
  341. }.
  342. %% @spec merge_scripts(Context, ContextAcc) -> Context
  343. %% @doc Merge the scripts from context C into the context accumulator, used when collecting all scripts in an output stream
  344. merge_scripts(C, Acc) ->
  345. Acc#context{
  346. updates=combine(Acc#context.updates, C#context.updates),
  347. actions=combine(Acc#context.actions, C#context.actions),
  348. content_scripts=combine(Acc#context.content_scripts, C#context.content_scripts),
  349. scripts=combine(Acc#context.scripts, C#context.scripts),
  350. wire=combine(Acc#context.wire, C#context.wire),
  351. validators=combine(Acc#context.validators, C#context.validators)
  352. }.
  353. combine([],X) -> X;
  354. combine(X,[]) -> X;
  355. combine(X,Y) -> [X++Y].
  356. %% @doc Remove all scripts from the context
  357. %% @spec clean_scripts(Context) -> Context
  358. clean_scripts(C) ->
  359. z_script:clean(C).
  360. %% @doc Overwrite the scripts in Context with the scripts in From
  361. %% @spec copy_scripts(From, Context) -> Context
  362. copy_scripts(From, Context) ->
  363. Context#context{
  364. updates=From#context.updates,
  365. actions=From#context.actions,
  366. content_scripts=From#context.content_scripts,
  367. scripts=From#context.scripts,
  368. wire=From#context.wire,
  369. validators=From#context.validators
  370. }.
  371. %% @doc Continue an existing session, if the session id is in the request.
  372. continue_session(Context) ->
  373. case Context#context.session_pid of
  374. undefined ->
  375. case z_session_manager:continue_session(Context) of
  376. {ok, Context1} ->
  377. Context2 = z_auth:logon_from_session(Context1),
  378. z_notifier:foldl(session_context, Context2, Context2);
  379. {error, _} ->
  380. Context
  381. end;
  382. _ ->
  383. Context
  384. end.
  385. %% @doc Check if the current context has a session attached
  386. has_session(#context{session_pid=SessionPid}) when is_pid(SessionPid) ->
  387. true;
  388. has_session(_) ->
  389. false.
  390. %% @doc Ensure session and page session and fetch and parse the query string
  391. ensure_all(Context) ->
  392. ensure_page_session(
  393. ensure_session(
  394. ensure_qs(Context))).
  395. %% @doc Ensure that we have a session, start a new session process when needed
  396. ensure_session(Context) ->
  397. case Context#context.session_pid of
  398. undefined ->
  399. Context1 = z_session_manager:ensure_session(Context),
  400. Context2 = z_auth:logon_from_session(Context1),
  401. Context3 = z_notifier:foldl(session_context, Context2, Context2),
  402. add_nocache_headers(Context3);
  403. _ ->
  404. Context
  405. end.
  406. %% @doc Ensure that we have a page session, used for comet and postback requests
  407. ensure_page_session(Context) ->
  408. case Context#context.page_pid of
  409. undefined ->
  410. Context1 = ensure_session(Context),
  411. z_session:ensure_page_session(Context1);
  412. _ ->
  413. Context
  414. end.
  415. %% @doc Ensure that we have parsed the query string, fetch body if necessary
  416. ensure_qs(Context) ->
  417. case proplists:lookup('q', Context#context.props) of
  418. {'q', _Qs} ->
  419. Context;
  420. none ->
  421. ReqData = Context#context.wm_reqdata,
  422. Query = wrq:req_qs(ReqData),
  423. PathDict = wrq:path_info(ReqData),
  424. PathArgs = lists:map(
  425. fun ({T,V}) when is_atom(V) -> {atom_to_list(T),atom_to_list(V)};
  426. ({T,V}) -> {atom_to_list(T),mochiweb_util:unquote(V)}
  427. end,
  428. dict:to_list(PathDict)),
  429. QPropsUrl = z_utils:prop_replace('q', PathArgs++Query, Context#context.props),
  430. {Body, ContextParsed} = parse_form_urlencoded(Context#context{props=QPropsUrl}),
  431. QPropsAll = z_utils:prop_replace('q', PathArgs++Body++Query, ContextParsed#context.props),
  432. ContextParsed#context{props=QPropsAll}
  433. end.
  434. %% @spec get_reqdata(Context) -> #wm_reqdata{}
  435. %% @doc Return the webmachine request data of the context
  436. get_reqdata(Context) ->
  437. Context#context.wm_reqdata.
  438. %% @spec set_reqdata(ReqData, Context) -> #wm_reqdata{}
  439. %% @doc Set the webmachine request data of the context
  440. set_reqdata(ReqData = #wm_reqdata{}, Context) ->
  441. Context#context{wm_reqdata=ReqData}.
  442. %% @spec get_resource_module(Context) -> term()
  443. %% @doc Get the resource module handling the request.
  444. get_resource_module(Context) ->
  445. Context#context.resource_module.
  446. %% @spec set_resource_module(Module::atom(), Context) -> NewContext
  447. set_resource_module(Module, Context) ->
  448. Context#context{resource_module=Module}.
  449. %% @spec get_q(Key::string(), Context) -> Value::string() | undefined
  450. %% @doc Get a request parameter, either from the query string or the post body. Post body has precedence over the query string.
  451. get_q([Key|_] = Keys, Context) when is_list(Key); is_atom(Key) ->
  452. lists:foldl(fun(K, Acc) ->
  453. case get_q(K, Context) of
  454. undefined -> Acc;
  455. Value -> [{z_convert:to_atom(K), Value}|Acc]
  456. end
  457. end,
  458. [],
  459. Keys);
  460. get_q(Key, Context) ->
  461. case proplists:lookup('q', Context#context.props) of
  462. {'q', Qs} -> proplists:get_value(z_convert:to_list(Key), Qs);
  463. none -> undefined
  464. end.
  465. %% @spec get_q(Key::string(), Context, Default) -> Value::string()
  466. %% @doc Get a request parameter, either from the query string or the post body. Post body has precedence over the query string.
  467. get_q(Key, Context, Default) ->
  468. case proplists:lookup('q', Context#context.props) of
  469. {'q', Qs} -> proplists:get_value(z_convert:to_list(Key), Qs, Default);
  470. none -> Default
  471. end.
  472. %% @spec get_q_all(Context) -> [{Key::string(), [Values]}]
  473. %% @doc Get all parameters.
  474. get_q_all(Context) ->
  475. {'q', Qs} = proplists:lookup('q', Context#context.props),
  476. Qs.
  477. %% @spec get_q_all(Key::string(), Context) -> [Values]
  478. %% @doc Get the all the parameters with the same name, returns the empty list when non found.
  479. get_q_all(Key, Context) ->
  480. {'q', Qs} = proplists:lookup('q', Context#context.props),
  481. proplists:get_all_values(z_convert:to_list(Key), Qs).
  482. %% @spec get_q_all_noz(Context) -> [{Key::string(), [Values]}]
  483. %% @doc Get all query/post args, filter the zotonic internal args.
  484. get_q_all_noz(Context) ->
  485. lists:filter(fun({X,_}) -> not is_zotonic_arg(X) end, z_context:get_q_all(Context)).
  486. is_zotonic_arg("zotonic_host") -> true;
  487. is_zotonic_arg("zotonic_dispatch") -> true;
  488. is_zotonic_arg("postback") -> true;
  489. is_zotonic_arg("triggervalue") -> true;
  490. is_zotonic_arg("z_trigger_id") -> true;
  491. is_zotonic_arg("z_target_id") -> true;
  492. is_zotonic_arg("z_delegate") -> true;
  493. is_zotonic_arg("z_sid") -> true;
  494. is_zotonic_arg("z_pageid") -> true;
  495. is_zotonic_arg("z_v") -> true;
  496. is_zotonic_arg("z_msg") -> true;
  497. is_zotonic_arg("z_comet") -> true;
  498. is_zotonic_arg(_) -> false.
  499. %% @spec get_q_validated(Key, Context) -> Value
  500. %% @doc Fetch a query parameter and perform the validation connected to the parameter. An exception {not_validated, Key}
  501. %% is thrown when there was no validator, when the validator is invalid or when the validation failed.
  502. get_q_validated([Key|_] = Keys, Context) when is_list(Key); is_atom(Key) ->
  503. lists:foldl(fun (K, Acc) ->
  504. case get_q_validated(K, Context) of
  505. undefined -> Acc;
  506. Value -> [{z_convert:to_atom(K), Value}|Acc]
  507. end
  508. end,
  509. [],
  510. Keys);
  511. get_q_validated(Key, Context) ->
  512. case proplists:lookup('q_validated', Context#context.props) of
  513. {'q_validated', Qs} ->
  514. case proplists:lookup(z_convert:to_list(Key), Qs) of
  515. {_Key, Value} -> Value;
  516. none -> throw({not_validated, Key})
  517. end
  518. end.
  519. %% ------------------------------------------------------------------------------------
  520. %% Communicate with pages, session and user processes
  521. %% ------------------------------------------------------------------------------------
  522. %% @doc Add the script from the context to all pages of the session.
  523. add_script_session(Context) ->
  524. Script = z_script:get_script(Context),
  525. add_script_session(Script, Context).
  526. %% @doc Add the script from the context to the page in the user agent.
  527. add_script_page(Context) ->
  528. Script = z_script:get_script(Context),
  529. add_script_page(Script, Context).
  530. %% @doc Add a script to the all pages of the session. Used for comet feeds.
  531. add_script_session(Script, Context) ->
  532. z_session:add_script(Script, Context#context.session_pid).
  533. %% @doc Add a script to the page in the user agent. Used for comet feeds.
  534. add_script_page(Script, Context) ->
  535. z_session_page:add_script(Script, Context#context.page_pid).
  536. %% @doc Spawn a new process, link it to the session process.
  537. spawn_link_session(Module, Func, Args, Context) ->
  538. z_session:spawn_link(Module, Func, Args, Context).
  539. %% @doc Spawn a new process, link it to the page process. Used for comet feeds.
  540. spawn_link_page(Module, Func, Args, Context) ->
  541. z_session_page:spawn_link(Module, Func, Args, Context).
  542. %% ------------------------------------------------------------------------------------
  543. %% Set/get/modify state properties
  544. %% ------------------------------------------------------------------------------------
  545. %% @spec get_value(Key::string(), Context) -> Value | undefined
  546. %% @doc Find a key in the context, page, session or persistent state.
  547. %% @todo Add page and user lookup
  548. get_value(Key, Context) ->
  549. case get(Key, Context) of
  550. undefined ->
  551. case get_page(Key, Context) of
  552. undefined ->
  553. case get_session(Key, Context) of
  554. undefined -> get_persistent(Key, Context);
  555. Value -> Value
  556. end;
  557. Value ->
  558. Value
  559. end;
  560. Value ->
  561. Value
  562. end.
  563. %% @doc Ensure that we have an id for the visitor
  564. persistent_id(Context) ->
  565. z_session:persistent_id(Context).
  566. %% @spec set_persistent(Key, Value, Context) -> Context
  567. %% @doc Set the value of the visitor variable Key to Value
  568. set_persistent(Key, Value, Context) ->
  569. z_session:set_persistent(Key, Value, Context),
  570. Context.
  571. %% @spec get_persistent(Key, Context) -> Value
  572. %% @doc Fetch the value of the visitor variable Key
  573. get_persistent(_Key, #context{session_pid=undefined}) ->
  574. undefined;
  575. get_persistent(Key, Context) ->
  576. z_session:get_persistent(Key, Context).
  577. %% @spec set_session(Key, Value, Context) -> Context
  578. %% @doc Set the value of the session variable Key to Value
  579. set_session(Key, Value, Context) ->
  580. z_session:set(Key, Value, Context#context.session_pid),
  581. Context.
  582. %% @spec get_session(Key, Context) -> Value
  583. %% @doc Fetch the value of the session variable Key
  584. get_session(_Key, #context{session_pid=undefined}) ->
  585. undefined;
  586. get_session(Key, Context) ->
  587. z_session:get(Key, Context#context.session_pid).
  588. %% @spec incr_session(Key, Increment, Context) -> {NewValue, NewContext}
  589. %% @doc Increment the session variable Key
  590. incr_session(Key, Value, Context) ->
  591. {z_session:incr(Key, Value, Context#context.session_pid), Context}.
  592. %% @spec set_page(Key, Value, Context) -> Context
  593. %% @doc Set the value of the page variable Key to Value
  594. set_page(Key, Value, Context) ->
  595. z_session_page:set(Key, Value, Context#context.page_pid),
  596. Context.
  597. %% @spec get_page(Key, Context) -> Value
  598. %% @doc Fetch the value of the page variable Key
  599. get_page(_Key, #context{page_pid=undefined}) ->
  600. undefined;
  601. get_page(Key, Context) ->
  602. z_session_page:get(Key, Context#context.page_pid).
  603. %% @spec incr_page(Key, Increment, Context) -> {NewValue, NewContext}
  604. %% @doc Increment the page variable Key
  605. incr_page(Key, Value, Context) ->
  606. {z_session_page:incr(Key, Value, Context#context.session_pid), Context}.
  607. %% @spec set(Key, Value, Context) -> Context
  608. %% @doc Set the value of the context variable Key to Value
  609. set(Key, Value, Context) ->
  610. Props = z_utils:prop_replace(Key, Value, Context#context.props),
  611. Context#context{props = Props}.
  612. %% @spec set(PropList, Context) -> Context
  613. %% @doc Set the value of the context variables to all {Key, Value} properties.
  614. set(PropList, Context) when is_list(PropList) ->
  615. NewProps = lists:foldl(
  616. fun ({Key,Value}, Props) ->
  617. z_utils:prop_replace(Key, Value, Props)
  618. end, Context#context.props, PropList),
  619. Context#context{props = NewProps}.
  620. %% @spec get(Key, Context) -> Value | undefined
  621. %% @doc Fetch the value of the context variable Key, return undefined when Key is not found.
  622. get(Key, Context) ->
  623. case proplists:lookup(Key, Context#context.props) of
  624. {Key, Value} -> Value;
  625. none -> undefined
  626. end.
  627. %% @spec get(Key, Context, Default) -> Value | Default
  628. %% @doc Fetch the value of the context variable Key, return Default when Key is not found.
  629. get(Key, Context, Default) ->
  630. case proplists:lookup(Key, Context#context.props) of
  631. {Key, Value} -> Value;
  632. none -> Default
  633. end.
  634. %% @spec get_all(Context) -> PropList
  635. %% @doc Return a proplist with all context variables.
  636. get_all(Context) ->
  637. Context#context.props.
  638. %% @spec incr(Key, Increment, Context) -> {NewValue,NewContext}
  639. %% @doc Increment the context variable Key
  640. incr(Key, Value, Context) ->
  641. R = case z_convert:to_integer(get(Key, Context)) of
  642. undefined -> Value;
  643. N -> N + Value
  644. end,
  645. {R, set(Key, R, Context)}.
  646. %% @doc Return the selected language of the Context
  647. language(Context) ->
  648. Context#context.language.
  649. %% @doc Set the language of the context.
  650. %% @spec set_language(atom(), context()) -> context()
  651. set_language(Lang, Context) ->
  652. Context#context{language=Lang}.
  653. %% @doc Set a response header for the request in the context.
  654. %% @spec set_resp_header(Header, Value, Context) -> NewContext
  655. set_resp_header(Header, Value, Context = #context{wm_reqdata=ReqData}) ->
  656. RD1 = wrq:set_resp_header(Header, Value, ReqData),
  657. Context#context{wm_reqdata=RD1}.
  658. %% @doc Get a response header
  659. %% @spec get_resp_header(Header, Context) -> Value
  660. get_resp_header(Header, #context{wm_reqdata=ReqData}) ->
  661. wrq:get_resp_header(Header, ReqData).
  662. %% @doc Get a request header
  663. %% @spec get_req_header(Header, Context) -> Value
  664. get_req_header(Header, Context) ->
  665. ReqData = get_reqdata(Context),
  666. wrq:get_req_header(Header, ReqData).
  667. %% @doc Return the request path
  668. %% @spec get_req_path(Context) -> list()
  669. get_req_path(Context) ->
  670. ReqData = get_reqdata(Context),
  671. wrq:raw_path(ReqData).
  672. %% @doc Fetch the cookie domain, defaults to 'undefined' which will equal the domain
  673. %% to the domain of the current request.
  674. %% @spec cookie_domain(Context) -> list() | undefined
  675. cookie_domain(Context) ->
  676. case m_site:get(cookie_domain, Context) of
  677. Empty when Empty == undefined; Empty == []; Empty == <<>> ->
  678. %% When there is a stream domain, the check if the stream domain is a subdomain
  679. %% of the hostname, if so then set a wildcard
  680. case m_site:get(streamhost, Context) of
  681. None when None == undefined; None == []; None == <<>> ->
  682. undefined;
  683. StreamDomain ->
  684. [StreamDomain1|_] = string:tokens(z_convert:to_list(StreamDomain), ":"),
  685. Hostname = hostname(Context),
  686. case postfix(Hostname, StreamDomain1) of
  687. [] -> Hostname;
  688. [$.|_] = Prefix -> Prefix;
  689. Prefix -> [$.|Prefix]
  690. end
  691. end;
  692. Domain ->
  693. z_convert:to_list(Domain)
  694. end.
  695. %% Return the longest matching postfix of two lists.
  696. postfix(A, B) ->
  697. postfix(lists:reverse(z_convert:to_list(A)), lists:reverse(z_convert:to_list(B)), []).
  698. postfix([X|A], [X|B], Acc) ->
  699. postfix(A, B, [X|Acc]);
  700. postfix(_A, _B, Acc) ->
  701. Acc.
  702. %% @doc The document domain used for cross domain iframe javascripts
  703. document_domain(Context) ->
  704. case cookie_domain(Context) of
  705. [$.|Domain] -> Domain;
  706. Domain -> Domain
  707. end.
  708. %% @doc Fetch the domain and port for stream (comet/websocket) connections
  709. %% @spec streamhost(Context) -> list()
  710. streamhost(Context) ->
  711. case m_site:get(streamhost, Context) of
  712. Empty when Empty == undefined; Empty == []; Empty == <<>> ->
  713. hostname_port(Context);
  714. Domain ->
  715. Domain
  716. end.
  717. %% ------------------------------------------------------------------------------------
  718. %% Local helper functions
  719. %% ------------------------------------------------------------------------------------
  720. %% @spec parse_form_urlencoded(context()) -> {list(), NewContext}
  721. %% @doc Return the keys in the body of the request, only if the request is application/x-www-form-urlencoded
  722. parse_form_urlencoded(Context) ->
  723. ReqData = get_reqdata(Context),
  724. case wrq:get_req_header_lc("content-type", ReqData) of
  725. "application/x-www-form-urlencoded" ++ _ ->
  726. case wrq:req_body(ReqData) of
  727. {undefined, ReqData1} ->
  728. {[], set_reqdata(ReqData1, Context)};
  729. {Binary, ReqData1} ->
  730. {mochiweb_util:parse_qs(Binary), set_reqdata(ReqData1, Context)}
  731. end;
  732. "multipart/form-data" ++ _ ->
  733. FileCheckFun = fun(_Filename, _ContentType, _Size) ->
  734. ok
  735. end,
  736. {Form, ContextRcv} = z_parse_multipart:recv_parse(FileCheckFun, Context),
  737. FileArgs = [ {Name, #upload{filename=Filename, tmpfile=TmpFile}} || {Name, Filename, TmpFile} <- Form#multipart_form.files ],
  738. {Form#multipart_form.args ++ FileArgs, ContextRcv};
  739. _Other ->
  740. {[], Context}
  741. end.
  742. %% @doc Some user agents have too aggressive client side caching.
  743. %% These headers prevent the caching of content on the user agent iff
  744. %% the content generated has a session. You can prevent addition of
  745. %% these headers by not calling z_context:ensure_session/1, or
  746. %% z_context:ensure_all/1.
  747. %% @spec add_nocache_headers(#context{}) -> #context{}
  748. add_nocache_headers(Context = #context{wm_reqdata=ReqData}) ->
  749. RD1 = wrq:set_resp_header("Cache-Control", "no-store, no-cache, must-revalidate, post-check=0, pre-check=0", ReqData),
  750. RD2 = wrq:set_resp_header("Expires", httpd_util:rfc1123_date({{2008,12,10}, {15,30,0}}), RD1),
  751. % This let IE6 accept our cookies, basically we tell IE6 that our cookies do not contain any private data.
  752. RD3 = wrq:set_resp_header("P3P", "CP=\"NOI ADM DEV PSAi COM NAV OUR OTRo STP IND DEM\"", RD2),
  753. Context#context{wm_reqdata=RD3}.