Packages
ejabberd
24.2.6
26.4.0
26.3.0
26.2.0
26.1.0
25.10.0
25.8.0
25.7.0
25.4.0
25.3.0
24.12.0
24.10.0
24.7.0
24.6.0
24.2.6
23.10.0
23.4.0
23.1.0
22.10.0
22.5.0
21.12.0
21.7.0
21.4.0
21.1.0
20.12.0
20.7.0
20.4.0
20.3.0
20.2.0
20.1.0
19.9.1
19.9.0
19.8.0
19.5.0
19.2.0
18.12.1
18.12.0
18.6.0
18.4.0
18.3.0
18.1.0
17.11.0
17.9.0
17.6.0
17.3.0
17.1.0
16.12.0-beta1
16.9.0
16.8.0
16.6.2
16.6.1
16.6.0
16.4.1
16.4.0
16.3.0
16.2.0
16.1.0-beta1
Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP, MQTT, SIP Server)
Current section
Files
Jump to
Current section
Files
src/ext_mod.erl
%%%----------------------------------------------------------------------
%%% File : ext_mod.erl
%%% Author : Christophe Romain <christophe.romain@process-one.net>
%%% Purpose : external modules management
%%% Created : 19 Feb 2015 by Christophe Romain <christophe.romain@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2006-2024 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License along
%%% with this program; if not, write to the Free Software Foundation, Inc.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
-module(ext_mod).
-behaviour(gen_server).
-author("Christophe Romain <christophe.romain@process-one.net>").
-export([start_link/0, update/0, check/1,
available_command/0, available/0, available/1,
installed_command/0, installed/0, installed/1,
install/1, uninstall/1, upgrade/0, upgrade/1, add_paths/0,
add_sources/1, add_sources/2, del_sources/1, modules_dir/0,
install_contrib_modules/2,
config_dir/0, get_commands_spec/0]).
-export([modules_configs/0, module_ebin_dir/1]).
-export([compile_erlang_file/2, compile_elixir_file/2]).
-export([web_menu_node/3, web_page_node/5, get_page/3]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-include("ejabberd_commands.hrl").
-include("ejabberd_web_admin.hrl").
-include("logger.hrl").
-include("translate.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-define(REPOS, "git@github.com:processone/ejabberd-contrib.git").
-record(state, {}).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
init([]) ->
process_flag(trap_exit, true),
add_paths(),
application:start(inets),
inets:start(httpc, [{profile, ext_mod}]),
ejabberd_commands:register_commands(get_commands_spec()),
ejabberd_hooks:add(webadmin_menu_node, ?MODULE, web_menu_node, 50),
ejabberd_hooks:add(webadmin_page_node, ?MODULE, web_page_node, 50),
{ok, #state{}}.
add_paths() ->
[code:add_pathsz([module_ebin_dir(Module)|module_deps_dirs(Module)])
|| {Module, _} <- installed()].
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ejabberd_hooks:delete(webadmin_menu_node, ?MODULE, web_menu_node, 50),
ejabberd_hooks:delete(webadmin_page_node, ?MODULE, web_page_node, 50),
ejabberd_commands:unregister_commands(get_commands_spec()).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% -- ejabberd commands
get_commands_spec() ->
[#ejabberd_commands{name = modules_update_specs,
tags = [modules],
desc = "Update the module source code from Git",
longdesc = "A connection to Internet is required",
module = ?MODULE, function = update,
args = [],
result = {res, rescode}},
#ejabberd_commands{name = modules_available,
tags = [modules],
desc = "List the contributed modules available to install",
module = ?MODULE, function = available_command,
result_desc = "List of tuples with module name and description",
result_example = [{mod_cron, "Execute scheduled commands"},
{mod_rest, "ReST frontend"}],
args = [],
result = {modules, {list,
{module, {tuple,
[{name, atom},
{summary, string}]}}}}},
#ejabberd_commands{name = modules_installed,
tags = [modules],
desc = "List the contributed modules already installed",
module = ?MODULE, function = installed_command,
result_desc = "List of tuples with module name and description",
result_example = [{mod_cron, "Execute scheduled commands"},
{mod_rest, "ReST frontend"}],
args = [],
result = {modules, {list,
{module, {tuple,
[{name, atom},
{summary, string}]}}}}},
#ejabberd_commands{name = module_install,
tags = [modules],
desc = "Compile, install and start an available contributed module",
module = ?MODULE, function = install,
args_desc = ["Module name"],
args_example = [<<"mod_rest">>],
args = [{module, binary}],
result = {res, rescode}},
#ejabberd_commands{name = module_uninstall,
tags = [modules],
desc = "Uninstall a contributed module",
module = ?MODULE, function = uninstall,
args_desc = ["Module name"],
args_example = [<<"mod_rest">>],
args = [{module, binary}],
result = {res, rescode}},
#ejabberd_commands{name = module_upgrade,
tags = [modules],
desc = "Upgrade the running code of an installed module",
longdesc = "In practice, this uninstalls and installs the module",
module = ?MODULE, function = upgrade,
args_desc = ["Module name"],
args_example = [<<"mod_rest">>],
args = [{module, binary}],
result = {res, rescode}},
#ejabberd_commands{name = module_check,
tags = [modules],
desc = "Check the contributed module repository compliance",
module = ?MODULE, function = check,
args_desc = ["Module name"],
args_example = [<<"mod_rest">>],
args = [{module, binary}],
result = {res, rescode}}
].
%% -- public modules functions
update() ->
Contrib = maps:put(?REPOS, [], maps:new()),
Jungles = lists:foldl(fun({Package, Spec}, Acc) ->
Repo = proplists:get_value(url, Spec, ""),
Mods = maps:get(Repo, Acc, []),
maps:put(Repo, [Package|Mods], Acc)
end, Contrib, modules_spec(sources_dir(), "*/*")),
Repos = maps:fold(fun(Repo, _Mods, Acc) ->
Update = add_sources(Repo),
?INFO_MSG("Update packages from repo ~ts: ~p", [Repo, Update]),
case Update of
ok -> Acc;
Error -> [{repository, Repo, Error}|Acc]
end
end, [], Jungles),
Res = lists:foldl(fun({Package, Spec}, Acc) ->
Repo = proplists:get_value(url, Spec, ""),
Update = add_sources(Package, Repo),
?INFO_MSG("Update package ~ts: ~p", [Package, Update]),
case Update of
ok -> Acc;
Error -> [{Package, Repo, Error}|Acc]
end
end, Repos, modules_spec(sources_dir(), "*")),
case Res of
[] -> ok;
[Error|_] -> Error
end.
available() ->
Jungle = modules_spec(sources_dir(), "*/*"),
Standalone = modules_spec(sources_dir(), "*"),
lists:keysort(1,
lists:foldl(fun({Key, Val}, Acc) ->
lists:keystore(Key, 1, Acc, {Key, Val})
end, Jungle, Standalone)).
available(Module) when is_atom(Module) ->
available(misc:atom_to_binary(Module));
available(Package) when is_binary(Package) ->
Available = [misc:atom_to_binary(K) || K<-proplists:get_keys(available())],
lists:member(Package, Available).
available_command() ->
[short_spec(Item) || Item <- available()].
installed() ->
modules_spec(modules_dir(), "*").
installed(Module) when is_atom(Module) ->
installed(misc:atom_to_binary(Module));
installed(Package) when is_binary(Package) ->
Installed = [misc:atom_to_binary(K) || K<-proplists:get_keys(installed())],
lists:member(Package, Installed).
installed_command() ->
[short_spec(Item) || Item <- installed()].
install(Module) when is_atom(Module) ->
install(misc:atom_to_binary(Module), undefined);
install(Package) when is_binary(Package) ->
install(Package, undefined).
install(Package, Config) when is_binary(Package) ->
Spec = [S || {Mod, S} <- available(), misc:atom_to_binary(Mod)==Package],
case {Spec, installed(Package), is_contrib_allowed(Config)} of
{_, _, false} ->
{error, not_allowed};
{[], _, _} ->
{error, not_available};
{_, true, _} ->
{error, conflict};
{[Attrs], _, _} ->
Module = misc:binary_to_atom(Package),
case compile_and_install(Module, Attrs, Config) of
ok ->
code:add_pathsz([module_ebin_dir(Module)|module_deps_dirs(Module)]),
ejabberd_config_reload(Config),
copy_commit_json(Package, Attrs),
ModuleRuntime = get_runtime_module_name(Module),
case erlang:function_exported(ModuleRuntime, post_install, 0) of
true -> ModuleRuntime:post_install();
_ -> ok
end;
Error ->
delete_path(module_lib_dir(Module)),
Error
end
end.
ejabberd_config_reload(Config) when is_list(Config) ->
%% Don't reload config when ejabberd is starting
%% because it will be reloaded after installing
%% all the external modules from install_contrib_modules
ok;
ejabberd_config_reload(undefined) ->
ejabberd_config:reload().
uninstall(Module) when is_atom(Module) ->
uninstall(misc:atom_to_binary(Module));
uninstall(Package) when is_binary(Package) ->
case installed(Package) of
true ->
Module = misc:binary_to_atom(Package),
ModuleRuntime = get_runtime_module_name(Module),
case erlang:function_exported(ModuleRuntime, pre_uninstall, 0) of
true -> ModuleRuntime:pre_uninstall();
_ -> ok
end,
[catch gen_mod:stop_module(Host, ModuleRuntime)
|| Host <- ejabberd_option:hosts()],
code:purge(ModuleRuntime),
code:delete(ModuleRuntime),
[code:del_path(PathDelete) || PathDelete <- [module_ebin_dir(Module)|module_deps_dirs(Module)]],
delete_path(module_lib_dir(Module)),
ejabberd_config:reload();
false ->
{error, not_installed}
end.
upgrade() ->
[{Package, upgrade(Package)} || {Package, _Spec} <- installed()].
upgrade(Module) when is_atom(Module) ->
upgrade(misc:atom_to_binary(Module));
upgrade(Package) when is_binary(Package) ->
uninstall(Package),
install(Package).
add_sources(Path) when is_list(Path) ->
add_sources(iolist_to_binary(module_name(Path)), Path).
add_sources(_, "") ->
{error, no_url};
add_sources(Module, Path) when is_atom(Module), is_list(Path) ->
add_sources(misc:atom_to_binary(Module), Path);
add_sources(Package, Path) when is_binary(Package), is_list(Path) ->
DestDir = sources_dir(),
RepDir = filename:join(DestDir, module_name(Path)),
delete_path(RepDir, binary_to_list(Package)),
case filelib:ensure_dir(RepDir) of
ok ->
case {string:left(Path, 4), string:right(Path, 2)} of
{"http", "ip"} -> extract(zip, geturl(Path), DestDir);
{"http", "gz"} -> extract(tar, geturl(Path), DestDir);
{"http", _} -> extract_url(Path, DestDir);
{"git@", _} -> extract_github_master(Path, DestDir);
{_, "ip"} -> extract(zip, Path, DestDir);
{_, "gz"} -> extract(tar, Path, DestDir);
_ -> {error, unsupported_source}
end;
Error ->
Error
end.
del_sources(Module) when is_atom(Module) ->
del_sources(misc:atom_to_binary(Module));
del_sources(Package) when is_binary(Package) ->
case uninstall(Package) of
ok ->
SrcDir = module_src_dir(misc:binary_to_atom(Package)),
delete_path(SrcDir);
Error ->
Error
end.
check(Module) when is_atom(Module) ->
check(misc:atom_to_binary(Module));
check(Package) when is_binary(Package) ->
case {available(Package), installed(Package)} of
{false, _} ->
{error, not_available};
{_, false} ->
Status = install(Package),
uninstall(Package),
case Status of
ok -> check_sources(misc:binary_to_atom(Package));
Error -> Error
end;
_ ->
check_sources(misc:binary_to_atom(Package))
end.
%% -- archives and variables functions
geturl(Url) ->
case getenv("PROXY_SERVER", "", ":") of
[H, Port] ->
httpc:set_options([{proxy, {{H, list_to_integer(Port)}, []}}], ext_mod);
[H] ->
httpc:set_options([{proxy, {{H, 8080}, []}}], ext_mod);
_ ->
ok
end,
User = case getenv("PROXY_USER", "", ":") of
[U, Pass] -> [{proxy_auth, {U, Pass}}];
_ -> []
end,
UA = {"User-Agent", "ejabberd/ext_mod"},
case httpc:request(get, {Url, [UA]}, User, [{body_format, binary}], ext_mod) of
{ok, {{_, 200, _}, Headers, Response}} ->
{ok, Headers, Response};
{ok, {{_, 403, Reason}, _Headers, _Response}} ->
{error, Reason};
{ok, {{_, Code, _}, _Headers, Response}} ->
{error, {Code, Response}};
{error, Reason} ->
{error, Reason}
end.
getenv(Env) ->
getenv(Env, "").
getenv(Env, Default) ->
case os:getenv(Env) of
false -> Default;
"" -> Default;
Value -> Value
end.
getenv(Env, Default, Separator) ->
string:tokens(getenv(Env, Default), Separator).
extract(zip, {ok, _, Body}, DestDir) ->
extract(zip, iolist_to_binary(Body), DestDir);
extract(tar, {ok, _, Body}, DestDir) ->
extract(tar, {binary, iolist_to_binary(Body)}, DestDir);
extract(_, {error, Reason}, _) ->
{error, Reason};
extract(zip, Zip, DestDir) ->
case zip:extract(Zip, [{cwd, DestDir}]) of
{ok, _} -> ok;
Error -> Error
end;
extract(tar, Tar, DestDir) ->
erl_tar:extract(Tar, [compressed, {cwd, DestDir}]).
extract_url(Path, DestDir) ->
hd([extract_github_master(Path, DestDir) || string:str(Path, "github") > 0]
++[{error, unsupported_source}]).
extract_github_master(Repos, DestDir) ->
Base = case string:tokens(Repos, ":") of
["git@github.com", T1] -> "https://github.com/"++T1;
_ -> Repos
end,
Url = case lists:reverse(Base) of
[$t,$i,$g,$.|T2] -> lists:reverse(T2);
_ -> Base
end,
case extract(zip, geturl(Url++"/archive/master.zip"), DestDir) of
ok ->
RepDir = filename:join(DestDir, module_name(Repos)),
RepDirSpec = filename:join(DestDir, module_spec_name(RepDir)),
file:rename(RepDir++"-master", RepDirSpec),
maybe_write_commit_json(Url, RepDirSpec);
Error ->
Error
end.
copy(From, To) ->
case filelib:is_dir(From) of
true ->
Copy = fun(F) ->
SubFrom = filename:join(From, F),
SubTo = filename:join(To, F),
copy(SubFrom, SubTo)
end,
lists:foldl(fun(ok, ok) -> ok;
(ok, Error) -> Error;
(Error, _) -> Error
end, ok,
[Copy(filename:basename(X)) || X<-filelib:wildcard(From++"/*")]);
false ->
filelib:ensure_dir(To),
case file:copy(From, To) of
{ok, _} -> ok;
Error -> Error
end
end.
delete_path(Path) ->
case filelib:is_dir(Path) of
true ->
[delete_path(SubPath) || SubPath <- filelib:wildcard(Path++"/*")],
file:del_dir(Path);
false ->
file:delete(Path)
end.
delete_path(Path, Package) ->
delete_path(filename:join(filename:dirname(Path), Package)).
modules_dir() ->
DefaultDir = filename:join(getenv("HOME"), ".ejabberd-modules"),
getenv("CONTRIB_MODULES_PATH", DefaultDir).
sources_dir() ->
filename:join(modules_dir(), "sources").
config_dir() ->
DefaultDir = filename:join(modules_dir(), "conf"),
getenv("CONTRIB_MODULES_CONF_DIR", DefaultDir).
-spec modules_configs() -> [binary()].
modules_configs() ->
Fs = [{filename:rootname(filename:basename(F)), F}
|| F <- filelib:wildcard(config_dir() ++ "/*.{yml,yaml}")
++ filelib:wildcard(modules_dir() ++ "/*/conf/*.{yml,yaml}")],
[unicode:characters_to_binary(proplists:get_value(F, Fs))
|| F <- proplists:get_keys(Fs)].
module_lib_dir(Package) ->
filename:join(modules_dir(), Package).
module_ebin_dir(Package) ->
filename:join(module_lib_dir(Package), "ebin").
module_src_dir(Package) ->
Rep = module_name(Package),
SrcDir = sources_dir(),
Standalone = filelib:wildcard(Rep, SrcDir),
Jungle = filelib:wildcard("*/"++Rep, SrcDir),
case Standalone++Jungle of
[RepDir|_] -> filename:join(SrcDir, RepDir);
_ -> filename:join(SrcDir, Rep)
end.
module_name(Id) ->
filename:basename(filename:rootname(Id)).
module_spec_name(Path) ->
case filelib:wildcard(filename:join(Path++"-master", "*.spec")) of
"" ->
module_name(Path);
ModuleName ->
filename:basename(ModuleName, ".spec")
end.
module(Id) ->
misc:binary_to_atom(iolist_to_binary(module_name(Id))).
module_spec(Spec) ->
[{path, filename:dirname(Spec)}
| case consult(Spec) of
{ok, Meta} -> Meta;
_ -> []
end].
modules_spec(Dir, Path) ->
Wildcard = filename:join(Path, "*.spec"),
lists:sort(
[{module(Match), module_spec(filename:join(Dir, Match))}
|| Match <- filelib:wildcard(Wildcard, Dir)]).
short_spec({Module, Attrs}) when is_atom(Module), is_list(Attrs) ->
{Module, proplists:get_value(summary, Attrs, "")}.
is_contrib_allowed(Config) when is_list(Config) ->
case lists:keyfind(allow_contrib_modules, 1, Config) of
false -> true;
{_, false} -> false;
{_, true} -> true
end;
is_contrib_allowed(undefined) ->
ejabberd_option:allow_contrib_modules().
%% -- build functions
check_sources(Module) ->
SrcDir = module_src_dir(Module),
SpecFile = filename:flatten([Module, ".spec"]),
{ok, Dir} = file:get_cwd(),
file:set_cwd(SrcDir),
HaveSrc = case filelib:is_dir("src") or filelib:is_dir("lib") of
true -> [];
false -> [{missing, "src (Erlang) or lib (Elixir) sources directory"}]
end,
DirCheck = lists:foldl(
fun({Type, Name}, Acc) ->
case filelib:Type(Name) of
true -> Acc;
false -> [{missing, Name}|Acc]
end
end, HaveSrc, [{is_file, "README.txt"},
{is_file, "COPYING"},
{is_file, SpecFile}]),
SpecCheck = case consult(SpecFile) of
{ok, Spec} ->
lists:foldl(
fun(Key, Acc) ->
case lists:keysearch(Key, 1, Spec) of
false -> [{missing_meta, Key}|Acc];
{value, {Key, [_NoEmpty|_]}} -> Acc;
{value, {Key, Val}} -> [{invalid_meta, {Key, Val}}|Acc]
end
end, [], [author, summary, home, url]);
{error, Error} ->
[{invalid_spec, Error}]
end,
file:set_cwd(Dir),
Result = DirCheck ++ SpecCheck,
case Result of
[] -> ok;
_ -> {error, Result}
end.
compile_and_install(Module, Spec, Config) ->
SrcDir = module_src_dir(Module),
LibDir = module_lib_dir(Module),
case filelib:is_dir(SrcDir) of
true ->
case compile_deps(SrcDir) of
ok ->
case compile(SrcDir) of
ok -> install(Module, Spec, SrcDir, LibDir, Config);
Error -> Error
end;
Error ->
Error
end;
false ->
Path = proplists:get_value(url, Spec, ""),
case add_sources(Module, Path) of
ok -> compile_and_install(Module, Spec, Config);
Error -> Error
end
end.
compile_deps(LibDir) ->
Deps = filename:join(LibDir, "deps"),
case filelib:is_dir(Deps) of
true -> ok; % assume deps are included
false -> fetch_rebar_deps(LibDir)
end,
Rs = [compile(Dep) || Dep <- filelib:wildcard(filename:join(Deps, "*"))],
compile_result(Rs).
compile(LibDir) ->
Bin = filename:join(LibDir, "ebin"),
Inc = filename:join(LibDir, "include"),
Lib = filename:join(LibDir, "lib"),
Src = filename:join(LibDir, "src"),
Options = [{outdir, Bin}, {i, Inc} | compile_options()],
filelib:ensure_dir(filename:join(Bin, ".")),
[copy(App, Bin) || App <- filelib:wildcard(Src++"/*.app")],
compile_c_files(LibDir),
Er = [compile_erlang_file(Bin, File, Options)
|| File <- filelib:wildcard(Src++"/*.erl")],
Ex = [compile_elixir_file(Bin, File)
|| File <- filelib:wildcard(Lib ++ "/**/*.ex")],
compile_result(lists:flatten([Er, Ex])).
compile_c_files(LibDir) ->
case file:read_file_info(filename:join(LibDir, "c_src/Makefile")) of
{ok, _} ->
os:cmd("cd "++LibDir++"; make -C c_src");
{error, _} ->
ok
end.
compile_result(Results) ->
case lists:dropwhile(
fun({ok, _}) -> true;
(_) -> false
end, Results) of
[] -> ok;
[Error|_] -> Error
end.
maybe_define_lager_macro() ->
case list_to_integer(erlang:system_info(otp_release)) < 22 of
true -> [{d, 'LAGER'}];
false -> []
end.
compile_options() ->
[verbose, report_errors, report_warnings, debug_info, ?ALL_DEFS]
++ maybe_define_lager_macro()
++ [{i, filename:join(app_dir(App), "include")}
|| App <- [fast_xml, xmpp, p1_utils, ejabberd]]
++ [{i, filename:join(mod_dir(Mod), "include")}
|| Mod <- installed()].
app_dir(App) ->
case code:lib_dir(App) of
{error, bad_name} ->
case code:which(App) of
Beam when is_list(Beam) ->
filename:dirname(filename:dirname(Beam));
_ ->
"."
end;
Dir ->
Dir
end.
mod_dir({Package, Spec}) ->
Default = filename:join(modules_dir(), Package),
proplists:get_value(path, Spec, Default).
compile_erlang_file(Dest, File) ->
compile_erlang_file(Dest, File, compile_options()).
compile_erlang_file(Dest, File, ErlOptions) ->
Options = [{outdir, Dest} | ErlOptions],
case compile:file(File, Options) of
{ok, Module} -> {ok, Module};
{ok, Module, _} -> {ok, Module};
{ok, Module, _, _} -> {ok, Module};
error -> {error, {compilation_failed, File}};
{error, E, W} -> {error, {compilation_failed, File, E, W}}
end.
-ifdef(ELIXIR_ENABLED).
compile_elixir_file(Dest, File) when is_list(Dest) and is_list(File) ->
compile_elixir_file(list_to_binary(Dest), list_to_binary(File));
compile_elixir_file(Dest, File) ->
try 'Elixir.Kernel.ParallelCompiler':files_to_path([File], Dest, []) of
Modules when is_list(Modules) -> {ok, Modules}
catch
_ -> {error, {compilation_failed, File}}
end.
-else.
compile_elixir_file(_, File) ->
{error, {compilation_failed, File}}.
-endif.
install(Module, Spec, SrcDir, LibDir, Config) ->
{ok, CurDir} = file:get_cwd(),
file:set_cwd(SrcDir),
Files1 = [{File, copy(File, filename:join(LibDir, File))}
|| File <- filelib:wildcard("{ebin,priv,conf,include}/**")],
Files2 = [{File, copy(File, filename:join(LibDir, filename:join(lists:nthtail(2,filename:split(File)))))}
|| File <- filelib:wildcard("deps/*/ebin/**")],
Files3 = [{File, copy(File, filename:join(LibDir, File))}
|| File <- filelib:wildcard("deps/*/priv/**")],
Errors = lists:dropwhile(fun({_, ok}) -> true;
(_) -> false
end, Files1++Files2++Files3),
inform_module_configuration(Module, LibDir, Files1, Config),
Result = case Errors of
[{F, {error, E}}|_] ->
{error, {F, E}};
[] ->
SpecPath = proplists:get_value(path, Spec),
SpecFile = filename:flatten([Module, ".spec"]),
copy(filename:join(SpecPath, SpecFile), filename:join(LibDir, SpecFile))
end,
file:set_cwd(CurDir),
Result.
inform_module_configuration(Module, LibDir, Files1, Config) ->
Res = lists:filter(fun({[$c, $o, $n, $f |_], ok}) -> true;
(_) -> false
end, Files1),
AlreadyConfigured = lists:keymember(Module, 1, get_modules(Config)),
case {Res, AlreadyConfigured} of
{[{ConfigPath, ok}], false} ->
FullConfigPath = filename:join(LibDir, ConfigPath),
io:format("Module ~p has been installed and started.~n"
"It's configured in the file:~n ~s~n"
"Configure the module in that file, or remove it~n"
"and configure in your main ejabberd.yml~n",
[Module, FullConfigPath]);
{[{ConfigPath, ok}], true} ->
FullConfigPath = filename:join(LibDir, ConfigPath),
file:rename(FullConfigPath, FullConfigPath++".example"),
io:format("Module ~p has been installed and started.~n"
"The ~p configuration in your ejabberd.yml is used.~n",
[Module, Module]);
{[], _} ->
io:format("Module ~p has been installed.~n"
"Now you can configure it in your ejabberd.yml~n",
[Module])
end.
get_modules(Config) when is_list(Config) ->
{modules, Modules} = lists:keyfind(modules, 1, Config),
Modules;
get_modules(undefined) ->
ejabberd_config:get_option(modules).
%% -- minimalist rebar spec parser, only support git
fetch_rebar_deps(SrcDir) ->
case rebar_deps(filename:join(SrcDir, "rebar.config"))
++ rebar_deps(filename:join(SrcDir, "rebar.config.script")) of
[] ->
ok;
Deps ->
{ok, CurDir} = file:get_cwd(),
file:set_cwd(SrcDir),
filelib:ensure_dir(filename:join("deps", ".")),
lists:foreach(fun({_App, Cmd}) ->
os:cmd("cd deps; "++Cmd++"; cd ..")
end, Deps),
file:set_cwd(CurDir)
end.
rebar_deps(Script) ->
case file:script(Script) of
{ok, Config} when is_list(Config) ->
[rebar_dep(Dep) || Dep <- proplists:get_value(deps, Config, [])];
{ok, {deps, Deps}} ->
[rebar_dep(Dep) || Dep <- Deps];
_ ->
[]
end.
rebar_dep({App, _, {git, Url}}) ->
{App, "git clone "++Url++" "++filename:basename(App)};
rebar_dep({App, _, {git, Url, {branch, Ref}}}) ->
{App, "git clone -n "++Url++" "++filename:basename(App)++
"; (cd "++filename:basename(App)++
"; git checkout -q origin/"++Ref++")"};
rebar_dep({App, _, {git, Url, {tag, Ref}}}) ->
{App, "git clone -n "++Url++" "++filename:basename(App)++
"; (cd "++filename:basename(App)++
"; git checkout -q "++Ref++")"};
rebar_dep({App, _, {git, Url, Ref}}) ->
{App, "git clone -n "++Url++" "++filename:basename(App)++
"; (cd "++filename:basename(App)++
"; git checkout -q "++Ref++")"}.
module_deps_dirs(Module) ->
SrcDir = module_src_dir(Module),
LibDir = module_lib_dir(Module),
DepsDir = filename:join(LibDir, "deps"),
Deps = rebar_deps(filename:join(SrcDir, "rebar.config"))
++ rebar_deps(filename:join(SrcDir, "rebar.config.script")),
[filename:join(DepsDir, App) || {App, _Cmd} <- Deps].
%% -- YAML spec parser
consult(File) ->
case fast_yaml:decode_from_file(File, [plain_as_atom]) of
{ok, []} -> {ok, []};
{ok, [Doc|_]} -> {ok, [format(Spec) || Spec <- Doc]};
{error, Err} -> {error, fast_yaml:format_error(Err)}
end.
format({Key, Val}) when is_binary(Val) ->
{Key, binary_to_list(Val)};
format({Key, Val}) -> % TODO: improve Yaml parsing
{Key, Val}.
%% -- COMMIT.json
maybe_write_commit_json(Url, RepDir) ->
case (os:getenv("GITHUB_ACTIONS") == "true") of
true ->
ok;
false ->
write_commit_json(Url, RepDir)
end.
write_commit_json(Url, RepDir) ->
Url2 = string_replace(Url, "https://github.com", "https://api.github.com/repos"),
BranchUrl = lists:flatten(Url2 ++ "/branches/master"),
case geturl(BranchUrl) of
{ok, _Headers, Body} ->
{ok, F} = file:open(filename:join(RepDir, "COMMIT.json"), [raw, write]),
file:write(F, Body),
file:close(F);
{error, Reason} ->
Reason
end.
find_commit_json(Attrs) ->
FromPath = get_module_path(Attrs),
case {find_commit_json_path(FromPath),
find_commit_json_path(filename:join(FromPath, ".."))}
of
{{ok, FromFile}, _} ->
FromFile;
{_, {ok, FromFile}} ->
FromFile;
_ ->
not_found
end.
-ifdef(HAVE_URI_STRING). %% Erlang/OTP 20 or higher can use this:
string_replace(Subject, Pattern, Replacement) ->
string:replace(Subject, Pattern, Replacement).
find_commit_json_path(Path) ->
filelib:find_file("COMMIT.json", Path).
-else. % Workaround for Erlang/OTP older than 20:
string_replace(Subject, Pattern, Replacement) ->
B = binary:replace(list_to_binary(Subject),
list_to_binary(Pattern),
list_to_binary(Replacement)),
binary_to_list(B).
find_commit_json_path(Path) ->
case filelib:wildcard("COMMIT.json", Path) of
[] ->
{error, commit_json_not_found};
["COMMIT.json"] = File ->
{ok, filename:join(Path, File)}
end.
-endif.
copy_commit_json(Package, Attrs) ->
DestPath = module_lib_dir(Package),
case find_commit_json(Attrs) of
not_found ->
ok;
FromFile ->
file:copy(FromFile, filename:join(DestPath, "COMMIT.json"))
end.
get_commit_details(Dirname) ->
RepDir = filename:join(sources_dir(), Dirname),
get_commit_details2(filename:join(RepDir, "COMMIT.json")).
get_commit_details2(Path) ->
case file:read_file(Path) of
{ok, Body} ->
parse_details(Body);
_ ->
#{sha => unknown_sha,
date => <<>>,
message => <<>>,
html => <<>>,
author_name => <<>>,
commit_html_url => <<>>}
end.
parse_details(Body) ->
{Contents} = jiffy:decode(Body),
{_, {Commit}} = lists:keyfind(<<"commit">>, 1, Contents),
{_, Sha} = lists:keyfind(<<"sha">>, 1, Commit),
{_, CommitHtmlUrl} = lists:keyfind(<<"html_url">>, 1, Commit),
{_, {Commit2}} = lists:keyfind(<<"commit">>, 1, Commit),
{_, Message} = lists:keyfind(<<"message">>, 1, Commit2),
{_, {Author}} = lists:keyfind(<<"author">>, 1, Commit2),
{_, AuthorName} = lists:keyfind(<<"name">>, 1, Author),
{_, {Committer}} = lists:keyfind(<<"committer">>, 1, Commit2),
{_, Date} = lists:keyfind(<<"date">>, 1, Committer),
{_, {Links}} = lists:keyfind(<<"_links">>, 1, Contents),
{_, Html} = lists:keyfind(<<"html">>, 1, Links),
#{sha => Sha,
date => Date,
message => Message,
html => Html,
author_name => AuthorName,
commit_html_url => CommitHtmlUrl}.
%% -- Web Admin
-define(AXC(URL, Attributes, Text),
?XAE(<<"a">>, [{<<"href">>, URL} | Attributes], [?C(Text)])
).
-define(INPUTCHECKED(Type, Name, Value),
?XA(<<"input">>,
[{<<"type">>, Type},
{<<"name">>, Name},
{<<"disabled">>, <<"true">>},
{<<"checked">>, <<"true">>},
{<<"value">>, Value}
]
)
).
web_menu_node(Acc, _Node, Lang) ->
Acc ++ [{<<"contrib">>, translate:translate(Lang, ?T("Contrib Modules"))}].
web_page_node(_, Node, [<<"contrib">>], Query, Lang) ->
Res = rpc:call(Node, ?MODULE, get_page, [Node, Query, Lang]),
{stop, Res};
web_page_node(Acc, _, _, _, _) ->
Acc.
get_page(Node, Query, Lang) ->
QueryRes = list_modules_parse_query(Query),
Title = ?H1GL(translate:translate(Lang, ?T("Contrib Modules")),
<<"../../developer/extending-ejabberd/modules/#ejabberd-contrib">>,
<<"ejabberd-contrib">>),
Contents = get_content(Node, Query, Lang),
Result = case QueryRes of
ok -> [?XREST(?T("Submitted"))];
nothing -> []
end,
Title ++ Result ++ Contents.
get_module_home(Module, Attrs) ->
case get_module_information(home, Attrs) of
"https://github.com/processone/ejabberd-contrib/tree/master/" = P1 ->
P1 ++ atom_to_list(Module);
Other ->
Other
end.
get_module_summary(Attrs) ->
get_module_information(summary, Attrs).
get_module_author(Attrs) ->
get_module_information(author, Attrs).
get_module_path(Attrs) ->
get_module_information(path, Attrs).
get_module_information(Attribute, Attrs) ->
case lists:keyfind(Attribute, 1, Attrs) of
false -> "";
{_, Value} -> Value
end.
get_installed_module_el({ModAtom, Attrs}, Lang) ->
Mod = misc:atom_to_binary(ModAtom),
Home = list_to_binary(get_module_home(ModAtom, Attrs)),
Summary = list_to_binary(get_module_summary(Attrs)),
Author = list_to_binary(get_module_author(Attrs)),
FromPath = get_module_path(Attrs),
FromFile = case find_commit_json_path(FromPath) of
{ok, FF} -> FF;
{error, _} -> "dummypath"
end,
#{sha := CommitSha,
date := CommitDate,
message := CommitMessage,
author_name := CommitAuthorName,
commit_html_url := CommitHtmlUrl} = get_commit_details2(FromFile),
[SourceSpec] = [S || {M, S} <- available(), M == ModAtom],
SourceFile = find_commit_json(SourceSpec),
#{sha := SourceSha,
date := SourceDate,
message := SourceMessage,
author_name := SourceAuthorName,
commit_html_url := SourceHtmlUrl} = get_commit_details2(SourceFile),
UpgradeEls =
case CommitSha == SourceSha of
true ->
[];
false ->
SourceTitleEl = make_title_el(SourceDate, SourceMessage, SourceAuthorName),
[?XE(<<"td">>,
[?INPUT(<<"checkbox">>, <<"selected_upgrade">>, Mod),
?C(<<" ">>),
?AXC(SourceHtmlUrl, [SourceTitleEl], binary:part(SourceSha, {0, 8}))
]
)
]
end,
Started =
case gen_mod:is_loaded(hd(ejabberd_option:hosts()), ModAtom) of
false ->
[?C(<<" ">>)];
true ->
[]
end,
TitleEl = make_title_el(CommitDate, CommitMessage, CommitAuthorName),
Status = get_module_status_el(ModAtom),
HomeTitleEl = make_home_title_el(Summary, Author),
?XE(<<"tr">>,
[?XE(<<"td">>, [?AXC(Home, [HomeTitleEl], Mod)]),
?XE(<<"td">>,
[?INPUTTD(<<"checkbox">>, <<"selected_uninstall">>, Mod),
?C(<<" ">>),
get_commit_link(CommitHtmlUrl, TitleEl, CommitSha),
?C(<<" - ">>)]
++ Started
++ Status)
| UpgradeEls]).
get_module_status_el(ModAtom) ->
case {get_module_status(ModAtom),
get_module_status(elixir_module_name(ModAtom))} of
{Str, unknown} when is_list(Str) ->
[?C(<<" ">>), ?C(Str)];
{unknown, Str} when is_list(Str) ->
[?C(<<" ">>), ?C(Str)];
{unknown, unknown} ->
[]
end.
get_module_status(Module) ->
try Module:mod_status() of
Str when is_list(Str) ->
Str
catch
_:_ ->
unknown
end.
%% When a module named mod_whatever in ejabberd-modules
%% is written in Elixir, its runtime name is 'Elixir.ModWhatever'
get_runtime_module_name(Module) ->
case is_elixir_module(Module) of
true -> elixir_module_name(Module);
false -> Module
end.
is_elixir_module(Module) ->
LibDir = module_src_dir(Module),
Lib = filename:join(LibDir, "lib"),
Src = filename:join(LibDir, "src"),
case {filelib:wildcard(Lib++"/*.{ex}"),
filelib:wildcard(Src++"/*.{erl}")} of
{[_ | _], []} ->
true;
{[], [_ | _]} ->
false
end.
%% Converts mod_some_thing to Elixir.ModSomeThing
elixir_module_name(ModAtom) ->
list_to_atom("Elixir." ++ elixir_module_name("_" ++ atom_to_list(ModAtom), [])).
elixir_module_name([], Res) ->
lists:reverse(Res);
elixir_module_name([$_, Char | Remaining], Res) ->
[Upper] = uppercase([Char]),
elixir_module_name(Remaining, [Upper | Res]);
elixir_module_name([Char | Remaining], Res) ->
elixir_module_name(Remaining, [Char | Res]).
-ifdef(HAVE_URI_STRING).
uppercase(String) ->
string:uppercase(String). % OTP 20 or higher
-else.
uppercase(String) ->
string:to_upper(String). % OTP older than 20
-endif.
get_available_module_el({ModAtom, Attrs}) ->
Installed = installed(),
Mod = misc:atom_to_binary(ModAtom),
Home = list_to_binary(get_module_home(ModAtom, Attrs)),
Summary = list_to_binary(get_module_summary(Attrs)),
Author = list_to_binary(get_module_author(Attrs)),
HomeTitleEl = make_home_title_el(Summary, Author),
InstallCheckbox =
case lists:keymember(ModAtom, 1, Installed) of
false -> [?INPUT(<<"checkbox">>, <<"selected_install">>, Mod)];
true -> [?INPUTCHECKED(<<"checkbox">>, <<"selected_install">>, Mod)]
end,
?XE(<<"tr">>,
[?XE(<<"td">>, InstallCheckbox ++ [?C(<<" ">>), ?AXC(Home, [HomeTitleEl], Mod)]),
?XE(<<"td">>, [?C(Summary)])]).
get_installed_modules_table(Lang) ->
Modules = installed(),
Tail = [?XE(<<"tr">>,
[?XE(<<"td">>, []),
?XE(<<"td">>,
[?INPUTTD(<<"submit">>, <<"uninstall">>, ?T("Uninstall"))]
),
?XE(<<"td">>,
[?INPUTT(<<"submit">>, <<"upgrade">>, ?T("Upgrade"))]
)
]
)
],
TBody = [get_installed_module_el(Module, Lang) || Module <- lists:sort(Modules)],
?XAE(<<"table">>,
[],
[?XE(<<"tbody">>, TBody ++ Tail)]
).
get_available_modules_table(Lang) ->
Modules = get_available_notinstalled(),
Tail = [?XE(<<"tr">>,
[?XE(<<"td">>,
[?INPUTT(<<"submit">>, <<"install">>, ?T("Install"))]
)
]
)
],
TBody = [get_available_module_el(Module) || Module <- lists:sort(Modules)],
?XAE(<<"table">>,
[],
[?XE(<<"tbody">>, TBody ++ Tail)]
).
make_title_el(Date, Message, AuthorName) ->
LinkTitle = <<Message/binary, "\n", AuthorName/binary, "\n", Date/binary>>,
{<<"title">>, LinkTitle}.
make_home_title_el(Summary, Author) ->
LinkTitle = <<Summary/binary, "\n", Author/binary>>,
{<<"title">>, LinkTitle}.
get_commit_link(_CommitHtmlUrl, _TitleErl, unknown_sha) ->
?C(<<"Please Update Specs">>);
get_commit_link(CommitHtmlUrl, TitleEl, CommitSha) ->
?AXC(CommitHtmlUrl, [TitleEl], binary:part(CommitSha, {0, 8})).
get_content(Node, Query, Lang) ->
{{_CommandCtl}, _Res} =
case catch parse_and_execute(Query, Node) of
{'EXIT', _} -> {{""}, <<"">>};
Result_tuple -> Result_tuple
end,
AvailableModulesEls = get_available_modules_table(Lang),
InstalledModulesEls = get_installed_modules_table(Lang),
Sources = get_sources_list(),
SourceEls = (?XAE(<<"table">>,
[],
[?XE(<<"tbody">>,
(lists:map(
fun(Dirname) ->
#{sha := CommitSha,
date := CommitDate,
message := CommitMessage,
html := Html,
author_name := AuthorName,
commit_html_url := CommitHtmlUrl
} = get_commit_details(Dirname),
TitleEl = make_title_el(CommitDate, CommitMessage, AuthorName),
?XE(<<"tr">>,
[?XE(<<"td">>, [?AC(Html, Dirname)]),
?XE(<<"td">>,
[get_commit_link(CommitHtmlUrl, TitleEl, CommitSha)]
),
?XE(<<"td">>, [?C(CommitMessage)])
])
end,
lists:sort(Sources)
))
)
]
)),
[?XC(<<"p">>,
translate:translate(
Lang, ?T("Update specs to get modules source, then install desired ones.")
)
),
?XAE(<<"form">>,
[{<<"method">>, <<"post">>}],
[?XCT(<<"h3">>, ?T("Sources Specs:")),
SourceEls,
?BR,
?INPUTT(<<"submit">>,
<<"updatespecs">>,
translate:translate(Lang, ?T("Update Specs"))),
?XCT(<<"h3">>, ?T("Installed Modules:")),
InstalledModulesEls,
?BR,
?XCT(<<"h3">>, ?T("Other Modules Available:")),
AvailableModulesEls
]
)
].
get_sources_list() ->
case file:list_dir(sources_dir()) of
{ok, Filenames} -> Filenames;
{error, enoent} -> []
end.
get_available_notinstalled() ->
Installed = installed(),
lists:filter(
fun({Mod, _}) ->
not lists:keymember(Mod, 1, Installed)
end,
available()
).
parse_and_execute(Query, Node) ->
{[Exec], _} = lists:partition(
fun(ExType) ->
lists:keymember(ExType, 1, Query)
end,
[<<"updatespecs">>]
),
Commands = {get_val(<<"updatespecs">>, Query)},
{_, R} = parse1_command(Exec, Commands, Node),
{Commands, R}.
get_val(Val, Query) ->
{value, {_, R}} = lists:keysearch(Val, 1, Query),
binary_to_list(R).
parse1_command(<<"updatespecs">>, {_}, _Node) ->
Res = update(),
{oook, io_lib:format("~p", [Res])}.
list_modules_parse_query(Query) ->
case {lists:keysearch(<<"install">>, 1, Query),
lists:keysearch(<<"upgrade">>, 1, Query),
lists:keysearch(<<"uninstall">>, 1, Query)}
of
{{value, _}, _, _} -> list_modules_parse_install(Query);
{_, {value, _}, _} -> list_modules_parse_upgrade(Query);
{_, _, {value, _}} -> list_modules_parse_uninstall(Query);
_ -> nothing
end.
list_modules_parse_install(Query) ->
lists:foreach(
fun({Mod, _}) ->
ModBin = misc:atom_to_binary(Mod),
case lists:member({<<"selected_install">>, ModBin}, Query) of
true -> install(Mod);
_ -> ok
end
end,
get_available_notinstalled()),
ok.
list_modules_parse_upgrade(Query) ->
lists:foreach(
fun({Mod, _}) ->
ModBin = misc:atom_to_binary(Mod),
case lists:member({<<"selected_upgrade">>, ModBin}, Query) of
true -> upgrade(Mod);
_ -> ok
end
end,
installed()),
ok.
list_modules_parse_uninstall(Query) ->
lists:foreach(
fun({Mod, _}) ->
ModBin = misc:atom_to_binary(Mod),
case lists:member({<<"selected_uninstall">>, ModBin}, Query) of
true -> uninstall(Mod);
_ -> ok
end
end,
installed()),
ok.
install_contrib_modules(Modules, Config) ->
lists:filter(fun(Module) ->
case install(misc:atom_to_binary(Module), Config) of
{error, conflict} ->
false;
ok ->
true
end
end,
Modules).