%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2009-2026 Marc Worrell
%% @doc Handle parameter validation of a request. Checks for the presence
%% of z_v elements containing validation information.
%% @end
%% Copyright 2009-2026 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(z_validation).
-export([
validate_query_args/1,
report_errors/2,
rename_args/1,
get_q/2
]).
-include_lib("zotonic.hrl").
%% @doc Rename validator arguments to names that are compatible with the LiveValidation plugin.
-spec rename_args(proplists:proplist()) -> proplists:proplist().
rename_args(Args) ->
rename_args(Args, []).
rename_args([], Acc) ->
Acc;
rename_args([{failure_message, Msg}|T], Acc) ->
rename_args(T, [{failureMessage, Msg}|Acc]);
rename_args([{valid_message, Msg}|T], Acc) ->
rename_args(T, [{validMessage, Msg}|Acc]);
rename_args([{not_a_number_message, Msg}|T], Acc) ->
rename_args(T, [{notANumberMessage, Msg}|Acc]);
rename_args([{not_an_integer_message, Msg}|T], Acc) ->
rename_args(T, [{notAnIntegerMessage, Msg}|Acc]);
rename_args([{wrong_number_message, Msg}|T], Acc) ->
rename_args(T, [{wrongNumberMessage, Msg}|Acc]);
rename_args([{too_low_message, Msg}|T], Acc) ->
rename_args(T, [{tooLowMessage, Msg}|Acc]);
rename_args([{too_high_message, Msg}|T], Acc) ->
rename_args(T, [{tooHighMessage, Msg}|Acc]);
rename_args([{too_short_message, Msg}|T], Acc) ->
rename_args(T, [{tooShortMessage, Msg}|Acc]);
rename_args([{too_long_message, Msg}|T], Acc) ->
rename_args(T, [{tooLongMessage, Msg}|Acc]);
rename_args([{wrong_length_message, Msg}|T], Acc) ->
rename_args(T, [{wrongLengthMessage, Msg}|Acc]);
rename_args([{partial_match, Msg}|T], Acc) ->
rename_args(T, [{partialMatch, Msg}|Acc]);
rename_args([{case_sensitive, Msg}|T], Acc) ->
rename_args(T, [{caseSensitive, Msg}|Acc]);
rename_args([{allow_null, Msg}|T], Acc) ->
rename_args(T, [{allowNull, Msg}|Acc]);
rename_args([{only_on_submit, Value}|T], Acc) ->
rename_args(T, [{onlyOnSubmit, Value}|Acc]);
rename_args([{only_on_blur, Value}|T], Acc) ->
rename_args(T, [{onlyOnBlur, Value}|Acc]);
rename_args([{message_after, Value}|T], Acc) ->
rename_args(T, [{insertAfterWhatNode, Value}|Acc]);
rename_args([H|T], Acc) ->
rename_args(T, [H|Acc]).
%% @todo Translate unique id-names to base names (after validation) #name -> fghw-name in postback+qs -> name in validated result
-spec validate_query_args(z:context()) -> {ok, z:context()} | {error, z:context()}.
%% @doc Checks for z_v arguments, performs enclosed checks and adds the validated terms to the q_validated list.
%% Errors are reported back to the user agent
validate_query_args(Context) ->
case z_context:get(q_validated, Context) of
undefined ->
QArgs = z_context:get_q_all(Context),
case z_notifier:foldl(#validate_query_args{}, {ok, QArgs}, Context) of
{ok, QArgsValidated} ->
ContextNotifier = z_context:set_q_all(QArgsValidated, Context),
Validations = z_context:get_q_all(<<"z_v">>, ContextNotifier),
{Validated,Context1} = lists:foldl(
fun(X, {Acc, Ctx}) ->
{XV, Ctx1} = validate(X,Ctx),
{[XV|Acc], Ctx1}
end,
{[], ContextNotifier},
Validations),
% format is like: [{<<"email">>,{ok,<<"me@example.com">>}}]
% Grep all errors, make scripts for the context var
% Move all ok values to the q_validated dict
IsError = fun
({_Id, {error, _, _}}) -> true;
(_X) -> false
end,
GetValue = fun
({Id, {ok, Value}}) when is_tuple(Value) -> {Id, Value};
({Id, {ok, Value}}) when is_list(Value) -> {Id, iolist_to_binary(Value)};
({Id, {ok, Value}}) when is_binary(Value) -> {Id, Value}
end,
{Errors,Values} = lists:partition(IsError, Validated),
QsValidated = lists:map(GetValue, Values),
Context2 = z_context:set(q_validated, QsValidated, Context1),
Context3 = report_errors(Errors, Context2),
case Errors of
[] -> {ok, Context3};
_ -> {error, Context3}
end;
{error, Reason} ->
?LOG_ERROR(#{
text => <<"Error validating query args">>,
in => zotonic_core,
result => error,
reason => Reason,
qargs => QArgs
}),
% TODO: add a generic validation error
{error, Context}
end;
_ ->
{ok, Context}
end.
%% @doc Add all errors as javascript message to the request result.
report_errors([], Context) ->
Context;
report_errors([{_Id, {error, _ErrId, {script, Script}}}|T], Context) ->
Context1 = z_render:add_script(Script, Context),
report_errors(T, Context1);
report_errors([{_Id, {error, ErrId, Error}}|T], Context) ->
Script = [<<"z_validation_error('">>, ErrId, <<"', \"">>, z_utils:js_escape(z_convert:to_list(Error)),<<"\");\n">>],
Context1 = z_render:add_script(Script, Context),
report_errors(T, Context1).
%% @doc Perform all validations
validate(Val, Context) ->
{Name,Pickled} = split_name_pickled(Val),
{Id,Name,Validations} = z_crypto:depickle(Pickled, Context),
Value = case [ V || V <- z_context:get_q_all(Name, Context), V =/= [], V =/= <<>> ] of
[A] -> A;
Vs -> Vs
end,
%% Fold all validations, stop on error
ValidateF = fun
(_Validation,{{error, _, _}=Error, Ctx}) -> {Error, Ctx};
(Validation,{{ok, V}, Ctx}) ->
case Validation of
{Type,Module,Args} -> Module:validate(Type, Id, V, Args, Ctx);
{Type,Module} -> Module:validate(Type, Id, V, [], Ctx)
end
end,
{Validated, Context1} = lists:foldl(ValidateF, {{ok,Value}, Context}, Validations),
{{Name, Validated}, Context1}.
split_name_pickled(Val) ->
[Pickled|Name] = lists:reverse(binary:split(Val, <<":">>, [global])),
Name1 = iolist_to_binary(lists:join($,, lists:reverse(Name))),
{Name1, Pickled}.
%% @doc Simple utility function to get the 'q' value of an argument. When the argument has a generated unique prefix then
%% the prefix is stripped.
get_q(Name, Context) ->
case z_context:get_q(Name, Context) of
undefined ->
case string:tokens(Name, "-") of
[_Prefix, Name1] -> z_context:get_q(Name1, Context);
_ -> undefined
end;
Value -> Value
end.