Current section

Files

Jump to
zotonic_mod_admin src support z_admin_refers.erl
Raw

src/support/z_admin_refers.erl

%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2023-2025 Maximonster Interactive Things BV
%% @doc Ensure that alle embedded ids in a resource are connected using
%% a 'refers' edge.
%% @end
%% Copyright 2023-2025 Maximonster Interactive Things BV
%%
%% 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(z_admin_refers).
-export([
ensure_refers/2,
insert_ensure_refers_all_task/1,
task_ensure_refers_all/2
]).
-include_lib("zotonic_core/include/zotonic.hrl").
%% @doc Insert a task to check all resource for embedded resource references and
%% add a refers edge for all of those references.
-spec insert_ensure_refers_all_task( Context ) -> ok when
Context :: z:context().
insert_ensure_refers_all_task(Context) ->
{ok, _} = z_pivot_rsc:insert_task(?MODULE, task_ensure_refers_all, <<>>, [1], Context),
ok.
%% @doc Check all resource for embedded resource references and add a refers
%% edge for all of them.
-spec task_ensure_refers_all(FromId, Context) -> z_pivot_rsc:task_return() when
FromId :: pos_integer(),
Context :: z:context().
task_ensure_refers_all(FromId, Context) ->
case z_db:q("
select id from rsc
where id > $1
order by id asc
limit 1000",
[ FromId ],
Context)
of
[] ->
ok;
Rs ->
Ids = [ Id || {Id} <- Rs ],
ContextSudo = z_acl:sudo(Context),
lists:foreach(
fun(Id) ->
ensure_refers(Id, ContextSudo)
end, Ids),
{delay, 0, [ lists:max(Ids) ]}
end.
%% @doc Set the 'refers' edges to keep track which resources are used where.
%% This is needed for the automatic cleanup of 'dependent' resources.
-spec ensure_refers(Id, Context) -> ok when
Id :: m_rsc:resource_id(),
Context :: z:context().
ensure_refers(Id, Context) ->
% Keep an administration of what resources are referred from within the pivoted resource
RscCols = z_db:column_names_bin(rsc, Context),
EmbeddedIds = find_ids(m_rsc:get(Id, Context), RscCols, Context),
EmbeddedIds1 = lists:filter(fun is_integer/1, EmbeddedIds),
Objects = m_edge:objects(Id, refers, Context),
New = EmbeddedIds1 -- Objects,
Old = Objects -- EmbeddedIds1,
New1 = lists:filter(fun(ObjId) -> m_rsc:exists(ObjId, Context) end, New),
?LOG_DEBUG(#{
text => <<"Adding refers edges">>,
rsc_id => Id,
all => EmbeddedIds1,
new => New1,
delete => Old
}),
[ m_edge:insert(Id, refers, ObjId, [no_touch], Context) || ObjId <- New1 ],
[ m_edge:delete(Id, refers, ObjId, [no_touch], Context) || ObjId <- Old ],
ok.
find_ids(undefined, _RscCols, _Context) ->
[];
find_ids(Rsc, RscCols, Context) ->
Ids = maps:fold(
fun(K, V, Acc) ->
case lists:member(K, RscCols) of
true ->
% Table level references, ignore for programmatic
% reference tracking.
Acc;
false ->
case ids(K, V, Context) of
[] -> Acc;
VIds -> [ VIds | Acc ]
end
end
end,
[],
Rsc),
lists:usort(lists:flatten(Ids)).
ids(<<"managed_props">>, _, _Context) ->
[];
ids(K, V, Context) when is_integer(V); is_binary(V) ->
case is_rsc_prop(K) of
true ->
[ m_rsc:rid(V, Context) ];
false when is_binary(V) ->
case is_html_prop(K) of
true ->
embedded_media(V);
false ->
[]
end;
false ->
[]
end;
ids(K, #trans{} = V, _Context) ->
case is_html_prop(K) of
true ->
embedded_media(V);
false ->
[]
end;
ids(_K, [ V | _ ] = L, Context) when is_map(V) ->
lists:flatmap(
fun(B) ->
find_ids(B, [], Context)
end,
L);
ids(_K, V, Context) when is_map(V) ->
Ids = maps:fold(
fun
(K1, V1, Acc) when is_binary(K1) ->
[ ids(K1, V1, Context) | Acc ];
(_, _, Acc) ->
Acc
end,
[],
V),
lists:flatten(Ids);
ids(_, _, _Context) ->
[].
is_rsc_prop(<<"id">>) -> false;
is_rsc_prop(<<"creator_id">>) -> false;
is_rsc_prop(<<"modifier_id">>) -> false;
is_rsc_prop(<<"category_id">>) -> false;
is_rsc_prop(<<"content_group_id">>) -> false;
is_rsc_prop(<<"rsc_id">>) -> true;
is_rsc_prop(<<"rsc_id2">>) -> true;
is_rsc_prop(P) ->
case binary:longest_common_suffix([ P, <<"_id">> ]) of
3 -> true;
_ ->
case binary:longest_common_suffix([ P, <<"_id2">> ]) of
4 -> true;
_ -> false
end
end.
is_html_prop(<<"body">>) -> true;
is_html_prop(<<"body_", _/binary>>) -> true;
is_html_prop(P) ->
case binary:longest_common_suffix([ P, <<"_html">> ]) of
5 -> true;
_ -> false
end.
embedded_media(undefined) ->
[];
embedded_media(<<>>) ->
[];
embedded_media(Input) when is_binary(Input) ->
case re:run(Input, "\\<\\!-- z-media ([0-9]+) ", [global, {capture, all_but_first, binary}]) of
nomatch ->
[];
{match, L} ->
[ z_convert:to_integer(I) || [I] <- L ]
end;
embedded_media(#trans{ tr = Tr }) ->
lists:flatmap(
fun({_, B}) ->
embedded_media(B)
end,
Tr);
embedded_media(_) ->
[].