Current section
Files
Jump to
Current section
Files
src/ephp.erl
%% @doc The ephp module is in charge to give an easy way to create the context
%% and other actions to be performed from the project where ephp is
%% included mainly to run the PHP code.
%%
%% The easy way to use is:
%%
%% <pre lang="erlang"><![CDATA[
%% {ok, Ctx} = ephp:context_new(),
%% PHP = "<? $a = 5 * 23; ?>Result for $a = <?=$a?>",
%% {ok, Text} = ephp:eval(Ctx, PHP).
%% ]]></pre>
%%
%% This module is in use for PHP script, contains the `main/1' function
%% to run from console.
%% @end
-module(ephp).
-author('manuel@altenwald.com').
-compile([warnings_as_errors]).
-export([
context_new/0,
context_new/1,
register_var/3,
register_func/6,
register_module/2,
eval/2,
eval/3,
start/0,
main/1, %% for escriptize
register_superglobals/2
]).
-ifdef(TEST).
-export([stop_cover/0]).
-endif.
-export_type([
context_id/0,
vars_id/0,
output_id/0,
funcs_id/0,
classes_id/0,
objects_id/0,
consts_id/0,
includes_id/0,
shutdown_id/0,
errors_id/0
]).
-opaque context_id() :: reference().
-opaque vars_id() :: reference().
-opaque output_id() :: reference().
-opaque funcs_id() :: reference().
-opaque classes_id() :: reference().
-opaque objects_id() :: reference().
-opaque consts_id() :: reference().
-opaque includes_id() :: reference().
-opaque shutdown_id() :: reference().
-opaque errors_id() :: reference().
-include("ephp.hrl").
-spec context_new() -> {ok, context()}.
%% @doc creates a new context using `-' as script name.
context_new() ->
context_new(<<"-">>).
-spec context_new(Filename :: binary()) -> {ok, context()}.
%% @doc creates a new context passing `Filename' as param.
context_new(Filename) ->
Modules = application:get_env(ephp, modules, []),
{ok, Ctx} = ephp_context:start_link(),
[ register_module(Ctx, Module) || Module <- Modules ],
ephp_context:set_active_file(Ctx, Filename),
{ok, Ctx}.
-type values() :: integer() | binary() | float() | ephp_array().
-spec register_var(Ctx :: context(), Var :: binary(), Value :: values()) ->
ok | {error, reason()}.
%% @doc register a variable with a value in the context passed as param.
register_var(Ctx, Var, Value) when
is_reference(Ctx) andalso
(is_integer(Value) orelse
is_float(Value) orelse
is_binary(Value) orelse
?IS_ARRAY(Value)) ->
ephp_context:set(Ctx, #variable{name=Var}, Value);
register_var(_Ctx, _Var, _Value) ->
{error, badarg}.
-spec register_func(context(), PHPName :: binary(), module(), Fun :: atom(),
PackArgs :: boolean(),
Args :: ephp_func:validation_args()
) -> ok | {error, reason()}.
%% @doc register function in a context passed as a param. The params to be
%% sent are the PHP function name, the module, function name and args
%% in the Erlang side.
%%
%% Other param is about if the params should be packed or not. That means
%% the args could be sent one by one or as only one in an array.
%% @end
register_func(Ctx, PHPName, Module, Fun, PackArgs, Args) ->
ephp_context:register_func(Ctx, PHPName, Module, Fun, PackArgs, Args).
-spec register_module(context(), module()) -> ok.
%% @doc register a module.
%% @see ephp_func
register_module(Ctx, Module) ->
ephp_config:module_init(Module),
case erlang:function_exported(Module, handle_error, 3) of
true -> ephp_error:add_message_handler(Ctx, Module);
false -> ok
end,
ClassRef = ephp_context:get_classes(Ctx),
case erlang:function_exported(Module, get_classes, 0) of
true ->
Classes = Module:get_classes(),
ephp_class:register_classes(ClassRef, Ctx, Classes);
false ->
ok
end,
lists:foreach(fun
({Func, Opts}) ->
PackArgs = proplists:get_value(pack_args, Opts, false),
Name = proplists:get_value(alias, Opts, atom_to_binary(Func, utf8)),
Args = proplists:get_value(args, Opts),
register_func(Ctx, Name, Module, Func, PackArgs, Args);
(Func) ->
Name = atom_to_binary(Func, utf8),
register_func(Ctx, Name, Module, Func, false, undefined)
end, Module:init_func()).
-spec eval(context(), PHP :: string() | binary()) ->
{ok, Result :: binary()} | {error, Reason :: reason()} |
{error,{Code::binary(), Line::integer(), Col::integer()}}.
%% @doc eval PHP code in a context passed as params.
eval(Context, PHP) ->
eval(<<"-">>, Context, PHP).
-spec eval(Filename :: binary(), context(), PHP :: string() | binary()) ->
{ok, Result :: binary()} |
{error, reason(), line(), File::binary(), error_level(), Data::any()}.
%% @equiv eval/2
%% @doc adds the `Filename' to configure properly the `__FILE__' and `__DIR__'
%% constants.
%% @end
eval(Filename, Context, PHP) ->
case catch ephp_parser:parse(PHP) of
{error, eparse, Line, _ErrorLevel, Data} ->
ephp_error:handle_error(Context, {error, eparse, Line,
Filename, ?E_PARSE, Data}),
{error, eparse, Line, Filename, ?E_PARSE, Data};
Compiled ->
Cover = ephp_cover:get_config(),
ok = ephp_cover:init_file(Cover, Filename, Compiled),
case catch ephp_interpr:process(Context, Compiled, Cover) of
{ok, Return} ->
try
ephp_shutdown:shutdown(Context)
catch
throw:{ok, die} -> ok
end,
{ok, Return};
die ->
try
ephp_shutdown:shutdown(Context)
catch
throw:{ok, die} -> ok
end,
{ok, undefined};
{error, Reason, Index, Level, Data} ->
File = ephp_context:get_active_file(Context),
Error = {error, Reason, Index, File, Level, Data},
try
ephp_error:handle_error(Context, Error)
catch
throw:{ok, die} -> ok
end,
ephp_shutdown:shutdown(Context),
Error
end
end.
-spec start() -> ok.
%% @doc function to ensure all of the applications and the base configuration
%% is set properly before use ephp.
%% @end
start() ->
application:start(ezic),
application:start(zucchini),
application:start(ephp),
application:set_env(ephp, modules, ?MODULES),
ok.
-spec main(Args :: [string()]) -> integer().
%% @doc called from script passing the name of the filename to be run or
%% nothing to show the help message.
%% @end
main(["-d", Dir]) ->
start(),
{Funcs, TotalOk, Total} = parse_subdirs(Dir),
{ok, Context} = context_new(),
FTotal = length(Funcs),
FTotalOk = lists:foldl(fun(Func, FuncOK) ->
case ephp_context:is_defined_function(Context, Func) of
true ->
io:format("~s: OK~n", [Func]),
FuncOK + 1;
false ->
io:format("~s: NOT FOUND~n", [Func]),
FuncOK
end
end, 0, Funcs),
io:format("~nFunctions FOUND=~p Total=~p (~p%) / Code OK=~p Total=~p (~p%)~n",
[FTotalOk, FTotal, ephp_data:ceiling(FTotalOk * 100 / FTotal),
TotalOk, Total, ephp_data:ceiling(TotalOk * 100 / Total)]),
quit(0);
main(["-l", File]) ->
try
ephp_parser:file(File),
io:format("No syntax errors detected in: ~s~n", [File]),
quit(0)
catch
error:{badmatch, {error, enoent}} ->
io:format("Could not open input file: ~s~n", [File]),
quit(1)
end;
main(["-p", File]) ->
try
io:format("parsing =>~n~p~n---~n", [ephp_parser:file(File)]),
quit(0)
catch
error:{badmatch, {error, enoent}} ->
io:format("Could not open input file: ~s~n", [File]),
quit(1)
end;
main([Filename|_] = RawArgs) ->
start(),
case file:read_file(Filename) of
{ok, Content} ->
start_profiling(),
start_cover(),
AbsFilename = list_to_binary(filename:absname(Filename)),
ephp_config:start_link(?PHP_INI_FILE),
{ok, Ctx} = context_new(AbsFilename),
register_superglobals(Ctx, RawArgs),
case eval(AbsFilename, Ctx, Content) of
{ok, _Return} ->
Result = ephp_context:get_output(Ctx),
io:format("~s", [Result]),
ephp_context:destroy_all(Ctx),
stop_profiling(),
stop_cover(),
quit(0);
{error, _Reason, _Index, _File, _Level, _Data} ->
stop_profiling(),
quit(1)
end;
{error, enoent} ->
io:format("File not found: ~s~n", [Filename]),
quit(2);
{error, Reason} ->
io:format("Error: ~p~n", [Reason]),
quit(3)
end;
main(_) ->
io:format("Usage: ephp <file.php>~n", []),
quit(1).
-ifndef(TEST).
-spec quit(integer()) -> no_return().
%% @hidden
quit(Code) ->
erlang:halt(Code).
-else.
-spec quit(integer()) -> integer().
%% @hidden
quit(Code) ->
Code.
-endif.
-ifdef(PROFILING).
-spec start_profiling() -> ok.
%% @doc starts the profiling functions to gather information using eprof.
start_profiling() ->
eprof:start(),
eprof:start_profiling([self()]),
ok.
-spec stop_profiling() -> ok.
%% @doc stops the profiling system and send the analysis to the standard
%% output.
%% @end
stop_profiling() ->
eprof:stop_profiling(),
eprof:analyze(total),
ok.
-else.
-spec start_profiling() -> ok.
%% @hidden
start_profiling() ->
ok.
-spec stop_profiling() -> ok.
%% @hidden
stop_profiling() ->
ok.
-endif.
-spec start_cover() -> ok.
%% @doc starts the cover system.
start_cover() ->
ephp_cover:start_link().
-spec stop_cover() -> ok.
%% @doc stops the cover system.
stop_cover() ->
case ephp_cover:get_config() of
true -> ephp_cover:dump();
false -> ok
end.
-spec register_superglobals(context(), [string()]) -> ok.
%% @doc register the superglobals variables in the context passed as param.
register_superglobals(Ctx, [Filename|_] = RawArgs) ->
Args = [ ephp_data:to_bin(RawArg) || RawArg <- RawArgs ],
ArrayArgs = ephp_array:from_list(Args),
Server = [
%% TODO: add the rest of _SERVER vars
{<<"argc">>, ephp_array:size(ArrayArgs)},
{<<"argv">>, ArrayArgs},
{<<"PHP_SELF">>, ephp_data:to_bin(Filename)}
],
ephp_context:set(Ctx, #variable{name = <<"_SERVER">>},
ephp_array:from_list(Server)),
SuperGlobals = [
<<"_GET">>,
<<"_POST">>,
<<"_FILES">>,
<<"_COOKIE">>,
<<"_SESSION">>,
<<"_REQUEST">>,
<<"_ENV">>
],
lists:foreach(fun(Global) ->
ephp_context:set(Ctx, #variable{name = Global}, ephp_array:new())
end, SuperGlobals),
ok.
-spec get_use_funcs([tuple()], [binary()]) -> [binary()].
%% @doc get the name of the functions used in the code (no methods).
get_use_funcs([#call{type = normal, name = Name} = Tuple|Tuples], R)
when is_binary(Name) ->
get_use_funcs(erlang:tuple_to_list(Tuple), []) ++
get_use_funcs(Tuples, [ephp_string:to_lower(Name)|R]);
get_use_funcs([{object,_,_}|Tuples], R) ->
get_use_funcs(Tuples, R);
get_use_funcs([Tuple|Tuples], R) when is_tuple(Tuple) ->
get_use_funcs(erlang:tuple_to_list(Tuple), []) ++
get_use_funcs(Tuples, R);
get_use_funcs([List|Rest], R) when is_list(List) ->
get_use_funcs(List, []) ++ get_use_funcs(Rest, R);
get_use_funcs([_Other|Rest], R) ->
get_use_funcs(Rest, R);
get_use_funcs([], R) ->
ordsets:from_list(R).
-spec get_defined_funcs([tuple()], [binary()]) -> [binary()].
%% @doc get the name of the functions used in the code (no methods).
get_defined_funcs([#function{name = Name} = Tuple|Tuples], R) ->
get_defined_funcs(erlang:tuple_to_list(Tuple), []) ++
get_defined_funcs(Tuples, [ephp_string:to_lower(Name)|R]);
get_defined_funcs([Tuple|Tuples], R) when is_tuple(Tuple) ->
get_defined_funcs(erlang:tuple_to_list(Tuple), []) ++
get_defined_funcs(Tuples, R);
get_defined_funcs([List|Rest], R) when is_list(List) ->
get_defined_funcs(List, []) ++ get_defined_funcs(Rest, R);
get_defined_funcs([_Other|Rest], R) ->
get_defined_funcs(Rest, R);
get_defined_funcs([], R) ->
ordsets:from_list(R).
-spec parse_subdirs(string()) -> {[binary()], pos_integer(), pos_integer()}.
%% @doc given a directory it retrieves the functions in use for all of the
%% files of that directory, the number of files ok (parsing correctly)
%% and the number of files in the directory (with the php extension).
%% @end
parse_subdirs(Dir) ->
Files = filelib:wildcard(Dir ++ "/**/*.php"),
Total = length(Files),
{Funcs, DefFuncs, TotalOk, _} =
lists:foldl(fun(File, {Funcs, DefFuncs, I, J}) ->
Porc = ephp_data:ceiling(I * 100 / Total),
io:format("(~3..0b/~b) ~2..0b% file => ~s", [I + 1, Total, Porc, File]),
try
P = ephp_parser:file(File),
io:format(": OK~n", []),
{ordsets:from_list(get_use_funcs(P, Funcs)),
ordsets:from_list(get_defined_funcs(P, DefFuncs)),
I + 1, J + 1}
catch _:_ ->
io:format(": FAIL~n", []),
{Funcs, DefFuncs, I, J + 1}
end
end, {[], [], 0, 0}, Files),
{Funcs -- DefFuncs, TotalOk, Total}.