%% @author Marc Worrell %% @copyright 2009-2012 Marc Worrell %% Date: 2009-06-09 %% @doc Administrative interface. Aka backend. %% Copyright 2009-2012 Marc Worrell %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. -module(mod_admin). -author("Marc Worrell "). -mod_title("Admin module"). -mod_description("Provides administrative interface for editing pages, media, users etc."). -mod_depends([ base, authentication, mod_search, mod_mqtt, mod_wires ]). -mod_provides([ admin ]). -mod_schema(1). -mod_prio(1000). -export([ observe_sanitize_element/3, observe_admin_menu/3, observe_admin_edit_blocks/3, observe_module_ready/2, event/2, do_link/5, manage_schema/2 ]). -include_lib("zotonic_core/include/zotonic.hrl"). -include_lib("include/admin_menu.hrl"). %% @doc Fix tinymce images that are the result of copying %% observe_sanitize_element(#sanitize_element{}, {<<"img">>, Attrs, _Enclosed} = Element, Context) -> case proplists:get_value(<<"src">>, Attrs) of <<"/admin/media/preview/", Number/binary>> -> NumberList = binary_to_list(Number), case m_rsc:rid(NumberList, Context) of undefined -> {nop, []}; ImgId -> CommentText = [ <<" z-media ">>, integer_to_list(ImgId), 32, class_to_opts(proplists:get_value(<<"class">>, Attrs)) ], {comment, iolist_to_binary(CommentText)} end; _OtherSrc -> Element end; observe_sanitize_element(#sanitize_element{}, Element, _Context) -> Element. class_to_opts(undefined) -> []; class_to_opts(Class) -> case re:run(Class, "z-tinymce-media-([a-z]+)-([a-z]*)", [{capture, all_but_first, binary}, global]) of nomatch -> []; {match, Ms} -> [ z_json:encode([{A,V} || [A,V] <- Ms]), 32 ] end. observe_admin_menu(#admin_menu{}, Acc, Context) -> [ #menu_item{id = admin_dashboard, label = ?__("Dashboard", Context), url = admin }, %% CONTENT %% #menu_item{id = admin_content, label = ?__("Content", Context)}, #menu_item{id = admin_overview, parent = admin_content, label = ?__("Pages", Context), url = admin_overview_rsc, sort = 1}, #menu_item{id = admin_media, parent = admin_content, label = ?__("Media", Context), url = admin_media, sort = 2}, #menu_separator{ parent=admin_content, sort = 10} ] ++ admin_menu_content_queries(Context) ++ [ %% STRUCTURE %% #menu_item{id = admin_structure, label = ?__("Structure", Context)}, %% MODULES %% #menu_item{id = admin_modules, label = ?__("Modules", Context)}, %% AUTH %% #menu_item{id = admin_auth, label = ?__("Auth", Context)}, %% SYSTEM %% #menu_item{id = admin_system, label = ?__("System", Context)}, #menu_item{id = admin_status, parent = admin_system, visiblecheck = {acl, use, mod_admin_config}, label = ?__("Status", Context), url = admin_status} |Acc]. admin_menu_content_queries(Context) -> #search_result{result=Result} = z_search:search({all_bytitle, [{cat,admin_content_query}]}, Context), AdminOverviewQueryId = m_rsc:rid(admin_overview_query, Context), Result1 = lists:filter( fun({_Title,Id}) -> z_acl:rsc_visible(Id, Context) andalso Id =/= AdminOverviewQueryId end, Result), CQ = lists:map( fun({Title, Id}) -> #menu_item{ id = {admin_query, Id}, parent = admin_content, label = Title, url = {admin_overview_rsc, [{qquery_id, Id}]}, sort = 10 } end, Result1), case CQ of [] -> []; _ -> CQ ++ [ #menu_separator{ parent = admin_content, sort = 10 } ] end. observe_admin_edit_blocks(#admin_edit_blocks{}, Menu, Context) -> [ {1, ?__("Standard", Context), [ {header, ?__("Header", Context)}, {text, ?__("Text", Context)}, {page, ?__("Embed page", Context)} ]} | Menu ]. observe_module_ready(module_ready, Context) -> z_depcache:flush(admin_menu, Context). event(#postback_notify{message= <<"admin-insert-block">>}, Context) -> Language = case z_context:get_q("language", Context) of undefined -> []; Ls -> Ls1 = string:tokens(Ls, ","), [ list_to_atom(L) || L <- lists:filter(fun z_language:is_valid/1, Ls1) ] end, EditLanguage = case z_context:get_q("edit_language", Context) of undefined -> z_context:language(Context); EL -> case z_language:is_valid(EL) of true -> list_to_atom(EL); false -> z_context:language(Context) end end, Type = z_string:to_name(z_context:get_q("type", Context)), RscId = z_convert:to_integer(z_context:get_q("rsc_id", Context)), Render = #render{ template="_admin_edit_block_li.tpl", vars=[ {id, RscId}, {r_language, Language}, {edit_language, EditLanguage}, is_new, {is_editable, z_acl:rsc_editable(RscId, Context)}, {blk, [{type, Type}]}, {blocks, lists:sort(z_notifier:foldl(#admin_edit_blocks{id=RscId}, [], Context))} ] }, case z_html:escape(z_context:get_q("after", Context)) of undefined -> z_render:insert_top("edit-blocks", Render, Context); AfterId -> z_render:insert_after(AfterId, Render, Context) end; event(#postback_notify{message = <<"feedback">>, trigger = Trigger, target=TargetId}, Context) when Trigger =:= <<"dialog-new-rsc-tab">>; Trigger =:= <<"dialog-connect-find">> -> % Find pages matching the search criteria. CreatorId = z_convert:to_integer(z_context:get_q(<<"find_creator_id">>, Context)), SubjectId = z_convert:to_integer(z_context:get_q(<<"subject_id">>, Context)), ObjectId = z_convert:to_integer(z_context:get_q(<<"object_id">>, Context)), Predicate = z_convert:to_binary(z_context:get_q(<<"predicate">>, Context, <<>>)), PredicateId = m_rsc:rid(Predicate, Context), TextL = lists:foldl( fun(Q, Acc) -> case z_context:get_q(Q, Context) of <<>> -> Acc; undefined -> Acc; V -> case Acc of [] -> V; _ -> [ V, " ", Acc ] end end end, [], [ <<"find_text">>, <<"title">>, <<"new_rsc_title">>, <<"name_first">>, <<"name_surname">>, <<"email">> ]), Text = iolist_to_binary(TextL), Category = case z_context:get_q(<<"find_category">>, Context) of undefined -> z_context:get_q(<<"category_id">>, Context); <<>> -> z_context:get_q(<<"category_id">>, Context); Cat -> Cat end, Cats = case z_convert:to_binary(Category) of <<"p:", Predicate/binary>> -> feedback_categories(SubjectId, Predicate, ObjectId, Context); <<>> when PredicateId =/= undefined -> feedback_categories(SubjectId, Predicate, ObjectId, Context); <<>> -> []; <<"*">> -> []; CIds -> CatIds = binary:split(CIds, <<",">>, [ global ]), [ {m_rsc:rid(CatId, Context)} || CatId <- CatIds, CatId =/= <<>> ] end, Vars = [ {intent, z_context:get_q(<<"intent">>, Context)}, {creator_id, CreatorId}, {subject_id, SubjectId}, {cat, Cats}, {cat_exclude, z_context:get_q(<<"cat_exclude">>, Context)}, {predicate, Predicate}, {text, Text}, {is_multi_cat, length(Cats) > 1}, {category_id, case Cats of [{CId}] -> CId; _ -> undefined end}, {is_zlink, z_convert:to_bool( z_context:get_q(<<"is_zlink">>, Context) )} ] ++ case z_context:get_q(<<"find_cg">>, Context) of <<>> -> []; "" -> []; undefined -> []; CgId -> [ {content_group, m_rsc:rid(CgId, Context)}] end, case Trigger of <<"dialog-connect-find">> -> z_render:wire([ {remove_class, [{target, TargetId}, {class, "loading"}]}, {update, [{target, TargetId}, {template, "_action_dialog_connect_tab_find_results.tpl"} | Vars]} ], Context); <<"dialog-new-rsc-tab">> -> z_render:wire([ {remove_class, [{target, TargetId}, {class, "loading"}]}, {update, [{target, TargetId}, {template, "_action_dialog_new_rsc_tab_find_results.tpl"} | Vars]} ], Context) end; event(#postback{message={admin_connect_select, Args}}, Context) -> SelectId = z_context:get_q(<<"select_id">>, Context), IsConnected = z_convert:to_bool(z_context:get_q(<<"is_connected">>, Context)), SubjectId0 = proplists:get_value(subject_id, Args), ObjectId0 = proplists:get_value(object_id, Args), Predicate = proplists:get_value(predicate, Args), Callback = proplists:get_value(callback, Args), IsConnectToggle = z_convert:to_bool( proplists:get_value(is_connect_toggle, Args) ), QAction = proplists:get_all_values(action, Args), QActions = proplists:get_value(actions, Args, []), QAction1 = case QAction of [undefined] -> []; _ -> QAction end, QActions1 = case QActions of undefined -> []; _ -> QActions end, Actions = QAction1 ++ QActions1, {SubjectId, ObjectId} = case z_utils:is_empty(ObjectId0) of true -> {z_convert:to_integer(SubjectId0), z_convert:to_integer(SelectId)}; false -> {z_convert:to_integer(SelectId), z_convert:to_integer(ObjectId0)} end, % Only disconnect if connection is not made from tinymce IsUnlink = case IsConnectToggle of false -> false; true -> IsConnected end, case do_link_unlink(IsUnlink, SubjectId, Predicate, ObjectId, Callback, Context) of {ok, Context1} -> Context2 = case z_convert:to_bool(proplists:get_value(autoclose, Args)) of true -> z_render:dialog_close(Context1); false -> Context1 end, z_render:wire(Actions, Context2); {error, Context1} -> Context1 end; %% Called when a block connection is done event(#postback_notify{message= <<"update">>, target=TargetId}, Context) -> Id = z_convert:to_integer(z_context:get_q(<<"id">>, Context)), Predicate = get_predicate(z_context:get_q(<<"predicate">>, Context), Context), Vars = [ {id, Id}, {predicate, Predicate} ], Context1 = z_render:wire({unmask, [{target_id, TargetId}]}, Context), z_render:update(TargetId, #render{template={cat, "_rsc_block_item.tpl"}, vars=Vars}, Context1); event(#postback{message = {admin_rsc_redirect, Args}}, Context) -> SelectId = m_rsc:rid(z_context:get_q(<<"select_id">>, Context), Context), Dispatch = case proplists:get_value(redirect, Args) of true -> admin_edit_rsc; false -> admin_edit_rsc; undefined -> admin_edit_rsc; D when is_atom(D) -> D end, z_render:wire({redirect, [ {dispatch, Dispatch}, {id, SelectId} ]}, Context); event(_E, Context) -> ?DEBUG(_E), Context. feedback_categories(SubjectId, Predicate, _ObjectId, Context) when is_integer(SubjectId) -> m_predicate:object_category(Predicate, Context); feedback_categories(_SubjectId, Predicate, ObjectId, Context) when is_integer(ObjectId) -> m_predicate:subject_category(Predicate, Context). get_predicate(undefined, _Context) -> undefined; get_predicate([], _Context) -> undefined; get_predicate(P, Context) when is_list(P) -> case z_utils:only_digits(P) of true -> RscId = m_rsc:rid(P, Context), z_convert:to_atom(m_rsc:p(RscId, name, Context)); false -> list_to_existing_atom(P) end. do_link(SubjectId, Predicate, ObjectId, Callback, Context) -> do_link_unlink(false, SubjectId, Predicate, ObjectId, Callback, Context). do_link_unlink(_IsUnlink, SubjectId, Predicate, ObjectId, Callback, Context) when Predicate =:= ""; Predicate =:= <<>>; Predicate =:= undefined; SubjectId =:= undefined; ObjectId =:= undefined -> ContextP = context_language(Context), Title = m_rsc:p(ObjectId, title, Context), Vars = [ {subject_id, undefined}, {predicate, undefined}, {object_id, ObjectId}, {url_language, m_rsc:page_url(ObjectId, ContextP)}, {title_language, z_trans:lookup_fallback(Title, ContextP)}, {title, z_trans:lookup_fallback(Title, Context)} ], case Callback of undefined -> {ok, Context}; "" -> {ok, Context}; <<>> -> {ok, Context}; {CB, Args} -> {ok, z_render:wire({script, [{script, [ z_sanitize:ensure_safe_js_callback(CB), $(, z_utils:js_object(Vars++Args,Context), $),$; ]}]}, Context)}; CB -> {ok, z_render:wire({script, [{script, [ z_sanitize:ensure_safe_js_callback(CB), $(, z_utils:js_object(Vars,Context), $),$; ]}]}, Context)} end; do_link_unlink(IsUnlink, SubjectId, Predicate, ObjectId, Callback, Context) -> case z_acl:rsc_linkable(SubjectId, Context) of true -> case m_edge:get_id(SubjectId, Predicate, ObjectId, Context) of undefined when IsUnlink -> do_link_unlink_feedback( false, false, undefined, SubjectId, Predicate, ObjectId, Callback, Context); undefined when not IsUnlink -> case m_edge:insert(SubjectId, Predicate, ObjectId, Context) of {ok, EdgeId} -> do_link_unlink_feedback( true, false, EdgeId, SubjectId, Predicate, ObjectId, Callback, Context); {error, _} -> do_link_unlink_error(IsUnlink, Context) end; EdgeId when IsUnlink -> case m_edge:delete(SubjectId, Predicate, ObjectId, Context) of ok -> do_link_unlink_feedback( false, true, EdgeId, SubjectId, Predicate, ObjectId, Callback, Context); {error, _} -> do_link_unlink_error(IsUnlink, Context) end; EdgeId when not IsUnlink-> do_link_unlink_feedback( false, false, EdgeId, SubjectId, Predicate, ObjectId, Callback, Context) end; false -> do_link_unlink_error(IsUnlink, Context) end. do_link_unlink_error(false = _IsUnlink, Context) -> {error, z_render:growl_error(?__("Sorry, you have no permission to add the connection.", Context), Context)}; do_link_unlink_error(true, Context) -> {error, z_render:growl_error(?__("Sorry, you have no permission to delete the connection.", Context), Context)}. do_link_unlink_feedback(IsNew, IsDelete, EdgeId, SubjectId, Predicate, ObjectId, Callback, Context) -> ContextP = context_language(Context), Title = case m_rsc:p(ObjectId, title, Context) of undefined -> ?__("Untitled", Context); T -> T end, Context1 = case {IsNew, IsDelete} of {true,false} -> z_render:growl([ ?__("Added the connection to", ContextP),<<" \"">>, Title, <<"\".">> ], Context); {false,true} -> z_render:growl([ ?__("Removed the connection to", ContextP),<<" \"">>, Title, <<"\".">> ], Context); {false,false} -> Context end, case Callback of undefined -> {ok, Context1}; "" -> {ok, Context1}; <<>> -> {ok, Context1}; _ -> Vars = [ {is_new, IsNew}, {is_delete, IsDelete}, {subject_id, SubjectId}, {predicate, Predicate}, {object_id, ObjectId}, {edge_id, EdgeId}, {url_language, m_rsc:page_url(ObjectId, ContextP)}, {title_language, z_trans:lookup_fallback(Title, ContextP)}, {title, z_trans:lookup_fallback(Title, Context)} ], case Callback of {CB, Args} -> {ok, z_render:wire({script, [{script, [ z_sanitize:ensure_safe_js_callback(CB), $(, z_utils:js_object(Vars++Args,ContextP), $),$; ]}]}, Context1)}; CB -> {ok, z_render:wire({script, [{script, [ z_sanitize:ensure_safe_js_callback(CB), $(, z_utils:js_object(Vars,ContextP), $),$; ]}]}, Context1)} end end. context_language(Context) -> case z_context:get_q("language", Context) of undefined -> Context; [] -> Context; Lang -> case z_language:to_language_atom(Lang) of {ok, LanguageCode} -> z_context:set_language(LanguageCode, Context); _ -> Context end end. manage_schema(_Version, _Context) -> #datamodel{ categories=[ {admin_content_query, 'query', [ {title, {trans, [ {en, <<"Admin content query">>}, {nl, <<"Admin inhoud zoekopdracht">>} ]}} ]} ] }.