• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

processone / ejabberd / 765

12 Jul 2024 01:47PM UTC coverage: 32.987% (+0.01%) from 32.975%
765

push

github

badlop
ext_mod: Fetch dependencies from hex.pm when mix is available

This doesn't work when running an OTP release build using mix,
which means it doesn't work in binary installers or containers;
only when using relive, or compiled with rebar3.

Set the desired hex package version in the module's rebar.config
For example, to fetch hex package recon 2.5.5 when mix is available,
and otherwise download using git: in the file
  ejabberd-contrib/ejabberd_observer_cli/rebar.config
set both the hex version and git details:
{deps, [
        {recon, "2.5.5", {git, "https://github.com/ferd/recon"}}
       ]}.

0 of 10 new or added lines in 1 file covered. (0.0%)

82 existing lines in 2 files now uncovered.

14530 of 44047 relevant lines covered (32.99%)

617.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

36.44
/src/ext_mod.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : ext_mod.erl
3
%%% Author  : Christophe Romain <christophe.romain@process-one.net>
4
%%% Purpose : external modules management
5
%%% Created : 19 Feb 2015 by Christophe Romain <christophe.romain@process-one.net>
6
%%%
7
%%%
8
%%% ejabberd, Copyright (C) 2006-2024   ProcessOne
9
%%%
10
%%% This program is free software; you can redistribute it and/or
11
%%% modify it under the terms of the GNU General Public License as
12
%%% published by the Free Software Foundation; either version 2 of the
13
%%% License, or (at your option) any later version.
14
%%%
15
%%% This program is distributed in the hope that it will be useful,
16
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
17
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18
%%% General Public License for more details.
19
%%%
20
%%% You should have received a copy of the GNU General Public License along
21
%%% with this program; if not, write to the Free Software Foundation, Inc.,
22
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23
%%%
24
%%%----------------------------------------------------------------------
25

26
-module(ext_mod).
27

28
-behaviour(gen_server).
29
-author("Christophe Romain <christophe.romain@process-one.net>").
30

31
-export([start_link/0, update/0, check/1,
32
         available_command/0, available/0, available/1,
33
         installed_command/0, installed/0, installed/1,
34
         install/1, uninstall/1, upgrade/0, upgrade/1, add_paths/0,
35
         add_sources/1, add_sources/2, del_sources/1, modules_dir/0,
36
         install_contrib_modules/2,
37
         config_dir/0, get_commands_spec/0]).
38
-export([modules_configs/0, module_ebin_dir/1]).
39
-export([compile_erlang_file/2, compile_elixir_files/2]).
40
-export([web_menu_node/3, web_page_node/3, webadmin_node_contrib/3]).
41

42
%% gen_server callbacks
43
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
44
         terminate/2, code_change/3]).
45

46
-include("ejabberd_commands.hrl").
47
-include("ejabberd_http.hrl").
48
-include("ejabberd_web_admin.hrl").
49
-include("logger.hrl").
50
-include("translate.hrl").
51
-include_lib("xmpp/include/xmpp.hrl").
52

53
-define(REPOS, "git@github.com:processone/ejabberd-contrib.git").
54

55
-record(state, {}).
56

57
start_link() ->
58
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
1✔
59

60
init([]) ->
61
    process_flag(trap_exit, true),
1✔
62
    add_paths(),
1✔
63
    application:start(inets),
1✔
64
    inets:start(httpc, [{profile, ext_mod}]),
1✔
65
    ejabberd_commands:register_commands(get_commands_spec()),
1✔
66
    ejabberd_hooks:add(webadmin_menu_node, ?MODULE, web_menu_node, 50),
1✔
67
    ejabberd_hooks:add(webadmin_page_node, ?MODULE, web_page_node, 50),
1✔
68
    {ok, #state{}}.
1✔
69

70
add_paths() ->
71
    [code:add_pathsz([module_ebin_dir(Module)|module_deps_dirs(Module)])
1✔
72
     || {Module, _} <- installed()].
1✔
73

74
handle_call(Request, From, State) ->
75
    ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
×
76
    {noreply, State}.
×
77

78
handle_cast(Msg, State) ->
79
    ?WARNING_MSG("Unexpected cast: ~p", [Msg]),
×
80
    {noreply, State}.
×
81

82
handle_info(Info, State) ->
83
    ?WARNING_MSG("Unexpected info: ~p", [Info]),
×
84
    {noreply, State}.
×
85

86
terminate(_Reason, _State) ->
87
    ejabberd_hooks:delete(webadmin_menu_node, ?MODULE, web_menu_node, 50),
1✔
88
    ejabberd_hooks:delete(webadmin_page_node, ?MODULE, web_page_node, 50),
1✔
89
    ejabberd_commands:unregister_commands(get_commands_spec()).
1✔
90

91
code_change(_OldVsn, State, _Extra) ->
92
    {ok, State}.
×
93

94
%% -- ejabberd commands
95
get_commands_spec() ->
96
    [#ejabberd_commands{name = modules_update_specs,
2✔
97
                        tags = [modules],
98
                        desc = "Update the module source code from Git",
99
                        longdesc = "A connection to Internet is required",
100
                        module = ?MODULE, function = update,
101
                        args = [],
102
                        result = {res, rescode}},
103
     #ejabberd_commands{name = modules_available,
104
                        tags = [modules],
105
                        desc = "List the contributed modules available to install",
106
                        module = ?MODULE, function = available_command,
107
                        result_desc = "List of tuples with module name and description",
108
                        result_example = [{mod_cron, "Execute scheduled commands"},
109
                                          {mod_rest, "ReST frontend"}],
110
                        args = [],
111
                        result = {modules, {list,
112
                                  {module, {tuple,
113
                                   [{name, atom},
114
                                    {summary, string}]}}}}},
115
     #ejabberd_commands{name = modules_installed,
116
                        tags = [modules],
117
                        desc = "List the contributed modules already installed",
118
                        module = ?MODULE, function = installed_command,
119
                        result_desc = "List of tuples with module name and description",
120
                        result_example = [{mod_cron, "Execute scheduled commands"},
121
                                          {mod_rest, "ReST frontend"}],
122
                        args = [],
123
                        result = {modules, {list,
124
                                  {module, {tuple,
125
                                   [{name, atom},
126
                                    {summary, string}]}}}}},
127
     #ejabberd_commands{name = module_install,
128
                        tags = [modules],
129
                        desc = "Compile, install and start an available contributed module",
130
                        module = ?MODULE, function = install,
131
                        args_desc = ["Module name"],
132
                        args_example = [<<"mod_rest">>],
133
                        args = [{module, binary}],
134
                        result = {res, rescode}},
135
     #ejabberd_commands{name = module_uninstall,
136
                        tags = [modules],
137
                        desc = "Uninstall a contributed module",
138
                        module = ?MODULE, function = uninstall,
139
                        args_desc = ["Module name"],
140
                        args_example = [<<"mod_rest">>],
141
                        args = [{module, binary}],
142
                        result = {res, rescode}},
143
     #ejabberd_commands{name = module_upgrade,
144
                        tags = [modules],
145
                        desc = "Upgrade the running code of an installed module",
146
                        longdesc = "In practice, this uninstalls and installs the module",
147
                        module = ?MODULE, function = upgrade,
148
                        args_desc = ["Module name"],
149
                        args_example = [<<"mod_rest">>],
150
                        args = [{module, binary}],
151
                        result = {res, rescode}},
152
     #ejabberd_commands{name = module_check,
153
                        tags = [modules],
154
                        desc = "Check the contributed module repository compliance",
155
                        module = ?MODULE, function = check,
156
                        args_desc = ["Module name"],
157
                        args_example = [<<"mod_rest">>],
158
                        args = [{module, binary}],
159
                        result = {res, rescode}}
160
        ].
161
%% -- public modules functions
162

163
update() ->
164
    Contrib = maps:put(?REPOS, [], maps:new()),
1✔
165
    Jungles = lists:foldl(fun({Package, Spec}, Acc) ->
1✔
166
                Repo = proplists:get_value(url, Spec, ""),
×
167
                Mods = maps:get(Repo, Acc, []),
×
168
                maps:put(Repo, [Package|Mods], Acc)
×
169
        end, Contrib, modules_spec(sources_dir(), "*/*")),
170
    Repos = maps:fold(fun(Repo, _Mods, Acc) ->
1✔
171
                Update = add_sources(Repo),
1✔
172
                ?INFO_MSG("Update packages from repo ~ts: ~p", [Repo, Update]),
1✔
173
                case Update of
1✔
174
                    ok -> Acc;
1✔
175
                    Error -> [{repository, Repo, Error}|Acc]
×
176
                end
177
        end, [], Jungles),
