PageRenderTime 70ms CodeModel.GetById 18ms app.highlight 44ms RepoModel.GetById 1ms app.codeStats 0ms

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