%% @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: %% %%
Result for $a = ",
%%      {ok, Text} = ephp:eval(Ctx, PHP).
%%      ]]>
%% %% 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_func/7, 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 ]). -type context_id() :: reference(). -type vars_id() :: reference(). -type output_id() :: reference(). -type funcs_id() :: reference(). -type classes_id() :: reference(). -type objects_id() :: reference(). -type consts_id() :: reference(). -type includes_id() :: reference(). -type shutdown_id() :: reference(). -type 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(), {ok, PathStr} = file:get_cwd(), ephp_stream:set_initial_path(list_to_binary(PathStr)), [ 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(), namespace(), PHPName :: binary(), module(), Fun :: atom(), PackArgs :: boolean(), 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, NS, PHPName, Module, Fun, PackArgs, Args) -> ephp_context:register_func(Ctx, NS, PHPName, Module, Fun, PackArgs, Args). -spec register_func(context(), PHPName :: binary(), module(), Fun :: atom(), PackArgs :: boolean(), 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), NS = proplists:get_value(namespace, Opts, []), register_func(Ctx, NS, Name, Module, Func, PackArgs, Args); (Func) -> Name = atom_to_binary(Func, utf8), register_func(Ctx, Name, Module, Func, false, undefined) end, Module:init_func()). -type eval_return() :: {ok, Result :: ephp_interpr:flow_status()} | {error, reason(), line(), File::binary(), error_level(), Data::any()}. -spec eval(context(), PHP :: string() | binary()) -> eval_return(). %% @doc eval PHP code in a context passed as params. eval(Context, PHP) -> eval(<<"-">>, Context, PHP). -spec eval(Filename :: binary(), context(), PHP :: string() | binary() | [term()]) -> eval_return(). %% @equiv eval/2 %% @doc adds the `Filename' to configure properly the `__FILE__' and `__DIR__' %% constants and evaluates the code for the third parameter. This parameter %% could contents a binary text with PHP code or a parsed PHP content. %% @end eval(Filename, Context, PHP) when is_binary(PHP) -> case catch ephp_parser:parse(Filename, PHP) of {error, ErrorName, Line, ErrorLevel, Data} -> ephp_error:handle_error(Context, {error, ErrorName, Line, Filename, ErrorLevel, Data}), {error, ErrorName, Line, Filename, ErrorLevel, Data}; Compiled -> eval(Filename, Context, Compiled) end; eval(Filename, Context, 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} -> maybe_die(fun() -> ephp_shutdown:shutdown(Context) end), {ok, Return}; die -> maybe_die(fun() -> ephp_shutdown:shutdown(Context) end), {ok, undefined}; {error, Reason, Index, Level, Data} -> File = ephp_context:get_active_file(Context), Error = {error, Reason, Index, File, Level, Data}, maybe_die(fun() -> ephp_error:handle_error(Context, Error) end), maybe_die(fun() -> ephp_shutdown:shutdown(Context) end), Error end. maybe_die(Fun) -> try Fun() catch throw:{ok, die} -> ok end. -spec opt_spec_list() -> [getopt:option_spec()]. opt_spec_list() -> [ {help, $h, "help", undefined, "This help information."}, {dir, $d, "dir", string, "Check directory for missing implemented PHP functions."}, {check, $l, undefined, string, "Check PHP code syntax for specific PHP file."}, {parse, $p, undefined, string, "Show parsing PHP code for specific PHP file."}, {info, $i, "info", undefined, "Show PHP info."}, {run, $r, undefined, string, "Run PHP code."}, {file, undefined, undefined, string, "PHP code to be run."} ]. -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:start(getopt), application:set_env(ephp, modules, ?MODULES), ok. usage(ErrorLevel) -> ScriptName = try escript:script_name() catch error:{badarg, []} -> "ephp" end, getopt:usage(opt_spec_list(), ScriptName), quit(ErrorLevel). -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(Args) -> case getopt:parse(opt_spec_list(), Args) of {ok, {Opts, _OtherArgs}} when Opts =/= [] -> main(Opts, Args); _Else -> usage(1) end. main([{dir, Dir}|_], _RawArgs) -> 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([{check, File}|_], _RawArgs) -> maybe_badmatch(fun() -> ephp_parser:file(File), io:format("No syntax errors detected in: ~s~n", [File]), quit(0) end, File); main([{parse, File}|_], _RawArgs) -> maybe_badmatch(fun() -> io:format("parsing =>~n~p~n---~n", [ephp_parser:file(File)]), quit(0) end, File); main([info|_], _RawArgs) -> start(), Content = <<">, ephp_config:start_link(?PHP_INI_FILE), {ok, Ctx} = context_new(<<"-">>), register_superglobals(Ctx, ["-"]), {ok, _} = eval(<<"-">>, Ctx, Content), output_and_close(Ctx), quit(0); main([{run, Code}|_], _RawArgs) -> start(), Content = list_to_binary(">), register_superglobals(Ctx, ["-"]), case eval(<<"-">>, Ctx, Content) of {ok, _Return} -> output_and_close(Ctx), quit(0); {error, _Reason, _Index, _File, _Level, _Data} -> quit(1) end; main([help|_], _) -> usage(0); main([{file, 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} -> output_and_close(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(_, _) -> usage(1). maybe_badmatch(Fun, File) -> try Fun() catch error:{badmatch, {error, enoent}} -> io:format("Could not open input file: ~s~n", [File]), quit(1) end. -spec output_and_close(context()) -> ok. output_and_close(Ctx) -> Result = ephp_context:get_output(Ctx), io:format("~s", [Result]), ephp_context:destroy_all(Ctx), ok. -ifndef(TEST). -spec quit(non_neg_integer() | abort | [char()]) -> no_return(). %% @hidden quit(Code) -> erlang:halt(Code). -else. -spec quit(non_neg_integer() | abort | [char()]) -> non_neg_integer() | abort | [char()]. %% @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) when is_list(Filename) -> 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">>, list_to_binary(Filename)} ], ephp_context:set(Ctx, #variable{name = <<"_SERVER">>}, ephp_array:from_list(Server)), ephp_context:set(Ctx, #variable{name = <<"argc">>}, ephp_array:size(ArrayArgs)), ephp_context:set(Ctx, #variable{name = <<"argv">>}, ArrayArgs), 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}.