178
    Res = lists:foldl(fun({Package, Spec}, Acc) ->
1✔
179
                Repo = proplists:get_value(url, Spec, ""),
×
180
                Update = add_sources(Package, Repo),
×
181
                ?INFO_MSG("Update package ~ts: ~p", [Package, Update]),
×
182
                case Update of
×
183
                    ok -> Acc;
×
184
                    Error -> [{Package, Repo, Error}|Acc]
×
185
                end
186
        end, Repos, modules_spec(sources_dir(), "*")),
187
    case Res of
1✔
188
        [] -> ok;
1✔
189
        [Error|_] -> Error
×
190
    end.
191

192
available() ->
193
    Jungle = modules_spec(sources_dir(), "*/*"),
2✔
194
    Standalone = modules_spec(sources_dir(), "*"),
2✔
195
    lists:keysort(1,
2✔
196
        lists:foldl(fun({Key, Val}, Acc) ->
197
                lists:keystore(Key, 1, Acc, {Key, Val})
×
198
            end, Jungle, Standalone)).
199
available(Module) when is_atom(Module) ->
200
    available(misc:atom_to_binary(Module));
×
201
available(Package) when is_binary(Package) ->
202
    Available = [misc:atom_to_binary(K) || K<-proplists:get_keys(available())],
×
203
    lists:member(Package, Available).
×
204

205
available_command() ->
206
    [short_spec(Item) || Item <- available()].
×
207

208
installed() ->
209
    modules_spec(modules_dir(), "*").
22✔
210
installed(Module) when is_atom(Module) ->
211
    installed(misc:atom_to_binary(Module));
×
212
installed(Package) when is_binary(Package) ->
213
    Installed = [misc:atom_to_binary(K) || K<-proplists:get_keys(installed())],
3✔
214
    lists:member(Package, Installed).
3✔
215

216
installed_command() ->
217
    [short_spec(Item) || Item <- installed()].
3✔
218

219
install(Module) when is_atom(Module) ->
220
    install(misc:atom_to_binary(Module), undefined);
×
221
install(Package) when is_binary(Package) ->
222
    install(Package, undefined).
2✔
223

224
install(Package, Config) when is_binary(Package) ->
225
    Spec = [S || {Mod, S} <- available(), misc:atom_to_binary(Mod)==Package],
2✔
226
    case {Spec, installed(Package), is_contrib_allowed(Config)} of
2✔
227
        {_, _, false} ->
228
            {error, not_allowed};
×
229
        {[], _, _} ->
230
            {error, not_available};
1✔
231
        {_, true, _} ->
232
            {error, conflict};
×
233
        {[Attrs], _, _} ->
234
            Module = misc:binary_to_atom(Package),
1✔
235
            case compile_and_install(Module, Attrs, Config) of
1✔
236
                ok ->
237
                    code:add_pathsz([module_ebin_dir(Module)|module_deps_dirs(Module)]),
1✔
238
                    ejabberd_config_reload(Config),
1✔
239
                    copy_commit_json(Package, Attrs),
1✔
240
                    ModuleRuntime = get_runtime_module_name(Module),
1✔
241
                    case erlang:function_exported(ModuleRuntime, post_install, 0) of
1✔
242
                        true -> ModuleRuntime:post_install();
×
243
                        _ -> ok
1✔
244
                    end;
245
                Error ->
246
                    delete_path(module_lib_dir(Module)),
×
247
                    Error
×
248
            end
249
    end.
250

251
ejabberd_config_reload(Config) when is_list(Config) ->
252
    %% Don't reload config when ejabberd is starting
253
    %% because it will be reloaded after installing
254
    %% all the external modules from install_contrib_modules
255
    ok;
×
256
ejabberd_config_reload(undefined) ->
257
    ejabberd_config:reload().
1✔
258

259
uninstall(Module) when is_atom(Module) ->
260
    uninstall(misc:atom_to_binary(Module));
×
261
uninstall(Package) when is_binary(Package) ->
262
    case installed(Package) of
1✔
263
        true ->
264
            Module = misc:binary_to_atom(Package),
×
265
            ModuleRuntime = get_runtime_module_name(Module),
×
266
            case erlang:function_exported(ModuleRuntime, pre_uninstall, 0) of
×
267
                true -> ModuleRuntime:pre_uninstall();
×
268
                _ -> ok
×
269
            end,
270
            [catch gen_mod:stop_module(Host, ModuleRuntime)
×
271
             || Host <- ejabberd_option:hosts()],
×
272
            code:purge(ModuleRuntime),
×
273
            code:delete(ModuleRuntime),
×
274
            [code:del_path(PathDelete) || PathDelete <- [module_ebin_dir(Module)|module_deps_dirs(Module)]],
×
275
            delete_path(module_lib_dir(Module)),
×
276
            ejabberd_config:reload();
×
277
        false ->
278
            {error, not_installed}
1✔
279
    end.
280

281
upgrade() ->
282
    [{Package, upgrade(Package)} || {Package, _Spec} <- installed()].
×
283
upgrade(Module) when is_atom(Module) ->
284
    upgrade(misc:atom_to_binary(Module));
×
285
upgrade(Package) when is_binary(Package) ->
286
    uninstall(Package),
×
287
    install(Package).
×
288

289
add_sources(Path) when is_list(Path) ->
290
    add_sources(iolist_to_binary(module_name(Path)), Path).
1✔
291
add_sources(_, "") ->
292
    {error, no_url};
×
293
add_sources(Module, Path) when is_atom(Module), is_list(Path) ->
294
    add_sources(misc:atom_to_binary(Module), Path);
×
295
add_sources(Package, Path) when is_binary(Package), is_list(Path) ->
296
    DestDir = sources_dir(),
1✔
297
    RepDir = filename:join(DestDir, module_name(Path)),
1✔
298
    delete_path(RepDir, binary_to_list(Package)),
1✔
299
    case filelib:ensure_dir(RepDir) of
1✔
300
        ok ->
301
            case {string:left(Path, 4), string:right(Path, 2)} of
1✔
302
                {"http", "ip"} -> extract(zip, geturl(Path), DestDir);
×
303
                {"http", "gz"} -> extract(tar, geturl(Path), DestDir);
×
304
                {"http", _} -> extract_url(Path, DestDir);
×
305
                {"git@", _} -> extract_github_master(Path, DestDir);
1✔
306
                {_, "ip"} -> extract(zip, Path, DestDir);
×
307
                {_, "gz"} -> extract(tar, Path, DestDir);
×
308
                _ -> {error, unsupported_source}
×
309
            end;
310
        Error ->
311
            Error
×
312
    end.
313

314
del_sources(Module) when is_atom(Module) ->
315
    del_sources(misc:atom_to_binary(Module));
×
316
del_sources(Package) when is_binary(Package) ->
317
    case uninstall(Package) of
×
318
        ok ->
319
            SrcDir = module_src_dir(misc:binary_to_atom(Package)),
×
320
            delete_path(SrcDir);
×
321
        Error ->
322
            Error
×
323
    end.
324

325
check(Module) when is_atom(Module) ->
326
    check(misc:atom_to_binary(Module));
×
327
check(Package) when is_binary(Package) ->
328
    case {available(Package), installed(Package)} of
×
329
        {false, _} ->
330
            {error, not_available};
×
331
        {_, false} ->
332
            Status = install(Package),
×
333
            uninstall(Package),
×
334
            case Status of
×
335
                ok -> check_sources(misc:binary_to_atom(Package));
×
336
                Error -> Error
×
337
            end;
338
        _ ->
339
            check_sources(misc:binary_to_atom(Package))
×
340
    end.
341

342
%% -- archives and variables functions
343

344
geturl(Url) ->
345
    case getenv("PROXY_SERVER", "", ":") of
1✔
346
        [H, Port] ->
347
            httpc:set_options([{proxy, {{H, list_to_integer(Port)}, []}}], ext_mod);
×
348
        [H] ->
349
            httpc:set_options([{proxy, {{H, 8080}, []}}], ext_mod);
×
350
        _ ->
351
            ok
1✔
352
    end,
353
    User = case getenv("PROXY_USER", "", ":") of
1✔
354
        [U, Pass] -> [{proxy_auth, {U, Pass}}];
×
355
        _ -> []
1✔
356
    end,
357
    UA = {"User-Agent", "ejabberd/ext_mod"},
1✔
358
    case httpc:request(get, {Url, [UA]}, User, [{body_format, binary}], ext_mod) of
