%% @author Marc Worrell, Arjan Scherpenisse %% @copyright 2009-2025 Marc Worrell, Arjan Scherpenisse %% @doc Admin webmachine_controller. %% @end %% Copyright 2009-2025 Marc Worrell, Arjan Scherpenisse %% %% 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(controller_admin_edit). -moduledoc(" The main admin edit controller. This controller serves the edit page where [resources](/id/doc_glossary#term-resource) can be edited. Todo Extend documentation "). -author("Marc Worrell "). -export([ service_available/1, resource_exists/1, previously_existed/1, moved_temporarily/1, is_authorized/1, event/2, filter_props/1, ensure_id/1, process/4 ]). -include_lib("zotonic_core/include/zotonic.hrl"). service_available(Context) -> Context1 = z_context:set_noindex_header(Context), Context2 = z_context:set_nocache_headers(Context1), {true, Context2}. is_authorized(Context) -> Context1 = z_context:set_resp_header(<<"x-frame-options">>, <<"SAMEORIGIN">>, Context), {Context2, Id} = ensure_id(Context1), z_controller_helper:is_authorized(Id, [ {use, mod_admin}, {view, Id} ], Context2). resource_exists(Context) -> {Context2, Id} = ensure_id(Context), case Id of undefined -> {false, Context2}; _N -> {m_rsc:exists(Id, Context2), Context2} end. previously_existed(Context) -> {Context1, Id} = ensure_id(Context), IsGone = m_rsc_gone:is_gone(Id, Context1), {IsGone, Context1}. moved_temporarily(Context) -> {Context1, Id} = ensure_id(Context), redirect(m_rsc_gone:get_new_location(Id, Context1), Context1). redirect(undefined, Context) -> {false, Context}; redirect(Location, Context) -> {{true, Location}, Context}. process(_Method, _AcceptedCT, _ProvidedCT, Context) -> Id = z_context:get(id, Context), Context1 = z_context:set_resource_headers(Id, Context), Blocks = z_notifier:foldr(#admin_edit_blocks{id=Id}, [], Context1), Vars = [ {id, Id}, {blocks, lists:sort(Blocks)} | z_context:get_all(Context1) ], Html = z_template:render(z_context:get(template, Context, {cat, "admin_edit.tpl"}), Vars, Context1), z_context:output(Html, Context1). %% @doc Fetch the (numerical) page id from the request ensure_id(Context) -> case z_context:get(id, Context) of N when is_integer(N) -> {Context, N}; undefined -> MaybeId = m_rsc:rid(z_context:get_q(<<"id">>, Context), Context), {z_context:set(id, MaybeId, Context), MaybeId} end. %% @doc Return the language the user is editing. This is part of the postback data %% which gets updated whenever the user switches languages. edit_language(Context) -> case z_context:get_q(<<"z_postback_data">>, Context) of #{ <<"z_edit_language">> := ELang } -> case z_language:to_language_atom(ELang) of {ok, ElangAtom} -> ElangAtom; {error, _} -> z_context:language(Context) end; _ -> z_context:language(Context) end. %% @doc Handle the submit of the resource edit form event(#submit{message=rscform} = Msg, Context) -> event(Msg#submit{message={rscform, []}}, Context); event(#submit{message={rscform, Args}}, Context) -> Post = z_context:get_q_all_noz(Context), Props = filter_props(Post), Id = z_convert:to_integer(proplists:get_value(<<"id">>, Props)), Props1 = proplists:delete(<<"id">>, Props), CatBefore = m_rsc:p(Id, <<"category_id">>, Context), Props2 = z_notifier:foldl( #admin_rscform{ id = Id, is_a = m_rsc:is_a(Id, Context) }, Props1, Context), UpdateOptions = case proplists:get_value(default_tz, Args) of undefined -> []; Tz -> [ {default_tz, Tz} ] end, case m_rsc:update(Id, Props2, UpdateOptions, Context) of {ok, _} -> case z_context:get_q(<<"z_submitter">>, Context) of SaveView when SaveView =:= <<"save_view">>; SaveView =:= <<"save_view_float">> -> ContextRedirect = z_context:set_language(edit_language(Context), Context), case proplists:get_value(view_location, Args) of undefined -> PageUrl = m_rsc:p(Id, <<"page_url">>, ContextRedirect), z_render:wire({redirect, [{location, PageUrl}]}, Context); Location -> z_render:wire({redirect, [{location, Location}]}, Context) end; Submitter -> case m_rsc:p(Id, <<"category_id">>, Context) of CatBefore -> Context1 = update_rsc_form(Id, Context), Title = z_convert:to_binary( z_trans:lookup_fallback(m_rsc:p(Id, <<"title">>, Context1), Context1)), Context2 = z_render:growl([<<"Saved \"">>, Title, <<"\".">>], Context1), case Submitter of <<"save_duplicate">> -> z_render:wire({dialog_duplicate_rsc, [{id, Id}]}, Context2); _SaveStay -> z_render:wire(proplists:get_all_values(on_success, Args), Context2) end; _CatOther -> z_render:wire({reload, []}, Context) end end; {error, duplicate_uri} -> z_render:growl_error(?__("Error, duplicate uri. Please change the uri.", Context), Context); {error, duplicate_page_path} -> z_render:growl_error(?__("Error, duplicate page path. Please change the uri.", Context), Context); {error, duplicate_name} -> z_render:growl_error(?__("Error, duplicate name. Please change the name.", Context), Context); {error, eacces} -> z_render:growl_error(?__("You don't have permission to edit this page.", Context), Context); {error, invalid_query} -> z_render:growl_error(?__("Your search query is invalid. Please correct it before saving.", Context), Context); {error, Message} when is_list(Message); is_binary(Message) -> z_render:growl_error(Message, Context) end; event(#postback{message={view, Args}}, Context) -> {id, Id} = proplists:lookup(id, Args), ContextRedirect = z_context:set_language(edit_language(Context), Context), PageUrl = m_rsc:p(Id, <<"page_url">>, ContextRedirect), z_render:wire({redirect, [{location, PageUrl}]}, Context); %% Opts: rsc_id, div_id, edge_template event(#postback{message={reload_media, Opts}}, Context) -> DivId = proplists:get_value(div_id, Opts), {Html, Context1} = z_template:render_to_iolist({cat, "_edit_media.tpl"}, Opts, Context), z_render:update(DivId, Html, Context1); event(#postback{message={delete_media, Opts}}, Context) -> {id, Id} = proplists:lookup(id, Opts), case m_media:delete(Id, Context) of ok -> Context; {error, eacces} -> z_render:growl_error("Sorry, you have no permission to edit this page.", Context) end; event(#sort{items=Sorted, drop={dragdrop, {object_sorter, Props}, _, _}}, Context) -> RscId = proplists:get_value(id, Props), Predicate = proplists:get_value(predicate, Props), EdgeIds = [ EdgeId || {dragdrop, EdgeId, _, _ElementId} <- Sorted ], m_edge:update_sequence_edge_ids(RscId, Predicate, EdgeIds, Context), Context; %% Previewing the results of a query in the admin edit event(#postback{message={query_preview, Opts}}, Context) -> DivId = proplists:get_value(div_id, Opts), RscId = proplists:get_value(rsc_id, Opts), try Q = z_search_props:from_text(z_context:get_q(<<"triggervalue">>, Context)), S = z_search:search(<<"query">>, Q, 1, 20, Context), Vars = [ {id, RscId}, {result, S} ], {Html, Context1} = z_template:render_to_iolist("_admin_query_preview.tpl", Vars, Context), z_render:update(DivId, Html, Context1) catch _: {error, {Kind, Arg}} -> z_render:growl_error([?__("There is an error in your query:", Context), " ", Kind, " - ", Arg], Context) end. set_value_slug(undefined, Context) -> set_value_slug(<<>>, Context); set_value_slug(#trans{ tr = Tr }, Context) -> lists:foldl( fun({Lang, V}, Ctx) -> z_render:set_value( "title_slug--" ++ atom_to_list(Lang), V, Ctx) end, Context, Tr); set_value_slug(Slug, Context) -> z_render:set_value("title_slug", Slug, Context). %% @doc Remove the submit buttons from the resource properties. filter_props(Fs) -> Remove = [ <<"save_view">>, <<"save_view_float">>, <<"save_duplicate">>, <<"save_stay">>, <<"save_stay_float">> ], lists:foldl(fun(P, Acc) -> proplists:delete(P, Acc) end, Fs, Remove). %% @doc Patch the rsc edit form with the new values. update_rsc_form(Id, Context) -> Context1 = z_render:set_value("field-name", m_rsc:p(Id, <<"name">>, Context), Context), Context2 = z_render:set_value("field-uri", m_rsc:p(Id, <<"uri_raw">>, Context), Context1), Context3 = update_rsc_page_path(m_rsc:p(Id, <<"page_path">>, Context), Context2), Context4 = z_render:set_value("website", m_rsc:p(Id, <<"website">>, Context), Context3), Context5 = set_value_slug(m_rsc:p(Id, <<"title_slug">>, Context), Context4), case z_convert:to_bool(m_rsc:p(Id, <<"is_protected">>, Context)) andalso z_acl:rsc_deletable(Id, Context) of true -> z_render:wire("delete-button", {disable, []}, Context5); false -> z_render:wire("delete-button", {enable, []}, Context5) end. update_rsc_page_path(undefined, Context) -> z_render:set_value("field-page-path", <<>>, Context); update_rsc_page_path(Path, Context) when is_binary(Path) -> Tr = #trans{ tr = [ {z_language:default_language(Context), Path} ]}, update_rsc_page_path(Tr, Context); update_rsc_page_path(#trans{} = TransPath, Context) -> lists:foldl( fun(Lang, CAcc) -> Path = z_trans:lookup_fallback(TransPath, [ Lang ], CAcc), Path1 = filter_urldecode:urldecode(z_convert:to_binary(Path), Context), EltId = <<"field-page-path--", (atom_to_binary(Lang))/binary>>, z_render:set_value(EltId, Path1, CAcc) end, Context, z_language:editable_language_codes(Context)).