1✔
359
        {ok, {{_, 200, _}, Headers, Response}} ->
360
            {ok, Headers, Response};
1✔
361
        {ok, {{_, 403, Reason}, _Headers, _Response}} ->
362
            {error, Reason};
×
363
        {ok, {{_, Code, _}, _Headers, Response}} ->
364
            {error, {Code, Response}};
×
365
        {error, Reason} ->
366
            {error, Reason}
×
367
    end.
368

369
getenv(Env) ->
370
    getenv(Env, "").
47✔
371
getenv(Env, Default) ->
372
    case os:getenv(Env) of
98✔
373
        false -> Default;
51✔
374
        "" -> Default;
×
375
        Value -> Value
47✔
376
    end.
377
getenv(Env, Default, Separator) ->
378
    string:tokens(getenv(Env, Default), Separator).
2✔
379

380
extract(zip, {ok, _, Body}, DestDir) ->
381
    extract(zip, iolist_to_binary(Body), DestDir);
1✔
382
extract(tar, {ok, _, Body}, DestDir) ->
383
    extract(tar, {binary, iolist_to_binary(Body)}, DestDir);
×
384
extract(_, {error, Reason}, _) ->
385
    {error, Reason};
×
386
extract(zip, Zip, DestDir) ->
387
    case zip:extract(Zip, [{cwd, DestDir}]) of
1✔
388
        {ok, _} -> ok;
1✔
389
        Error -> Error
×
390
    end;
391
extract(tar, Tar, DestDir) ->
392
    erl_tar:extract(Tar, [compressed, {cwd, DestDir}]).
×
393

394
extract_url(Path, DestDir) ->
395
    hd([extract_github_master(Path, DestDir) || string:str(Path, "github") > 0]
×
396
     ++[{error, unsupported_source}]).
397

398
extract_github_master(Repos, DestDir) ->
399
    Base = case string:tokens(Repos, ":") of
1✔
400
        ["git@github.com", T1] -> "https://github.com/"++T1;
1✔
401
        _ -> Repos
×
402
    end,
403
    Url = case lists:reverse(Base) of
1✔
404
        [$t,$i,$g,$.|T2] -> lists:reverse(T2);
1✔
405
        _ -> Base
×
406
    end,
407
    case extract(zip, geturl(Url++"/archive/master.zip"), DestDir) of
1✔
408
        ok ->
409
            RepDir = filename:join(DestDir, module_name(Repos)),
1✔
410
            RepDirSpec = filename:join(DestDir, module_spec_name(RepDir)),
1✔
411
            file:rename(RepDir++"-master", RepDirSpec),
1✔
412
            maybe_write_commit_json(Url, RepDirSpec);
1✔
413
        Error ->
414
            Error
×
415
    end.
416

417
copy(From, To) ->
418
    case filelib:is_dir(From) of
3✔
419
        true ->
420
            Copy = fun(F) ->
×
421
                    SubFrom = filename:join(From, F),
×
422
                    SubTo = filename:join(To, F),
×
423
                    copy(SubFrom, SubTo)
×
424
            end,
425
            lists:foldl(fun(ok, ok) -> ok;
×
426
                           (ok, Error) -> Error;
×
427
                           (Error, _) -> Error
×
428
                end, ok,
429
                [Copy(filename:basename(X)) || X<-filelib:wildcard(From++"/*")]);
×
430
        false ->
431
            filelib:ensure_dir(To),
3✔
432
            case file:copy(From, To) of
3✔
433
                {ok, _} -> ok;
3✔
434
                Error -> Error
×
435
            end
436
    end.
437

438
delete_path(Path) ->
439
    case filelib:is_dir(Path) of
1✔
440
        true ->
441
            [delete_path(SubPath) || SubPath <- filelib:wildcard(Path++"/*")],
×
442
            file:del_dir(Path);
×
443
        false ->
444
            file:delete(Path)
1✔
445
    end.
446

447
delete_path(Path, Package) ->
448
    delete_path(filename:join(filename:dirname(Path), Package)).
1✔
449

450
modules_dir() ->
451
    DefaultDir = filename:join(getenv("HOME"), ".ejabberd-modules"),
47✔
452
    getenv("CONTRIB_MODULES_PATH", DefaultDir).
47✔
453

454
sources_dir() ->
455
    filename:join(modules_dir(), "sources").
10✔
456

457
config_dir() ->
458
    DefaultDir = filename:join(modules_dir(), "conf"),
2✔
459
    getenv("CONTRIB_MODULES_CONF_DIR", DefaultDir).
2✔
460

461
-spec modules_configs() -> [binary()].
462
modules_configs() ->
463
    Fs = [{filename:rootname(filename:basename(F)), F}
2✔
464
          || F <- filelib:wildcard(config_dir() ++ "/*.{yml,yaml}")
465
                 ++ filelib:wildcard(modules_dir() ++ "/*/conf/*.{yml,yaml}")],
2✔
466
    [unicode:characters_to_binary(proplists:get_value(F, Fs))
2✔
467
     || F <- proplists:get_keys(Fs)].
2✔
468

469
module_lib_dir(Package) ->
470
    filename:join(modules_dir(), Package).
11✔
471

472
module_ebin_dir(Package) ->
473
    filename:join(module_lib_dir(Package), "ebin").
8✔
474

475
module_src_dir(Package) ->
476
    Rep = module_name(Package),
3✔
477
    SrcDir = sources_dir(),
3✔
478
    Standalone = filelib:wildcard(Rep, SrcDir),
3✔
479
    Jungle = filelib:wildcard("*/"++Rep, SrcDir),
3✔
480
    case Standalone++Jungle of
3✔
481
        [RepDir|_] -> filename:join(SrcDir, RepDir);
3✔
482
        _ -> filename:join(SrcDir, Rep)
×
483
    end.
484

485
module_name(Id) ->
486
    filename:basename(filename:rootname(Id)).
48✔
487

488
module_spec_name(Path) ->
489
    case filelib:wildcard(filename:join(Path++"-master", "*.spec")) of
1✔
490
        "" ->
491
            module_name(Path);
1✔
492
        ModuleName ->
493
            filename:basename(ModuleName, ".spec")
×
494
    end.
495

496
module(Id) ->
497
    misc:binary_to_atom(iolist_to_binary(module_name(Id))).
41✔
498

499
module_spec(Spec) ->
500
    [{path, filename:dirname(Spec)}
41✔
501
      | case consult(Spec) of
502
            {ok, Meta} -> Meta;
41✔
503
            _ -> []
×
504
        end].
505

506
modules_spec(Dir, Path) ->
507
    Wildcard = filename:join(Path, "*.spec"),
28✔
508
    lists:sort(
28✔
509
        [{module(Match), module_spec(filename:join(Dir, Match))}
41✔
510
         || Match <- filelib:wildcard(Wildcard, Dir)]).
28✔
511

512
short_spec({Module, Attrs}) when is_atom(Module), is_list(Attrs) ->
513
    {Module, proplists:get_value(summary, Attrs, "")}.
3✔
514

515
is_contrib_allowed(Config) when is_list(Config) ->
516
    case lists:keyfind(allow_contrib_modules, 1, Config) of
×
517
        false -> true;
×
518
        {_, false} -> false;
×
519
        {_, true} -> true
×
520
    end;
521
is_contrib_allowed(undefined) ->
522
    ejabberd_option:allow_contrib_modules().
2✔
523

524
%% -- build functions
525

526
check_sources(Module) ->
527
    SrcDir = module_src_dir(Module),
×
528
    SpecFile = filename:flatten([Module, ".spec"]),
×
529
    {ok, Dir} = file:get_cwd(),
×
530
    file:set_cwd(SrcDir),
×
531
    HaveSrc = case filelib:is_dir("src") or filelib:is_dir("lib") of
×
532
        true -> [];
×
533
        false -> [{missing, "src (Erlang) or lib (Elixir) sources directory"}]
×
534
    end,
535
    DirCheck = lists:foldl(
×
536
            fun({Type, Name}, Acc) ->
537
                case filelib:Type(Name) of
×
538
                    true -> Acc;
×
539
                    false -> [{missing, Name}|Acc]
×
540
                end
541
            end, HaveSrc, [{is_file, "README.txt"},
542
                           {is_file, "COPYING"},
543
                           {is_file, SpecFile}]),
544
    SpecCheck = case consult(SpecFile) of
×
545
        {ok, Spec} ->
546
            lists:foldl(
×
547
                fun(Key, Acc) ->
548
                    case lists:keysearch(Key, 1, Spec) of
×
549
                        false -> [{missing_meta, Key}|Acc];
×
550
                        {value, {Key, [_NoEmpty|_]}} -> Acc;
×
551
                        {value, {Key, Val}} -> [{invalid_meta, {Key, Val}}|Acc]
×
552
                    end
553
                end, [], [author, summary, home, url]);
554
        {error, Error} ->
555
            [{invalid_spec, Error}]
×
556
    end,
557
    file:set_cwd(Dir),
×
558
    Result = DirCheck ++ SpecCheck,
×
559
    case Result of
×
560
        [] -> ok;
×
561
        _ -> {error, Result}
×
562
    end.
563

564
compile_and_install(Module, Spec, Config) ->
565
    SrcDir = module_src_dir(Module),
1✔
566
    LibDir = module_lib_dir(Module),
1✔
567
    case filelib:is_dir(SrcDir) of
1✔
568
        true ->
569
            case compile_deps(SrcDir) of
1✔
570
                ok ->
571
                    case compile(SrcDir) of
1✔
572
                        ok -> install(Module, Spec, SrcDir, LibDir, Config);
1✔
573
                        Error -> Error
×
574
                    end;
575
                Error ->
576
                    Error
×
577
            end;
578
        false ->
579
            Path = proplists:get_value(url, Spec, ""),
×
580
            case add_sources(Module, Path) of
×
581
                ok -> compile_and_install(Module, Spec, Config);
×
582
                Error -> Error
×
583
            end
584
    end.
585

586
compile_deps(LibDir) ->
587
    Deps = filename:join(LibDir, "deps"),
1✔
588
    case filelib:is_dir(Deps) of
1✔
589
        true -> ok;  % assume deps are included
×
590
        false -> fetch_rebar_deps(LibDir)
1✔
591
    end,
592
    Rs = [compile(Dep) || Dep <- filelib:wildcard(filename:join(Deps, "*"))],
1✔
593
    compile_result(Rs).
1✔
594

595
compile(LibDir) ->
596
    Bin = filename:join(LibDir, "ebin"),
1✔
597
    Lib = filename:join(LibDir, "lib"),
1✔
598
    Src = filename:join(LibDir, "src"),
1✔
599
    Includes = [{i, Inc} || Inc <- filelib:wildcard(LibDir++"/../../**/include")],
1✔
600
    Options = [{outdir, Bin}, {i, LibDir++"/.."} | Includes ++ compile_options()],
1✔
601
    filelib:ensure_dir(filename:join(Bin, ".")),
1✔
602
    [copy(App, filename:join(Bin, filename:basename(App, ".src"))) || App <- filelib:wildcard(Src++"/*.app*")],
1✔
603
    compile_c_files(LibDir),
1✔
604
    Er = [compile_erlang_file(Bin, File, Options)
1✔
605
          || File <- filelib:wildcard(Src++"/**/*.erl")],
1✔
606
    Ex = compile_elixir_files(Bin, filelib:wildcard(Lib ++ "/**/*.ex")),
1✔
607
    compile_result(lists:flatten([Er, Ex])).
1✔
608

609
compile_c_files(LibDir) ->
610
    case file:read_file_info(filename:join(LibDir, "c_src/Makefile")) of
1✔
611
        {ok, _} ->
612
            os:cmd("cd "++LibDir++"; make -C c_src");
×
613
        {error, _} ->
614
            ok
1✔
615
    end.
616

617
compile_result(Results) ->
618
    case lists:dropwhile(
2✔
619
            fun({ok, _}) -> true;
1✔
620
               (_) -> false
1✔
621
            end, Results) of
622
        [] -> ok;
1✔
623
        [Error|_] -> Error
1✔
624
    end.
625

626
maybe_define_lager_macro() ->
627
    case list_to_integer(erlang:system_info(otp_release)) < 22 of
1✔
628
        true -> [{d, 'LAGER'}];
×
629
        false -> []
1✔
630
    end.
631

632
compile_options() ->
633
    [verbose, report_errors, report_warnings, debug_info, ?ALL_DEFS]
634
    ++ maybe_define_lager_macro()
1✔
635
    ++ [{i, filename:join(app_dir(App), "include")}
4✔
636
        || App <- [fast_xml, xmpp, p1_utils, ejabberd]]
1✔
637
    ++ [{i, filename:join(mod_dir(Mod), "include")}
×
638
        || Mod <- installed()].
1✔
639

640
app_dir(App) ->
641
    case code:lib_dir(App) of
4✔
642
        {error, bad_name} ->
643
            case code:which(App) of
×
644
                Beam when is_list(Beam) ->
645
                    filename:dirname(filename:dirname(Beam));
×
646
                _ ->
647
                    "."
×
648
            end;
649
        Dir ->
650
            Dir
4✔
651
    end.
652

653
mod_dir({Package, Spec}) ->
654
    Default = filename:join(modules_dir(), Package),
×
655
    proplists:get_value(path, Spec, Default).
×
656

657
compile_erlang_file(Dest, File) ->
658
    compile_erlang_file(Dest, File, compile_options()).
×
659

660
compile_erlang_file(Dest, File, ErlOptions) ->
661
    Options = [{outdir, Dest} | ErlOptions],
1✔
662
    case compile:file(File, Options) of
1✔
663
        {ok, Module} -> {ok, Module};
1✔
664
        {ok, Module, _} -> {ok, Module};
×
665
        {ok, Module, _, _} -> {ok, Module};
×
666
        error -> {error, {compilation_failed, File}};
×
667
        {error, E, W} -> {error, {compilation_failed, File, E, W}}
×
668
    end.
669

670
-ifdef(ELIXIR_ENABLED).
671
compile_elixir_files(Dest, [File | _] = Files) when is_list(Dest) and is_list(File) ->
672
  BinFiles = [list_to_binary(F) || F <- Files],
673
  compile_elixir_files(list_to_binary(Dest), BinFiles);
674

675
compile_elixir_files(Dest, Files) ->
676
  try 'Elixir.Kernel.ParallelCompiler':compile_to_path(Files, Dest, [{return_diagnostics, true}]) of
677
      {ok, Modules, []} when is_list(Modules) ->
678
          {ok, Modules};
679
      {ok, Modules, Warnings} when is_list(Modules) ->
680
          ?WARNING_MSG("Warnings compiling module: ~n~p", [Warnings]),
681
          {ok, Modules}
682
  catch
683
    A:B ->
684
          ?ERROR_MSG("Problem ~p compiling Elixir files: ~p~nFiles: ~p", [A, B, Files]),
685
          {error, {compilation_failed, Files}}
686
  end.
687
-else.
688
compile_elixir_files(_, []) ->
689
    ok;
1✔
690
compile_elixir_files(_, Files) ->
691
    ErrorString = "Attempted to compile Elixir files, but Elixir support is "
×
692
        "not available in ejabberd. Try compiling ejabberd using "
693
        "'./configure --enable-elixir' or './configure --with-rebar=mix'",
694
    ?ERROR_MSG(ErrorString, []),
×
695
    io:format("Error: " ++ ErrorString ++ "~n", []),
×
696
    {error, {elixir_not_available, Files}}.
×
697
-endif.
698

699
install(Module, Spec, SrcDir, LibDir, Config) ->
700
    {ok, CurDir} = file:get_cwd(),
1✔
701
    file:set_cwd(SrcDir),
1✔
702
    Files1 = [{File, copy(File, filename:join(LibDir, File))}
1✔
703
                  || File <- filelib:wildcard("{ebin,priv,conf,include}/**")],
1✔
704
    Files2 = [{File, copy(File, filename:join(LibDir, filename:join(lists:nthtail(2,filename:split(File)))))}
1✔
705
                  || File <- filelib:wildcard("deps/*/ebin/**")],
1✔
706
    Files3 = [{File, copy(File, filename:join(LibDir, File))}
1✔
707
                  || File <- filelib:wildcard("deps/*/priv/**")],
1✔
708
    Errors = lists:dropwhile(fun({_, ok}) -> true;
1✔
709
                                (_) -> false
×
710
            end, Files1++Files2++Files3),
711
    inform_module_configuration(Module, LibDir, Files1, Config),
1✔
712
    Result = case Errors of
1✔
713
        [{F, {error, E}}|_] ->
714
            {error, {F, E}};
×
715
        [] ->
716
            SpecPath = proplists:get_value(path, Spec),
1✔
717
            SpecFile = filename:flatten([Module, ".spec"]),
1✔
718
            copy(filename:join(SpecPath, SpecFile), filename:join(LibDir, SpecFile))
1✔
719
    end,
720
    file:set_cwd(CurDir),
1✔
721
    Result.
1✔
722

723
inform_module_configuration(Module, LibDir, Files1, Config) ->
724
    Res = lists:filter(fun({[$c, $o, $n, $f |_], ok}) -> true;
1✔
725
                          (_) -> false
1✔
726
            end, Files1),
727
    AlreadyConfigured = lists:keymember(Module, 1, get_modules(Config)),
1✔
728
    case {Res, AlreadyConfigured} of
1✔
729
        {[{ConfigPath, ok}], false} ->
730
            FullConfigPath = filename:join(LibDir, ConfigPath),
1✔
731
            io:format("Module ~p has been installed and started.~n"
1✔
732
                      "It's configured in the file:~n  ~s~n"
733
                      "Configure the module in that file, or remove it~n"
734
                      "and configure in your main ejabberd.yml~n",
735
                      [Module, FullConfigPath]);
736
        {[{ConfigPath, ok}], true} ->
737
            FullConfigPath = filename:join(LibDir, ConfigPath),
×
738
            file:rename(FullConfigPath, FullConfigPath++".example"),
×
739
            io:format("Module ~p has been installed and started.~n"
×
740
                      "The ~p configuration in your ejabberd.yml is used.~n",
741
                      [Module, Module]);
742
        {[], _} ->
743
            io:format("Module ~p has been installed.~n"
×
744
                      "Now you can configure it in your ejabberd.yml~n",
745
                      [Module])
746
    end.
747

748
get_modules(Config) when is_list(Config) ->
749
    {modules, Modules} = lists:keyfind(modules, 1, Config),
×
750
    Modules;
×
751
get_modules(undefined) ->
752
    ejabberd_config:get_option(modules).
1✔
753

754
%% -- minimalist rebar spec parser, only support git
755

756
fetch_rebar_deps(SrcDir) ->
757
    case rebar_deps(filename:join(SrcDir, "rebar.config"))
1✔
758
      ++ rebar_deps(filename:join(SrcDir, "rebar.config.script")) of
759
        [] ->
760
            ok;
1✔
761
        Deps ->
762
            {ok, CurDir} = file:get_cwd(),
×
763
            file:set_cwd(SrcDir),
×
764
            filelib:ensure_dir(filename:join("deps", ".")),
×
NEW
765
            lists:foreach(fun({App, Cmd}) ->
×
NEW
766
                        io:format("Fetching dependency ~s: ", [App]),
×
NEW
767
                        Result = os:cmd("cd deps; "++Cmd++"; cd .."),
×
NEW
768
                        io:format("~s", [Result])
×
769
                end, Deps),
770
            file:set_cwd(CurDir)
×
771
    end.
772

773
rebar_deps(Script) ->
774
    case file:script(Script) of
4✔
775
        {ok, Config} when is_list(Config) ->
776
            [rebar_dep(Dep) || Dep <- proplists:get_value(deps, Config, [])];
×
777
        {ok, {deps, Deps}} ->
778
            [rebar_dep(Dep) || Dep <- Deps];
2✔
779
        _ ->
780
            []
2✔
781
    end.
782

783
rebar_dep({App, Version, Git}) when Version /= ".*" ->
NEW
784
    AppS = atom_to_list(App),
×
NEW
785
    Help = os:cmd("mix hex.package"),
×
NEW
786
    case string:find(Help, "mix hex.package fetch") /= nomatch of
×
787
        true ->
NEW
788
            {App, "mix hex.package fetch "++AppS++" "++Version++" --unpack"};
×
789
        false ->
NEW
790
            io:format("I'll download ~p using git because I can't use Mix "
×
791
                      "to fetch from hex.pm:~n~s", [AppS, help]),
NEW
792
            rebar_dep({App, ".*", Git})
×
793
    end;
794

795
rebar_dep({App, _, {git, Url}}) ->
796
    {App, "git clone "++Url++" "++filename:basename(App)};
×
797
rebar_dep({App, _, {git, Url, {branch, Ref}}}) ->
798
    {App, "git clone -n "++Url++" "++filename:basename(App)++
×
799
     "; (cd "++filename:basename(App)++
800
     "; git checkout -q origin/"++Ref++")"};
801
rebar_dep({App, _, {git, Url, {tag, Ref}}}) ->
802
    {App, "git clone -n "++Url++" "++filename:basename(App)++
×
803
     "; (cd "++filename:basename(App)++
804
     "; git checkout -q "++Ref++")"};
805
rebar_dep({App, _, {git, Url, Ref}}) ->
806
    {App, "git clone -n "++Url++" "++filename:basename(App)++
×
807
     "; (cd "++filename:basename(App)++
808
     "; git checkout -q "++Ref++")"}.
809

810
module_deps_dirs(Module) ->
811
    SrcDir = module_src_dir(Module),
1✔
812
    LibDir = module_lib_dir(Module),
1✔
813
    DepsDir = filename:join(LibDir, "deps"),
1✔
814
    Deps = rebar_deps(filename:join(SrcDir, "rebar.config"))
1✔
815
      ++ rebar_deps(filename:join(SrcDir, "rebar.config.script")),
816
    [filename:join(DepsDir, App) || {App, _Cmd} <- Deps].
1✔
817

818
%% -- YAML spec parser
819

820
consult(File) ->
821
    case fast_yaml:decode_from_file(File, [plain_as_atom]) of
41✔
822
        {ok, []} -> {ok, []};
×
823
        {ok, [Doc|_]} -> {ok, [format(Spec) || Spec <- Doc]};
41✔
824
        {error, Err} -> {error, fast_yaml:format_error(Err)}
×
825
    end.
826

827
format({Key, Val}) when is_binary(Val) ->
828
    {Key, binary_to_list(Val)};
205✔
829
format({Key, Val}) -> % TODO: improve Yaml parsing
830
    {Key, Val}.
×
831

832
%% -- COMMIT.json
833

834
maybe_write_commit_json(Url, RepDir) ->
835
    case (os:getenv("GITHUB_ACTIONS") == "true") of
1✔
836
        true ->
837
            ok;
1✔
838
        false ->
839
            write_commit_json(Url, RepDir)
×
840
    end.
841

842
write_commit_json(Url, RepDir) ->
843
    Url2 = string_replace(Url, "https://github.com", "https://api.github.com/repos"),
×
844
    BranchUrl = lists:flatten(Url2 ++ "/branches/master"),
×
845
    case geturl(BranchUrl) of
×
846
        {ok, _Headers, Body} ->
847
            {ok, F} = file:open(filename:join(RepDir, "COMMIT.json"), [raw, write]),
×
848
            file:write(F, Body),
×
849
            file:close(F);
×
850
        {error, Reason} ->
851
            Reason
×
852
    end.
853

854
find_commit_json(Attrs) ->
855
    FromPath = get_module_path(Attrs),
1✔
856
    case {find_commit_json_path(FromPath),
1✔
857
          find_commit_json_path(filename:join(FromPath, ".."))}
858
    of
859
        {{ok, FromFile}, _} ->
860
            FromFile;
×
861
        {_, {ok, FromFile}} ->
862
            FromFile;
×
863
        _ ->
864
            not_found
1✔
865
    end.
866

867
-ifdef(HAVE_URI_STRING). %% Erlang/OTP 20 or higher can use this:
868
string_replace(Subject, Pattern, Replacement) ->
869
    string:replace(Subject, Pattern, Replacement).
×
870

871
find_commit_json_path(Path) ->
872
    filelib:find_file("COMMIT.json", Path).
2✔
873
-else. % Workaround for Erlang/OTP older than 20:
874
string_replace(Subject, Pattern, Replacement) ->
875
    B = binary:replace(list_to_binary(Subject),
876
                       list_to_binary(Pattern),
877
                       list_to_binary(Replacement)),
878
    binary_to_list(B).
879

880
find_commit_json_path(Path) ->
881
    case filelib:wildcard("COMMIT.json", Path) of
882
        [] ->
883
            {error, commit_json_not_found};
884
        ["COMMIT.json"] = File ->
885
            {ok, filename:join(Path, File)}
886
    end.
887
-endif.
888

889
copy_commit_json(Package, Attrs) ->
890
    DestPath = module_lib_dir(Package),
1✔
891
    case find_commit_json(Attrs) of
1✔
892
        not_found ->
893
            ok;
1✔
894
        FromFile ->
895
            file:copy(FromFile, filename:join(DestPath, "COMMIT.json"))
×
896
    end.
897

898
get_commit_details(Dirname) ->
899
    RepDir = filename:join(sources_dir(), Dirname),
×
900
    get_commit_details2(filename:join(RepDir, "COMMIT.json")).
×
901

902
get_commit_details2(Path) ->
903
    case file:read_file(Path) of
×
904
        {ok, Body} ->
905
            parse_details(Body);
×
906
        _ ->
907
            #{sha => unknown_sha,
×
908
              date => <<>>,
909
              message => <<>>,
910
              html => <<>>,
911
              author_name => <<>>,
912
              commit_html_url => <<>>}
913
    end.
914

915
parse_details(Body) ->
916
    Contents = misc:json_decode(Body),
×
917

918
    {ok, Commit} = maps:find(<<"commit">>, Contents),
×
919
    {ok, Sha} = maps:find(<<"sha">>, Commit),
×
920
    {ok, CommitHtmlUrl} = maps:find(<<"html_url">>, Commit),
×
921

922
    {ok, Commit2} = maps:find(<<"commit">>, Commit),
×
923
    {ok, Message} = maps:find(<<"message">>, Commit2),
×
924
    {ok, Author} = maps:find(<<"author">>, Commit2),
×
925
    {ok, AuthorName} = maps:find(<<"name">>, Author),
×
926
    {ok, Committer} = maps:find(<<"committer">>, Commit2),
×
927
    {ok, Date} = maps:find(<<"date">>, Committer),
×
928

929
    {ok, Links} = maps:find(<<"_links">>, Contents),
×
930
    {ok, Html} = maps:find(<<"html">>, Links),
×
931

932
    #{sha => Sha,
×
933
      date => Date,
934
      message => Message,
935
      html => Html,
936
      author_name => AuthorName,
937
      commit_html_url => CommitHtmlUrl}.
938

939
%% -- Web Admin
940

941
-define(AXC(URL, Attributes, Text),
942
        ?XAE(<<"a">>, [{<<"href">>, URL} | Attributes], [?C(Text)])
943
       ).
944

945
-define(INPUTCHECKED(Type, Name, Value),
946
        ?XA(<<"input">>,
947
            [{<<"type">>, Type},
948
             {<<"name">>, Name},
949
             {<<"disabled">>, <<"true">>},
950
             {<<"checked">>, <<"true">>},
951
             {<<"value">>, Value}
952
            ]
953
           )
954
       ).
955

956
%% @format-begin
957

958
web_menu_node(Acc, _Node, _Lang) ->
959
    Acc
960
    ++ [{<<"contrib">>, <<"Contrib Modules (Detailed)">>},
×
961
        {<<"contrib-api">>, <<"Contrib Modules (API)">>}].
962

963
web_page_node(_,
964
              Node,
965
              #request{path = [<<"contrib">>],
966
                       q = Query,
967
                       lang = Lang} =
968
                  R) ->
969
    Title =
×
970
        ?H1GL(<<"Contrib Modules (Detailed)">>,
971
              <<"../../developer/extending-ejabberd/modules/#ejabberd-contrib">>,
972
              <<"ejabberd-contrib">>),
973
    Res = [ejabberd_cluster:call(Node,
×
974
                                 ejabberd_web_admin,
975
                                 make_command,
976
                                 [webadmin_node_contrib,
977
                                  R,
978
                                  [{<<"node">>, Node}, {<<"query">>, Query}, {<<"lang">>, Lang}],
979
                                  []])],
980
    {stop, Title ++ Res};
×
981
web_page_node(_, Node, #request{path = [<<"contrib-api">> | RPath]} = R) ->
982
    Title =
×
983
        ?H1GL(<<"Contrib Modules (API)">>,
984
              <<"../../developer/extending-ejabberd/modules/#ejabberd-contrib">>,
985
              <<"ejabberd-contrib">>),
986
    _TableInstalled = make_table_installed(Node, R, RPath),
×
987
    _TableAvailable = make_table_available(Node, R, RPath),
×
988
    TableInstalled = make_table_installed(Node, R, RPath),
×
989
    TableAvailable = make_table_available(Node, R, RPath),
×
990
    Res = [?X(<<"hr">>),
×
991
           ?XAC(<<"h2">>, [{<<"id">>, <<"specs">>}], <<"Specs">>),
992
           ?XE(<<"blockquote">>,
993
               [ejabberd_cluster:call(Node,
994
                                      ejabberd_web_admin,
995
                                      make_command,
996
                                      [modules_update_specs, R])]),
997
           ?X(<<"hr">>),
998
           ?XAC(<<"h2">>, [{<<"id">>, <<"installed">>}], <<"Installed">>),
999
           ?XE(<<"blockquote">>,
1000
               [ejabberd_cluster:call(Node,
1001
                                      ejabberd_web_admin,
1002
                                      make_command,
1003
                                      [modules_installed, R, [], [{only, presentation}]]),
1004
                ejabberd_cluster:call(Node,
1005
                                      ejabberd_web_admin,
1006
                                      make_command,
1007
                                      [module_uninstall, R, [], [{only, presentation}]]),
1008
                ejabberd_cluster:call(Node,
1009
                                      ejabberd_web_admin,
1010
                                      make_command,
1011
                                      [module_upgrade, R, [], [{only, presentation}]]),
1012
                TableInstalled]),
1013
           ?X(<<"hr">>),
1014
           ?XAC(<<"h2">>, [{<<"id">>, <<"available">>}], <<"Available">>),
1015
           ?XE(<<"blockquote">>,
1016
               [ejabberd_cluster:call(Node,
1017
                                      ejabberd_web_admin,
1018
                                      make_command,
1019
                                      [modules_available, R, [], [{only, presentation}]]),
1020
                ejabberd_cluster:call(Node,
1021
                                      ejabberd_web_admin,
1022
                                      make_command,
1023
                                      [module_install, R, [], [{only, presentation}]]),
1024
                TableAvailable,
1025
                ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [module_check, R])])],
1026
    {stop, Title ++ Res};
×
1027
web_page_node(Acc, _, _) ->
1028
    Acc.
×
1029

1030
make_table_installed(Node, R, RPath) ->
1031
    Columns = [<<"Name">>, <<"Summary">>, <<"">>, <<"">>],
×
1032
    ModulesInstalled =
×
1033
        ejabberd_cluster:call(Node,
1034
                              ejabberd_web_admin,
1035
                              make_command_raw_value,
1036
                              [modules_installed, R, []]),
1037
    Rows =
×
1038
        lists:map(fun({Name, Summary}) ->
1039
                     NameBin = misc:atom_to_binary(Name),
×
1040
                     Upgrade =
×
1041
                         ejabberd_cluster:call(Node,
1042
                                               ejabberd_web_admin,
1043
                                               make_command,
1044
                                               [module_upgrade,
1045
                                                R,
1046
                                                [{<<"module">>, NameBin}],
1047
                                                [{only, button}, {input_name_append, [NameBin]}]]),
1048
                     Uninstall =
×
1049
                         ejabberd_cluster:call(Node,
1050
                                               ejabberd_web_admin,
1051
                                               make_command,
1052
                                               [module_uninstall,
1053
                                                R,
1054
                                                [{<<"module">>, NameBin}],
1055
                                                [{only, button},
1056
                                                 {style, danger},
1057
                                                 {input_name_append, [NameBin]}]]),
1058
                     {?C(NameBin), ?C(list_to_binary(Summary)), Upgrade, Uninstall}
×
1059
                  end,
1060
                  ModulesInstalled),
1061
    ejabberd_web_admin:make_table(200, RPath, Columns, Rows).
×
1062

1063
make_table_available(Node, R, RPath) ->
1064
    Columns = [<<"Name">>, <<"Summary">>, <<"">>],
×
1065
    ModulesAll =
×
1066
        ejabberd_cluster:call(Node,
1067
                              ejabberd_web_admin,
1068
                              make_command_raw_value,
1069
                              [modules_available, R, []]),
1070
    ModulesInstalled =
×
1071
        ejabberd_cluster:call(Node,
1072
                              ejabberd_web_admin,
1073
                              make_command_raw_value,
1074
                              [modules_installed, R, []]),
1075
    ModulesNotInstalled =
×
1076
        lists:filter(fun({Mod, _}) -> not lists:keymember(Mod, 1, ModulesInstalled) end,
×
1077
                     ModulesAll),
1078
    Rows =
×
1079
        lists:map(fun({Name, Summary}) ->
1080
                     NameBin = misc:atom_to_binary(Name),
×
1081
                     Install =
×
1082
                         ejabberd_cluster:call(Node,
1083
                                               ejabberd_web_admin,
1084
                                               make_command,
1085
                                               [module_install,
1086
                                                R,
1087
                                                [{<<"module">>, NameBin}],
1088
                                                [{only, button}, {input_name_append, [NameBin]}]]),
1089
                     {?C(NameBin), ?C(list_to_binary(Summary)), Install}
×
1090
                  end,
1091
                  ModulesNotInstalled),
1092
    ejabberd_web_admin:make_table(200, RPath, Columns, Rows).
×
1093

1094
webadmin_node_contrib(Node, Query, Lang) ->
1095
    QueryRes = list_modules_parse_query(Query),
×
1096
    Contents = get_content(Node, Query, Lang),
×
1097
    Result =
×
1098
        case QueryRes of
1099
            ok ->
1100
                [?XREST(?T("Submitted"))];
×
1101
            nothing ->
1102
                []
×
1103
        end,
1104
    Result ++ Contents.
×
1105
%% @format-end
1106

1107
get_module_home(Module, Attrs) ->
1108
    case get_module_information(home, Attrs) of
×
1109
        "https://github.com/processone/ejabberd-contrib/tree/master/" = P1 ->
1110
            P1 ++ atom_to_list(Module);
×
1111
        Other ->
1112
            Other
×
1113
    end.
1114

1115
get_module_summary(Attrs) ->
1116
    get_module_information(summary, Attrs).
×
1117

1118
get_module_author(Attrs) ->
1119
    get_module_information(author, Attrs).
×
1120

1121
get_module_path(Attrs) ->
1122
    get_module_information(path, Attrs).
1✔
1123

1124
get_module_information(Attribute, Attrs) ->
1125
    case lists:keyfind(Attribute, 1, Attrs) of
1✔
1126
        false -> "";
×
1127
        {_, Value} -> Value
1✔
1128
    end.
1129

1130
get_installed_module_el({ModAtom, Attrs}, Lang) ->
1131
    Mod = misc:atom_to_binary(ModAtom),
×
1132
    Home = list_to_binary(get_module_home(ModAtom, Attrs)),
×
1133
    Summary = list_to_binary(get_module_summary(Attrs)),
×
1134
    Author = list_to_binary(get_module_author(Attrs)),
×
1135
    FromPath = get_module_path(Attrs),
×
1136
    FromFile = case find_commit_json_path(FromPath) of
×
1137
                   {ok, FF} -> FF;
×
1138
                   {error, _} -> "dummypath"
×
1139
               end,
1140
    #{sha := CommitSha,
×
1141
      date := CommitDate,
1142
      message := CommitMessage,
1143
      author_name := CommitAuthorName,
1144
      commit_html_url := CommitHtmlUrl} = get_commit_details2(FromFile),
1145

1146
    [SourceSpec] = [S || {M, S} <- available(), M == ModAtom],
×
1147
    SourceFile = find_commit_json(SourceSpec),
×
1148
    #{sha := SourceSha,
×
1149
      date := SourceDate,
1150
      message := SourceMessage,
1151
      author_name := SourceAuthorName,
1152
      commit_html_url := SourceHtmlUrl} = get_commit_details2(SourceFile),
1153

1154
    UpgradeEls =
×
1155
        case CommitSha == SourceSha of
1156
            true ->
1157
                [];
×
1158
            false ->
1159
                SourceTitleEl = make_title_el(SourceDate, SourceMessage, SourceAuthorName),
×
1160
                [?XE(<<"td">>,
×
1161
                     [?INPUT(<<"checkbox">>, <<"selected_upgrade">>, Mod),
1162
                      ?C(<<" ">>),
1163
                      ?AXC(SourceHtmlUrl, [SourceTitleEl], binary:part(SourceSha, {0, 8}))
1164
                     ]
1165
                    )
1166
                ]
1167
        end,
1168

1169
    Started =
×
1170
        case gen_mod:is_loaded(hd(ejabberd_option:hosts()), ModAtom) of
1171
            false ->
1172
                [?C(<<" ">>)];
×
1173
            true ->
1174
                []
×
1175
        end,
1176
    TitleEl = make_title_el(CommitDate, CommitMessage, CommitAuthorName),
×
1177
    Status = get_module_status_el(ModAtom),
×
1178
    HomeTitleEl = make_home_title_el(Summary, Author),
×
1179
    ?XE(<<"tr">>,
×
1180
        [?XE(<<"td">>, [?AXC(Home, [HomeTitleEl], Mod)]),
1181
         ?XE(<<"td">>,
1182
             [?INPUTTD(<<"checkbox">>, <<"selected_uninstall">>, Mod),
1183
              ?C(<<" ">>),
1184
              get_commit_link(CommitHtmlUrl, TitleEl, CommitSha),
1185
              ?C(<<" - ">>)]
1186
             ++ Started
1187
             ++ Status)
1188
        | UpgradeEls]).
1189

1190
get_module_status_el(ModAtom) ->
1191
    case {get_module_status(ModAtom),
×
1192
          get_module_status(elixir_module_name(ModAtom))} of
1193
        {Str, unknown} when is_list(Str) ->
1194
            [?C(<<" ">>), ?C(Str)];
×
1195
        {unknown, Str} when is_list(Str) ->
1196
            [?C(<<" ">>), ?C(Str)];
×
1197
        {unknown, unknown} ->
1198
            []
×
1199
    end.
1200

1201
get_module_status(Module) ->
1202
    try Module:mod_status() of
×
1203
        Str when is_list(Str) ->
1204
            Str
×
1205
    catch
1206
        _:_ ->
1207
            unknown
×
1208
    end.
1209

1210
%% When a module named mod_whatever in ejabberd-modules
1211
%% is written in Elixir, its runtime name is 'Elixir.ModWhatever'
1212
get_runtime_module_name(Module) ->
1213
    case is_elixir_module(Module) of
1✔
1214
        true -> elixir_module_name(Module);
×
1215
        false -> Module
1✔
1216
    end.
1217

1218
is_elixir_module(Module) ->
1219
    LibDir = module_src_dir(Module),
1✔
1220
    Lib = filename:join(LibDir, "lib"),
1✔
1221
    Src = filename:join(LibDir, "src"),
1✔
1222
    case {filelib:wildcard(Lib++"/*.{ex}"),
1✔
1223
          filelib:wildcard(Src++"/*.{erl}")} of
1224
        {[_ | _], []} ->
1225
            true;
×
1226
        {[], _} ->
1227
            false
1✔
1228
    end.
1229

1230
%% Converts mod_some_thing to Elixir.ModSomeThing
1231
elixir_module_name(ModAtom) ->
1232
    list_to_atom("Elixir." ++ elixir_module_name("_" ++ atom_to_list(ModAtom), [])).
×
1233

1234
elixir_module_name([], Res) ->
1235
    lists:reverse(Res);
×
1236
elixir_module_name([$_, Char | Remaining], Res) ->
1237
    [Upper] = uppercase([Char]),
×
1238
    elixir_module_name(Remaining, [Upper | Res]);
×
1239
elixir_module_name([Char | Remaining], Res) ->
1240
    elixir_module_name(Remaining, [Char | Res]).
×
1241

1242
-ifdef(HAVE_URI_STRING).
1243
uppercase(String) ->
1244
    string:uppercase(String). % OTP 20 or higher
×
1245
-else.
1246
uppercase(String) ->
1247
    string:to_upper(String). % OTP older than 20
1248
-endif.
1249

1250
get_available_module_el({ModAtom, Attrs}) ->
1251
    Installed = installed(),
×
1252
    Mod = misc:atom_to_binary(ModAtom),
×
1253
    Home = list_to_binary(get_module_home(ModAtom, Attrs)),
×
1254
    Summary = list_to_binary(get_module_summary(Attrs)),
×
1255
    Author = list_to_binary(get_module_author(Attrs)),
×
1256
    HomeTitleEl = make_home_title_el(Summary, Author),
×
1257
    InstallCheckbox =
×
1258
        case lists:keymember(ModAtom, 1, Installed) of
1259
            false -> [?INPUT(<<"checkbox">>, <<"selected_install">>, Mod)];
×
1260
            true -> [?INPUTCHECKED(<<"checkbox">>, <<"selected_install">>, Mod)]
×
1261
        end,
1262
    ?XE(<<"tr">>,
×
1263
        [?XE(<<"td">>, InstallCheckbox ++ [?C(<<" ">>), ?AXC(Home, [HomeTitleEl], Mod)]),
1264
         ?XE(<<"td">>, [?C(Summary)])]).
1265

1266
get_installed_modules_table(Lang) ->
1267
    Modules = installed(),
×
1268
    Tail = [?XE(<<"tr">>,
×
1269
                [?XE(<<"td">>, []),
1270
                 ?XE(<<"td">>,
1271
                     [?INPUTTD(<<"submit">>, <<"uninstall">>, ?T("Uninstall"))]
1272
                    ),
1273
                 ?XE(<<"td">>,
1274
                     [?INPUTT(<<"submit">>, <<"upgrade">>, ?T("Upgrade"))]
1275
                    )
1276
                ]
1277
               )
1278
           ],
1279
    TBody = [get_installed_module_el(Module, Lang) || Module <- lists:sort(Modules)],
×
1280
    ?XAE(<<"table">>,
×
1281
         [],
1282
         [?XE(<<"tbody">>, TBody ++ Tail)]
1283
        ).
1284

1285
get_available_modules_table(Lang) ->
1286
    Modules = get_available_notinstalled(),
×
1287
    Tail = [?XE(<<"tr">>,
×
1288
                [?XE(<<"td">>,
1289
                     [?INPUTT(<<"submit">>, <<"install">>, ?T("Install"))]
1290
                    )
1291
                ]
1292
               )
1293
           ],
1294
    TBody = [get_available_module_el(Module) || Module <- lists:sort(Modules)],
×
1295
    ?XAE(<<"table">>,
×
1296
         [],
1297
         [?XE(<<"tbody">>, TBody ++ Tail)]
1298
        ).
1299

1300
make_title_el(Date, Message, AuthorName) ->
1301
    LinkTitle = <<Message/binary, "\n", AuthorName/binary, "\n", Date/binary>>,
×
1302
    {<<"title">>, LinkTitle}.
×
1303

1304
make_home_title_el(Summary, Author) ->
1305
    LinkTitle = <<Summary/binary, "\n", Author/binary>>,
×
1306
    {<<"title">>, LinkTitle}.
×
1307

1308
get_commit_link(_CommitHtmlUrl, _TitleErl, unknown_sha) ->
1309
    ?C(<<"Please Update Specs">>);
×
1310
get_commit_link(CommitHtmlUrl, TitleEl, CommitSha) ->
1311
    ?AXC(CommitHtmlUrl, [TitleEl], binary:part(CommitSha, {0, 8})).
×
1312

1313
get_content(Node, Query, Lang) ->
1314
    {{_CommandCtl}, _Res} =
×
1315
        case catch parse_and_execute(Query, Node) of
1316
            {'EXIT', _} -> {{""}, <<"">>};
×
1317
            Result_tuple -> Result_tuple
×
1318
        end,
1319

1320
    AvailableModulesEls = get_available_modules_table(Lang),
×
1321
    InstalledModulesEls = get_installed_modules_table(Lang),
×
1322

1323
    Sources = get_sources_list(),
×
1324
    SourceEls = (?XAE(<<"table">>,
×
1325
                      [],
1326
                      [?XE(<<"tbody">>,
1327
                           (lists:map(
1328
                              fun(Dirname) ->
1329
                                      #{sha := CommitSha,
×
1330
                                        date := CommitDate,
1331
                                        message := CommitMessage,
1332
                                        html := Html,
1333
                                        author_name := AuthorName,
1334
                                        commit_html_url := CommitHtmlUrl
1335
                                       } = get_commit_details(Dirname),
1336
                                      TitleEl = make_title_el(CommitDate, CommitMessage, AuthorName),
×
1337
                                      ?XE(<<"tr">>,
×
1338
                                          [?XE(<<"td">>, [?AC(Html, Dirname)]),
1339
                                           ?XE(<<"td">>,
1340
                                               [get_commit_link(CommitHtmlUrl, TitleEl, CommitSha)]
1341
                                              ),
1342
                                           ?XE(<<"td">>, [?C(CommitMessage)])
1343
                                          ])
1344
                              end,
1345
                              lists:sort(Sources)
1346
                             ))
1347
                          )
1348
                      ]
1349
                     )),
1350

1351
    [?XC(<<"p">>,
×
1352
         translate:translate(
1353
           Lang, ?T("Update specs to get modules source, then install desired ones.")
1354
          )
1355
        ),
1356
     ?XAE(<<"form">>,
1357
          [{<<"method">>, <<"post">>}],
1358
          [?XCT(<<"h3">>, ?T("Sources Specs:")),
1359
           SourceEls,
1360
           ?BR,
1361
           ?INPUTT(<<"submit">>,
1362
                   <<"updatespecs">>,
1363
                   translate:translate(Lang, ?T("Update Specs"))),
1364

1365
           ?XCT(<<"h3">>, ?T("Installed Modules:")),
1366
           InstalledModulesEls,
1367
           ?BR,
1368

1369
           ?XCT(<<"h3">>, ?T("Other Modules Available:")),
1370
           AvailableModulesEls
1371
          ]
1372
         )
1373
    ].
1374

1375
get_sources_list() ->
1376
    case file:list_dir(sources_dir()) of
×
1377
        {ok, Filenames} -> Filenames;
×
1378
        {error, enoent} -> []
×
1379
    end.
1380

1381
get_available_notinstalled() ->
1382
    Installed = installed(),
×
1383
    lists:filter(
×
1384
      fun({Mod, _}) ->
1385
              not lists:keymember(Mod, 1, Installed)
×
1386
      end,
1387
      available()
1388
     ).
1389

1390
parse_and_execute(Query, Node) ->
1391
    {[Exec], _} = lists:partition(
×
1392
                    fun(ExType) ->
1393
                            lists:keymember(ExType, 1, Query)
×
1394
                    end,
1395
                    [<<"updatespecs">>]
1396
                   ),
1397
    Commands = {get_val(<<"updatespecs">>, Query)},
×
1398
    {_, R} = parse1_command(Exec, Commands, Node),
×
1399
    {Commands, R}.
×
1400

1401
get_val(Val, Query) ->
1402
    {value, {_, R}} = lists:keysearch(Val, 1, Query),
×
1403
    binary_to_list(R).
×
1404

1405
parse1_command(<<"updatespecs">>, {_}, _Node) ->
1406
    Res = update(),
×
1407
    {oook, io_lib:format("~p", [Res])}.
×
1408

1409
list_modules_parse_query(Query) ->
1410
    case {lists:keysearch(<<"install">>, 1, Query),
×
1411
          lists:keysearch(<<"upgrade">>, 1, Query),
1412
          lists:keysearch(<<"uninstall">>, 1, Query)}
1413
    of
1414
        {{value, _}, _, _} -> list_modules_parse_install(Query);
×
1415
        {_, {value, _}, _} -> list_modules_parse_upgrade(Query);
×
1416
        {_, _, {value, _}} -> list_modules_parse_uninstall(Query);
×
1417
        _ -> nothing
×
1418
    end.
1419

1420
list_modules_parse_install(Query) ->
1421
    lists:foreach(
×
1422
      fun({Mod, _}) ->
1423
              ModBin = misc:atom_to_binary(Mod),
×
1424
              case lists:member({<<"selected_install">>, ModBin}, Query) of
×
1425
                  true -> install(Mod);
×
1426
                  _ -> ok
×
1427
              end
1428
      end,
1429
      get_available_notinstalled()),
1430
    ok.
×
1431

1432
list_modules_parse_upgrade(Query) ->
1433
    lists:foreach(
×
1434
      fun({Mod, _}) ->
1435
              ModBin = misc:atom_to_binary(Mod),
×
1436
              case lists:member({<<"selected_upgrade">>, ModBin}, Query) of
×
1437
                  true -> upgrade(Mod);
×
1438
                  _ -> ok
×
1439
              end
1440
      end,
1441
      installed()),
1442
    ok.
×
1443

1444
list_modules_parse_uninstall(Query) ->
1445
    lists:foreach(
×
1446
      fun({Mod, _}) ->
1447
              ModBin = misc:atom_to_binary(Mod),
×
1448
              case lists:member({<<"selected_uninstall">>, ModBin}, Query) of
×
1449
                  true -> uninstall(Mod);
×
1450
                  _ -> ok
×
1451
              end
1452
      end,
1453
      installed()),
1454
    ok.
×
1455

1456
install_contrib_modules(Modules, Config) ->
1457
    lists:filter(fun(Module) ->
×
1458
                         case install(misc:atom_to_binary(Module), Config) of
×
1459
                             {error, conflict} ->
1460
                                 false;
×
1461
                             ok ->
1462
                                 true
×
1463
                         end
1464
                 end,
1465
                 Modules).
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc