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

processone / ejabberd / 1258

12 Dec 2025 03:57PM UTC coverage: 33.638% (-0.006%) from 33.644%
1258

push

github

badlop
Container: Apply commit a22c88a

ejabberdctl.template: Show meaningful error when ERL_DIST_PORT is in use

15554 of 46240 relevant lines covered (33.64%)

1078.28 hits per line

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

40.64
/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-2025   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
-include_lib("stdlib/include/zip.hrl").
53

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

56
-record(state, {}).
57

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

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

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

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

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

83
handle_info({'ETS-TRANSFER', Table, Process, Module}, State) ->
84
    ?DEBUG("ejabberd now controls ETS table ~p from process ~p for module ~p",
×
85
              [Table, Process, Module]),
×
86
    {noreply, State};
×
87

88
handle_info(Info, State) ->
89
    ?WARNING_MSG("Unexpected info: ~p", [Info]),
×
90
    {noreply, State}.
×
91

92
terminate(_Reason, _State) ->
93
    ejabberd_hooks:delete(webadmin_menu_node, ?MODULE, web_menu_node, 50),
11✔
94
    ejabberd_hooks:delete(webadmin_page_node, ?MODULE, web_page_node, 50),
11✔
95
    ejabberd_commands:unregister_commands(get_commands_spec()).
11✔
96

97
code_change(_OldVsn, State, _Extra) ->
98
    {ok, State}.
×
99

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

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

199
available() ->
200
    Jungle = modules_spec(sources_dir(), "*/*"),
2✔
201
    Standalone = modules_spec(sources_dir(), "*"),
2✔
202
    lists:keysort(1,
2✔
203
        lists:foldl(fun({Key, Val}, Acc) ->
204
                lists:keystore(Key, 1, Acc, {Key, Val})
×
205
            end, Jungle, Standalone)).
206
available(Module) when is_atom(Module) ->
207
    available(misc:atom_to_binary(Module));
×
208
available(Package) when is_binary(Package) ->
209
    Available = [misc:atom_to_binary(K) || K<-proplists:get_keys(available())],
×
210
    lists:member(Package, Available).
×
211

212
available_command() ->
213
    [short_spec(Item) || Item <- available()].
×
214

215
installed() ->
216
    modules_spec(modules_dir(), "*").
112✔
217
installed(Module) when is_atom(Module) ->
218
    installed(misc:atom_to_binary(Module));
×
219
installed(Package) when is_binary(Package) ->
220
    Installed = [misc:atom_to_binary(K) || K<-proplists:get_keys(installed())],
4✔
221
    lists:member(Package, Installed).
4✔
222

223
installed_command() ->
224
    [short_spec(Item) || Item <- installed()].
5✔
225

226
install(Module) when is_atom(Module) ->
227
    install(misc:atom_to_binary(Module), undefined);
×
228
install(Package) when is_binary(Package) ->
229
    install(Package, undefined).
2✔
230

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

259
ejabberd_config_reload(Config) when is_list(Config) ->
260
    %% Don't reload config when ejabberd is starting
261
    %% because it will be reloaded after installing
262
    %% all the external modules from install_contrib_modules
263
    ok;
×
264
ejabberd_config_reload(undefined) ->
265
    ejabberd_config:reload().
1✔
266

267
maybe_print_module_status(Module) ->
268
    case get_module_status_el(Module) of
1✔
269
        [_, {xmlcdata, String}] ->
270
            io:format("~ts~n", [String]);
×
271
        _ ->
272
            ok
1✔
273
    end.
274

275
uninstall(Module) when is_atom(Module) ->
276
    uninstall(misc:atom_to_binary(Module));
×
277
uninstall(Package) when is_binary(Package) ->
278
    case installed(Package) of
2✔
279
        true ->
280
            Module = misc:binary_to_atom(Package),
1✔
281
            ModuleRuntime = get_runtime_module_name(Module),
1✔
282
            case erlang:function_exported(ModuleRuntime, pre_uninstall, 0) of
1✔
283
                true -> ModuleRuntime:pre_uninstall();
×
284
                _ -> ok
1✔
285
            end,
286
            [catch gen_mod:stop_module(Host, ModuleRuntime)
1✔
287
             || Host <- ejabberd_option:hosts()],
1✔
288
            code:purge(ModuleRuntime),
1✔
289
            code:delete(ModuleRuntime),
1✔
290
            [code:del_path(PathDelete) || PathDelete <- [module_ebin_dir(Module)|module_deps_dirs(Module)]],
1✔
291
            delete_path(module_lib_dir(Module)),
1✔
292
            ejabberd_config:reload();
1✔
293
        false ->
294
            {error, not_installed}
1✔
295
    end.
296

297
upgrade() ->
298
    [{Package, upgrade(Package)} || {Package, _Spec} <- installed()].
×
299
upgrade(Module) when is_atom(Module) ->
300
    upgrade(misc:atom_to_binary(Module));
×
301
upgrade(Package) when is_binary(Package) ->
302
    uninstall(Package),
×
303
    clean(Package),
×
304
    install(Package).
×
305

306
clean(Package) ->
307
    Spec = [S || {Mod, S} <- available(), misc:atom_to_binary(Mod)==Package],
×
308
    case Spec of
×
309
        [] ->
310
            {error, not_available};
×
311
        [Attrs] ->
312
            Path = proplists:get_value(path, Attrs),
×
313
            [delete_path(SubPath) || SubPath <- filelib:wildcard(Path++"/{deps,ebin}")]
×
314
    end.
315

316
add_sources(Path) when is_list(Path) ->
317
    add_sources(iolist_to_binary(module_name(Path)), Path).
1✔
318
add_sources(_, "") ->
319
    {error, no_url};
×
320
add_sources(Module, Path) when is_atom(Module), is_list(Path) ->
321
    add_sources(misc:atom_to_binary(Module), Path);
×
322
add_sources(Package, Path) when is_binary(Package), is_list(Path) ->
323
    DestDir = sources_dir(),
1✔
324
    RepDir = filename:join(DestDir, module_name(Path)),
1✔
325
    delete_path(RepDir, binary_to_list(Package)),
1✔
326
    case filelib:ensure_dir(RepDir) of
1✔
327
        ok ->
328
            case {string:left(Path, 4), string:right(Path, 2)} of
1✔
329
                {"http", "ip"} -> extract(zip, geturl(Path), DestDir);
×
330
                {"http", "gz"} -> extract(tar, geturl(Path), DestDir);
×
331
                {"http", _} -> extract_url(Path, DestDir);
×
332
                {"git@", _} -> extract_github_master(Path, DestDir);
1✔
333
                {_, "ip"} -> extract(zip, Path, DestDir);
×
334
                {_, "gz"} -> extract(tar, Path, DestDir);
×
335
                _ -> {error, unsupported_source}
×
336
            end;
337
        Error ->
338
            Error
×
339
    end.
340

341
del_sources(Module) when is_atom(Module) ->
342
    del_sources(misc:atom_to_binary(Module));
×
343
del_sources(Package) when is_binary(Package) ->
344
    case uninstall(Package) of
×
345
        ok ->
346
            SrcDir = module_src_dir(misc:binary_to_atom(Package)),
×
347
            delete_path(SrcDir);
×
348
        Error ->
349
            Error
×
350
    end.
351

352
check(Module) when is_atom(Module) ->
353
    check(misc:atom_to_binary(Module));
×
354
check(Package) when is_binary(Package) ->
355
    case {available(Package), installed(Package)} of
×
356
        {false, _} ->
357
            {error, not_available};
×
358
        {_, false} ->
359
            Status = install(Package),
×
360
            uninstall(Package),
×
361
            case Status of
×
362
                ok -> check_sources(misc:binary_to_atom(Package));
×
363
                Error -> Error
×
364
            end;
365
        _ ->
366
            check_sources(misc:binary_to_atom(Package))
×
367
    end.
368

369
%% -- archives and variables functions
370

371
geturl(Url) ->
372
    case getenv("PROXY_SERVER", "", ":") of
1✔
373
        [H, Port] ->
374
            httpc:set_options([{proxy, {{H, list_to_integer(Port)}, []}}], ext_mod);
×
375
        [H] ->
376
            httpc:set_options([{proxy, {{H, 8080}, []}}], ext_mod);
×
377
        _ ->
378
            ok
1✔
379
    end,
380
    User = case getenv("PROXY_USER", "", ":") of
1✔
381
        [U, Pass] -> [{proxy_auth, {U, Pass}}];
×
382
        _ -> []
1✔
383
    end,
384
    UA = {"User-Agent", "ejabberd/ext_mod"},
1✔
385
    case httpc:request(get, {Url, [UA]}, User, [{body_format, binary}], ext_mod) of
1✔
386
        {ok, {{_, 200, _}, Headers, Response}} ->
387
            {ok, Headers, Response};
1✔
388
        {ok, {{_, 403, Reason}, _Headers, _Response}} ->
389
            {error, Reason};
×
390
        {ok, {{_, Code, _}, _Headers, Response}} ->
391
            {error, {Code, Response}};
×
392
        {error, Reason} ->
393
            {error, Reason}
×
394
    end.
395

396
getenv(Env, Default) ->
397
    case os:getenv(Env) of
179✔
398
        false -> Default;
179✔
399
        "" -> Default;
×
400
        Value -> Value
×
401
    end.
402
getenv(Env, Default, Separator) ->
403
    string:tokens(getenv(Env, Default), Separator).
2✔
404

405
extract(zip, {ok, _, Body}, DestDir) ->
406
    extract(zip, iolist_to_binary(Body), DestDir);
1✔
407
extract(tar, {ok, _, Body}, DestDir) ->
408
    extract(tar, {binary, iolist_to_binary(Body)}, DestDir);
×
409
extract(_, {error, Reason}, _) ->
410
    {error, Reason};
×
411
extract(zip, Zip, DestDir) ->
412
    {ok, DirList} = zip:list_dir(Zip),
1✔
413
    Offending =
1✔
414
        lists:filter(fun (#zip_comment{}) ->
415
                             false;
1✔
416
                         (#zip_file{name = Filename}) ->
417
                             absolute == filename:pathtype(Filename)
742✔
418
                     end,
419
                     DirList),
420
    case Offending of
1✔
421
        [] ->
422
            extract(zip_verified, Zip, DestDir);
1✔
423
        _ ->
424
            Filenames = [F#zip_file.name || F <- Offending],
×
425
            ?ERROR_MSG("The zip file includes absolute file paths:~n  ~p", [Filenames]),
×
426
            {error, {zip_absolute_path, Filenames}}
×
427
    end;
428
extract(zip_verified, Zip, DestDir) ->
429
    case zip:extract(Zip, [{cwd, DestDir}]) of
1✔
430
        {ok, _} -> ok;
1✔
431
        Error -> Error
×
432
    end;
433
extract(tar, Tar, DestDir) ->
434
    erl_tar:extract(Tar, [compressed, {cwd, DestDir}]).
×
435

436
extract_url(Path, DestDir) ->
437
    hd([extract_github_master(Path, DestDir) || string:str(Path, "github") > 0]
×
438
     ++[{error, unsupported_source}]).
439

440
extract_github_master(Repos, DestDir) ->
441
    Base = case string:tokens(Repos, ":") of
1✔
442
        ["git@github.com", T1] -> "https://github.com/"++T1;
1✔
443
        _ -> Repos
×
444
    end,
445
    Url = case lists:reverse(Base) of
1✔
446
        [$t,$i,$g,$.|T2] -> lists:reverse(T2);
1✔
447
        _ -> Base
×
448
    end,
449
    case extract(zip, geturl(Url++"/archive/master.zip"), DestDir) of
1✔
450
        ok ->
451
            RepDir = filename:join(DestDir, module_name(Repos)),
1✔
452
            RepDirSpec = filename:join(DestDir, module_spec_name(RepDir)),
1✔
453
            file:rename(RepDir++"-master", RepDirSpec),
1✔
454
            maybe_write_commit_json(Url, RepDirSpec);
1✔
455
        Error ->
456
            Error
×
457
    end.
458

459
copy(From, To) ->
460
    case filelib:is_dir(From) of
3✔
461
        true ->
462
            Copy = fun(F) ->
×
463
                    SubFrom = filename:join(From, F),
×
464
                    SubTo = filename:join(To, F),
×
465
                    copy(SubFrom, SubTo)
×
466
            end,
467
            lists:foldl(fun(ok, ok) -> ok;
×
468
                           (ok, Error) -> Error;
×
469
                           (Error, _) -> Error
×
470
                end, ok,
471
                [Copy(filename:basename(X)) || X<-filelib:wildcard(From++"/*")]);
×
472
        false ->
473
            filelib:ensure_dir(To),
3✔
474
            case file:copy(From, To) of
3✔
475
                {ok, _} -> ok;
3✔
476
                Error -> Error
×
477
            end
478
    end.
479

480
delete_path(Path) ->
481
    case filelib:is_dir(Path) of
7✔
482
        true ->
483
            [delete_path(SubPath) || SubPath <- filelib:wildcard(Path++"/*")],
3✔
484
            file:del_dir(Path);
3✔
485
        false ->
486
            file:delete(Path)
4✔
487
    end.
488

489
delete_path(Path, Package) ->
490
    delete_path(filename:join(filename:dirname(Path), Package)).
1✔
491

492
modules_dir() ->
493
    DefaultDir = filename:join(misc:get_home(), ".ejabberd-modules"),
164✔
494
    getenv("CONTRIB_MODULES_PATH", DefaultDir).
164✔
495

496
sources_dir() ->
497
    filename:join(modules_dir(), "sources").
12✔
498

499
config_dir() ->
500
    DefaultDir = filename:join(modules_dir(), "conf"),
13✔
501
    getenv("CONTRIB_MODULES_CONF_DIR", DefaultDir).
13✔
502

503
-spec modules_configs() -> [binary()].
504
modules_configs() ->
505
    Fs = [{filename:rootname(filename:basename(F)), F}
13✔
506
          || F <- filelib:wildcard(config_dir() ++ "/*.{yml,yaml}")
507
                 ++ filelib:wildcard(modules_dir() ++ "/*/conf/*.{yml,yaml}")],
13✔
508
    [unicode:characters_to_binary(proplists:get_value(F, Fs))
13✔
509
     || F <- proplists:get_keys(Fs)].
13✔
510

511
module_lib_dir(Package) ->
512
    filename:join(modules_dir(), Package).
14✔
513

514
module_ebin_dir(Package) ->
515
    filename:join(module_lib_dir(Package), "ebin").
9✔
516

517
module_src_dir(Package) ->
518
    Rep = module_name(Package),
5✔
519
    SrcDir = sources_dir(),
5✔
520
    Standalone = filelib:wildcard(Rep, SrcDir),
5✔
521
    Jungle = filelib:wildcard("*/"++Rep, SrcDir),
5✔
522
    case Standalone++Jungle of
5✔
523
        [RepDir|_] -> filename:join(SrcDir, RepDir);
5✔
524
        _ -> filename:join(SrcDir, Rep)
×
525
    end.
526

527
module_name(Id) ->
528
    filename:basename(filename:rootname(Id)).
56✔
529

530
module_spec_name(Path) ->
531
    case filelib:wildcard(filename:join(Path++"-master", "*.spec")) of
1✔
532
        "" ->
533
            module_name(Path);
1✔
534
        ModuleName ->
535
            filename:basename(ModuleName, ".spec")
×
536
    end.
537

538
module(Id) ->
539
    misc:binary_to_atom(iolist_to_binary(module_name(Id))).
47✔
540

541
module_spec(Spec) ->
542
    [{path, filename:dirname(Spec)}
47✔
543
      | case consult(Spec) of
544
            {ok, Meta} -> Meta;
47✔
545
            _ -> []
×
546
        end].
547

548
modules_spec(Dir, Path) ->
549
    Wildcard = filename:join(Path, "*.spec"),
118✔
550
    lists:sort(
118✔
551
        [{module(Match), module_spec(filename:join(Dir, Match))}
47✔
552
         || Match <- filelib:wildcard(Wildcard, Dir)]).
118✔
553

554
short_spec({Module, Attrs}) when is_atom(Module), is_list(Attrs) ->
555
    {Module, proplists:get_value(summary, Attrs, "")}.
3✔
556

557
is_contrib_allowed(Config) when is_list(Config) ->
558
    case lists:keyfind(allow_contrib_modules, 1, Config) of
×
559
        false -> true;
×
560
        {_, false} -> false;
×
561
        {_, true} -> true
×
562
    end;
563
is_contrib_allowed(undefined) ->
564
    ejabberd_option:allow_contrib_modules().
2✔
565

566
%% -- build functions
567

568
check_sources(Module) ->
569
    SrcDir = module_src_dir(Module),
×
570
    SpecFile = filename:flatten([Module, ".spec"]),
×
571
    {ok, Dir} = file:get_cwd(),
×
572
    file:set_cwd(SrcDir),
×
573
    HaveSrc = case filelib:is_dir("src") or filelib:is_dir("lib") of
×
574
        true -> [];
×
575
        false -> [{missing, "src (Erlang) or lib (Elixir) sources directory"}]
×
576
    end,
577
    DirCheck = lists:foldl(
×
578
            fun({Type, Name}, Acc) ->
579
                case filelib:Type(Name) of
×
580
                    true -> Acc;
×
581
                    false -> [{missing, Name}|Acc]
×
582
                end
583
            end, HaveSrc, [{is_file, "README.md"},
584
                           {is_file, "COPYING"},
585
                           {is_file, SpecFile}]),
586
    SpecCheck = case consult(SpecFile) of
×
587
        {ok, Spec} ->
588
            lists:foldl(
×
589
                fun(Key, Acc) ->
590
                    case lists:keysearch(Key, 1, Spec) of
×
591
                        false -> [{missing_meta, Key}|Acc];
×
592
                        {value, {Key, [_NoEmpty|_]}} -> Acc;
×
593
                        {value, {Key, Val}} -> [{invalid_meta, {Key, Val}}|Acc]
×
594
                    end
595
                end, [], [author, summary, home, url]);
596
        {error, Error} ->
597
            [{invalid_spec, Error}]
×
598
    end,
599
    file:set_cwd(Dir),
×
600
    Result = DirCheck ++ SpecCheck,
×
601
    case Result of
×
602
        [] -> ok;
×
603
        _ -> {error, Result}
×
604
    end.
605

606
compile_and_install(Module, Spec, Config) ->
607
    SrcDir = module_src_dir(Module),
1✔
608
    LibDir = module_lib_dir(Module),
1✔
609
    case filelib:is_dir(SrcDir) of
1✔
610
        true ->
611
            case compile_deps(SrcDir) of
1✔
612
                ok ->
613
                    case compile(SrcDir, filename:join(SrcDir, "deps")) of
1✔
614
                        ok -> install(Module, Spec, SrcDir, LibDir, Config);
1✔
615
                        Error -> Error
×
616
                    end;
617
                Error ->
618
                    Error
×
619
            end;
620
        false ->
621
            Path = proplists:get_value(url, Spec, ""),
×
622
            case add_sources(Module, Path) of
×
623
                ok -> compile_and_install(Module, Spec, Config);
×
624
                Error -> Error
×
625
            end
626
    end.
627

628
compile_deps(LibDir) ->
629
    DepsDir = filename:join(LibDir, "deps"),
1✔
630
    case filelib:is_dir(DepsDir) of
1✔
631
        true -> ok;  % assume deps are included
×
632
        false -> fetch_rebar_deps(LibDir)
1✔
633
    end,
634
    Rs = [compile(Dep, DepsDir) || Dep <- filelib:wildcard(filename:join(DepsDir, "*"))],
1✔
635
    compile_result(Rs).
1✔
636

637
compile(LibDir, DepsDir) ->
638
    Bin = filename:join(LibDir, "ebin"),
1✔
639
    Lib = filename:join(LibDir, "lib"),
1✔
640
    Src = filename:join(LibDir, "src"),
1✔
641
    Includes = [ {i, Inc} || Inc <- filelib:wildcard(DepsDir++"/**/include") ],
1✔
642
    Options = [ {outdir, Bin},
1✔
643
                {i, LibDir++"/.."},
644
                {i, filename:join(LibDir, "include")}
645
              | Includes ++ compile_options()],
646
    ?DEBUG("compile options: ~p", [Options]),
1✔
647
    filelib:ensure_dir(filename:join(Bin, ".")),
1✔
648
    [copy(App, filename:join(Bin, filename:basename(App, ".src"))) || App <- filelib:wildcard(Src++"/*.app*")],
1✔
649
    compile_c_files(LibDir),
1✔
650
    ErlFiles = filelib:wildcard(Src++"/**/*.erl"),
1✔
651
    ?DEBUG("erl files to compile: ~p", [ErlFiles]),
1✔
652
    Er = [compile_erlang_file(Bin, File, Options)
1✔
653
          || File <- ErlFiles],
1✔
654
    Ex = compile_elixir_files(Bin, filelib:wildcard(Lib ++ "/**/*.ex")),
1✔
655
    compile_result(lists:flatten([Er, Ex])).
1✔
656

657
compile_c_files(LibDir) ->
658
    case file:read_file_info(filename:join(LibDir, "c_src/Makefile")) of
1✔
659
        {ok, _} ->
660
            os:cmd("cd "++LibDir++"; make -C c_src");
×
661
        {error, _} ->
662
            ok
1✔
663
    end.
664

665
compile_result(Results) ->
666
    case lists:dropwhile(
2✔
667
            fun({ok, _}) -> true;
1✔
668
               (_) -> false
1✔
669
            end, Results) of
670
        [] -> ok;
1✔
671
        [Error|_] -> Error
1✔
672
    end.
673

674
compile_options() ->
675
    [verbose, report_errors, report_warnings, debug_info, ?ALL_DEFS,
676
     {feature, maybe_expr, enable}]
677
    ++ [{i, filename:join(app_dir(App), "include")}
1✔
678
        || App <- [fast_xml, xmpp, p1_utils, ejabberd]]
1✔
679
    ++ [{i, filename:join(app_dir(App), "include")}
2✔
680
        || App <- [p1_xml, p1_xmpp]] % paths used in Debian packages
1✔
681
    ++ [{i, filename:join(mod_dir(Mod), "include")}
×
682
        || Mod <- installed()].
1✔
683

684
app_dir(App) ->
685
    case code:lib_dir(App) of
6✔
686
        {error, bad_name} ->
687
            case code:which(App) of
2✔
688
                Beam when is_list(Beam) ->
689
                    filename:dirname(filename:dirname(Beam));
×
690
                _ ->
691
                    "."
2✔
692
            end;
693
        Dir ->
694
            Dir
4✔
695
    end.
696

697
mod_dir({Package, Spec}) ->
698
    Default = filename:join(modules_dir(), Package),
×
699
    proplists:get_value(path, Spec, Default).
×
700

701
compile_erlang_file(Dest, File) ->
702
    compile_erlang_file(Dest, File, compile_options()).
×
703

704
compile_erlang_file(Dest, File, ErlOptions) ->
705
    Options = [{outdir, Dest} | ErlOptions],
1✔
706
    case compile:file(File, Options) of
1✔
707
        {ok, Module} -> {ok, Module};
1✔
708
        {ok, Module, _} -> {ok, Module};
×
709
        {ok, Module, _, _} -> {ok, Module};
×
710
        error -> {error, {compilation_failed, File}};
×
711
        {error, E, W} -> {error, {compilation_failed, File, E, W}}
×
712
    end.
713

714
-ifdef(ELIXIR_ENABLED).
715
compile_elixir_files(_, []) ->
716
    ok;
717
compile_elixir_files(Dest, [File | _] = Files) when is_list(Dest) and is_list(File) ->
718
  BinFiles = [list_to_binary(F) || F <- Files],
719
  compile_elixir_files(list_to_binary(Dest), BinFiles);
720

721
compile_elixir_files(Dest, Files) ->
722
  try 'Elixir.Kernel.ParallelCompiler':compile_to_path(Files, Dest, [{return_diagnostics, true}]) of
723
      {ok, Modules, []} when is_list(Modules) ->
724
          {ok, Modules};
725
      {ok, Modules, Warnings} when is_list(Modules) ->
726
          ?WARNING_MSG("Warnings compiling module: ~n~p", [Warnings]),
727
          {ok, Modules}
728
  catch
729
    A:B ->
730
          ?ERROR_MSG("Problem ~p compiling Elixir files: ~p~nFiles: ~p", [A, B, Files]),
731
          {error, {compilation_failed, Files}}
732
  end.
733
-else.
734
compile_elixir_files(_, []) ->
735
    ok;
1✔
736
compile_elixir_files(_, Files) ->
737
    ErrorString = "Attempted to compile Elixir files, but Elixir support is "
×
738
        "not available in ejabberd. Try compiling ejabberd using "
739
        "'./configure --enable-elixir' or './configure --with-rebar=mix'",
740
    ?ERROR_MSG(ErrorString, []),
×
741
    io:format("Error: " ++ ErrorString ++ "~n", []),
×
742
    {error, {elixir_not_available, Files}}.
×
743
-endif.
744

745
install(Module, Spec, SrcDir, LibDir, Config) ->
746
    {ok, CurDir} = file:get_cwd(),
1✔
747
    file:set_cwd(SrcDir),
1✔
748
    Files1 = [{File, copy(File, filename:join(LibDir, File))}
1✔
749
                  || File <- filelib:wildcard("{ebin,priv,conf,include}/**")],
1✔
750
    Files2 = [{File, copy(File, filename:join(LibDir, filename:join(lists:nthtail(2,filename:split(File)))))}
1✔
751
                  || File <- filelib:wildcard("deps/*/ebin/**")],
1✔
752
    Files3 = [{File, copy(File, filename:join(LibDir, File))}
1✔
753
                  || File <- filelib:wildcard("deps/*/priv/**")],
1✔
754
    Errors = lists:dropwhile(fun({_, ok}) -> true;
1✔
755
                                (_) -> false
×
756
            end, Files1++Files2++Files3),
757
    inform_module_configuration(Module, LibDir, Files1, Config),
1✔
758
    Result = case Errors of
1✔
759
        [{F, {error, E}}|_] ->
760
            {error, {F, E}};
×
761
        [] ->
762
            SpecPath = proplists:get_value(path, Spec),
1✔
763
            SpecFile = filename:flatten([Module, ".spec"]),
1✔
764
            copy(filename:join(SpecPath, SpecFile), filename:join(LibDir, SpecFile))
1✔
765
    end,
766
    file:set_cwd(CurDir),
1✔
767
    Result.
1✔
768

769
inform_module_configuration(Module, LibDir, Files1, Config) ->
770
    Res = lists:filter(fun({[$c, $o, $n, $f |_], ok}) -> true;
1✔
771
                          (_) -> false
1✔
772
            end, Files1),
773
    AlreadyConfigured = lists:keymember(Module, 1, get_modules(Config)),
1✔
774
    case {Res, AlreadyConfigured} of
1✔
775
        {[{ConfigPath, ok}], false} ->
776
            FullConfigPath = filename:join(LibDir, ConfigPath),
1✔
777
            io:format("Module ~p has been installed and started.~n"
1✔
778
                      "It's configured in the file:~n  ~s~n"
779
                      "Configure the module in that file, or remove it~n"
780
                      "and configure in your main ejabberd.yml~n",
781
                      [Module, FullConfigPath]);
782
        {[{ConfigPath, ok}], true} ->
783
            FullConfigPath = filename:join(LibDir, ConfigPath),
×
784
            file:rename(FullConfigPath, FullConfigPath++".example"),
×
785
            io:format("Module ~p has been installed and started.~n"
×
786
                      "The ~p configuration in your ejabberd.yml is used.~n",
787
                      [Module, Module]);
788
        {[], _} ->
789
            io:format("Module ~p has been installed.~n"
×
790
                      "Now you can configure it in your ejabberd.yml~n",
791
                      [Module])
792
    end.
793

794
get_modules(Config) when is_list(Config) ->
795
    {modules, Modules} = lists:keyfind(modules, 1, Config),
×
796
    Modules;
×
797
get_modules(undefined) ->
798
    ejabberd_config:get_option(modules).
1✔
799

800
%% -- minimalist rebar spec parser, only support git
801

802
fetch_rebar_deps(SrcDir) ->
803
    case rebar_deps(filename:join(SrcDir, "rebar.config"))
1✔
804
      ++ rebar_deps(filename:join(SrcDir, "rebar.config.script")) of
805
        [] ->
806
            ok;
1✔
807
        Deps ->
808
            {ok, CurDir} = file:get_cwd(),
×
809
            file:set_cwd(SrcDir),
×
810
            filelib:ensure_dir(filename:join("deps", ".")),
×
811
            lists:foreach(fun({App, Cmd}) ->
×
812
                        io:format("Fetching dependency ~s: ", [App]),
×
813
                        Result = os:cmd("cd deps; "++Cmd++"; cd .."),
×
814
                        io:format("~s", [Result])
×
815
                end, Deps),
816
            file:set_cwd(CurDir)
×
817
    end.
818

819
rebar_deps(Script) ->
820
    case file:script(Script) of
6✔
821
        {ok, Config} when is_list(Config) ->
822
            [rebar_dep(Dep) || Dep <- proplists:get_value(deps, Config, [])];
×
823
        {ok, {deps, Deps}} ->
824
            [rebar_dep(Dep) || Dep <- Deps];
3✔
825
        _ ->
826
            []
3✔
827
    end.
828

829
rebar_dep({App, Version, Git}) when Version /= ".*" ->
830
    AppS = atom_to_list(App),
×
831
    Help = os:cmd("mix hex.package"),
×
832
    case string:find(Help, "mix hex.package fetch") /= nomatch of
×
833
        true ->
834
            {App, "mix hex.package fetch "++AppS++" "++Version++" --unpack --output "++AppS};
×
835
        false ->
836
            io:format("I'll download ~p using git because I can't use Mix "
×
837
                      "to fetch from hex.pm:~n~s", [AppS, Help]),
838
            rebar_dep({App, ".*", Git})
×
839
    end;
840

841
rebar_dep({App, _, {git, Url}}) ->
842
    {App, "git clone "++Url++" "++filename:basename(App)};
×
843
rebar_dep({App, _, {git, Url, {branch, Ref}}}) ->
844
    {App, "git clone -n "++Url++" "++filename:basename(App)++
×
845
     "; (cd "++filename:basename(App)++
846
     "; git checkout -q origin/"++Ref++")"};
847
rebar_dep({App, _, {git, Url, {tag, Ref}}}) ->
848
    {App, "git clone -n "++Url++" "++filename:basename(App)++
×
849
     "; (cd "++filename:basename(App)++
850
     "; git checkout -q "++Ref++")"};
851
rebar_dep({App, _, {git, Url, Ref}}) ->
852
    {App, "git clone -n "++Url++" "++filename:basename(App)++
×
853
     "; (cd "++filename:basename(App)++
854
     "; git checkout -q "++Ref++")"}.
855

856
module_deps_dirs(Module) ->
857
    SrcDir = module_src_dir(Module),
2✔
858
    LibDir = module_lib_dir(Module),
2✔
859
    DepsDir = filename:join(LibDir, "deps"),
2✔
860
    Deps = rebar_deps(filename:join(SrcDir, "rebar.config"))
2✔
861
      ++ rebar_deps(filename:join(SrcDir, "rebar.config.script")),
862
    [filename:join(DepsDir, App) || {App, _Cmd} <- Deps].
2✔
863

864
%% -- YAML spec parser
865

866
consult(File) ->
867
    case fast_yaml:decode_from_file(File, [plain_as_atom]) of
47✔
868
        {ok, []} -> {ok, []};
×
869
        {ok, [Doc|_]} -> {ok, [format(Spec) || Spec <- Doc]};
47✔
870
        {error, Err} -> {error, fast_yaml:format_error(Err)}
×
871
    end.
872

873
format({Key, Val}) when is_binary(Val) ->
874
    {Key, binary_to_list(Val)};
235✔
875
format({Key, Val}) -> % TODO: improve Yaml parsing
876
    {Key, Val}.
×
877

878
%% -- COMMIT.json
879

880
maybe_write_commit_json(Url, RepDir) ->
881
    case (os:getenv("GITHUB_ACTIONS") == "true") of
1✔
882
        true ->
883
            ok;
1✔
884
        false ->
885
            write_commit_json(Url, RepDir)
×
886
    end.
887

888
write_commit_json(Url, RepDir) ->
889
    Url2 = string:replace(Url, "https://github.com", "https://api.github.com/repos"),
×
890
    BranchUrl = lists:flatten(Url2 ++ "/branches/master"),
×
891
    case geturl(BranchUrl) of
×
892
        {ok, _Headers, Body} ->
893
            {ok, F} = file:open(filename:join(RepDir, "COMMIT.json"), [raw, write]),
×
894
            file:write(F, Body),
×
895
            file:close(F);
×
896
        {error, Reason} ->
897
            Reason
×
898
    end.
899

900
find_commit_json(Attrs) ->
901
    FromPath = get_module_path(Attrs),
1✔
902
    case {filelib:find_file("COMMIT.json", FromPath),
1✔
903
          filelib:find_file("COMMIT.json", filename:join(FromPath, ".."))}
904
    of
905
        {{ok, FromFile}, _} ->
906
            FromFile;
×
907
        {_, {ok, FromFile}} ->
908
            FromFile;
×
909
        _ ->
910
            not_found
1✔
911
    end.
912

913
copy_commit_json(Package, Attrs) ->
914
    DestPath = module_lib_dir(Package),
1✔
915
    case find_commit_json(Attrs) of
1✔
916
        not_found ->
917
            ok;
1✔
918
        FromFile ->
919
            file:copy(FromFile, filename:join(DestPath, "COMMIT.json"))
×
920
    end.
921

922
get_commit_details(Dirname) ->
923
    RepDir = filename:join(sources_dir(), Dirname),
×
924
    get_commit_details2(filename:join(RepDir, "COMMIT.json")).
×
925

926
get_commit_details2(Path) ->
927
    case file:read_file(Path) of
×
928
        {ok, Body} ->
929
            parse_details(Body);
×
930
        _ ->
931
            #{sha => unknown_sha,
×
932
              date => <<>>,
933
              message => <<>>,
934
              html => <<>>,
935
              author_name => <<>>,
936
              commit_html_url => <<>>}
937
    end.
938

939
parse_details(Body) ->
940
    Contents = misc:json_decode(Body),
×
941

942
    {ok, Commit} = maps:find(<<"commit">>, Contents),
×
943
    {ok, Sha} = maps:find(<<"sha">>, Commit),
×
944
    {ok, CommitHtmlUrl} = maps:find(<<"html_url">>, Commit),
×
945

946
    {ok, Commit2} = maps:find(<<"commit">>, Commit),
×
947
    {ok, Message} = maps:find(<<"message">>, Commit2),
×
948
    {ok, Author} = maps:find(<<"author">>, Commit2),
×
949
    {ok, AuthorName} = maps:find(<<"name">>, Author),
×
950
    {ok, Committer} = maps:find(<<"committer">>, Commit2),
×
951
    {ok, Date} = maps:find(<<"date">>, Committer),
×
952

953
    {ok, Links} = maps:find(<<"_links">>, Contents),
×
954
    {ok, Html} = maps:find(<<"html">>, Links),
×
955

956
    #{sha => Sha,
×
957
      date => Date,
958
      message => Message,
959
      html => Html,
960
      author_name => AuthorName,
961
      commit_html_url => CommitHtmlUrl}.
962

963
%% -- Web Admin
964

965
-define(AXC(URL, Attributes, Text),
966
        ?XAE(<<"a">>, [{<<"href">>, URL} | Attributes], [?C(Text)])
967
       ).
968

969
-define(INPUTCHECKED(Type, Name, Value),
970
        ?XA(<<"input">>,
971
            [{<<"type">>, Type},
972
             {<<"name">>, Name},
973
             {<<"disabled">>, <<"true">>},
974
             {<<"checked">>, <<"true">>},
975
             {<<"value">>, Value}
976
            ]
977
           )
978
       ).
979

980
%% @format-begin
981

982
web_menu_node(Acc, _Node, _Lang) ->
983
    Acc
984
    ++ [{<<"contrib">>, <<"Contrib Modules (Detailed)">>},
×
985
        {<<"contrib-api">>, <<"Contrib Modules (API)">>}].
986

987
web_page_node(_,
988
              Node,
989
              #request{path = [<<"contrib">>],
990
                       q = Query,
991
                       lang = Lang} =
992
                  R) ->
993
    Title =
×
994
        ?H1GL(<<"Contrib Modules (Detailed)">>,
995
              <<"../../developer/extending-ejabberd/modules/#ejabberd-contrib">>,
996
              <<"ejabberd-contrib">>),
997
    Res = [ejabberd_cluster:call(Node,
×
998
                                 ejabberd_web_admin,
999
                                 make_command,
1000
                                 [webadmin_node_contrib,
1001
                                  R,
1002
                                  [{<<"node">>, Node}, {<<"query">>, Query}, {<<"lang">>, Lang}],
1003
                                  []])],
1004
    {stop, Title ++ Res};
×
1005
web_page_node(_, Node, #request{path = [<<"contrib-api">> | RPath]} = R) ->
1006
    Title =
×
1007
        ?H1GL(<<"Contrib Modules (API)">>,
1008
              <<"../../developer/extending-ejabberd/modules/#ejabberd-contrib">>,
1009
              <<"ejabberd-contrib">>),
1010
    _TableInstalled = make_table_installed(Node, R, RPath),
×
1011
    _TableAvailable = make_table_available(Node, R, RPath),
×
1012
    TableInstalled = make_table_installed(Node, R, RPath),
×
1013
    TableAvailable = make_table_available(Node, R, RPath),
×
1014
    Res = [?X(<<"hr">>),
×
1015
           ?XAC(<<"h2">>, [{<<"id">>, <<"specs">>}], <<"Specs">>),
1016
           ?XE(<<"blockquote">>,
1017
               [ejabberd_cluster:call(Node,
1018
                                      ejabberd_web_admin,
1019
                                      make_command,
1020
                                      [modules_update_specs, R])]),
1021
           ?X(<<"hr">>),
1022
           ?XAC(<<"h2">>, [{<<"id">>, <<"installed">>}], <<"Installed">>),
1023
           ?XE(<<"blockquote">>,
1024
               [ejabberd_cluster:call(Node,
1025
                                      ejabberd_web_admin,
1026
                                      make_command,
1027
                                      [modules_installed, R, [], [{only, presentation}]]),
1028
                ejabberd_cluster:call(Node,
1029
                                      ejabberd_web_admin,
1030
                                      make_command,
1031
                                      [module_uninstall, R, [], [{only, presentation}]]),
1032
                ejabberd_cluster:call(Node,
1033
                                      ejabberd_web_admin,
1034
                                      make_command,
1035
                                      [module_upgrade, R, [], [{only, presentation}]]),
1036
                TableInstalled]),
1037
           ?X(<<"hr">>),
1038
           ?XAC(<<"h2">>, [{<<"id">>, <<"available">>}], <<"Available">>),
1039
           ?XE(<<"blockquote">>,
1040
               [ejabberd_cluster:call(Node,
1041
                                      ejabberd_web_admin,
1042
                                      make_command,
1043
                                      [modules_available, R, [], [{only, presentation}]]),
1044
                ejabberd_cluster:call(Node,
1045
                                      ejabberd_web_admin,
1046
                                      make_command,
1047
                                      [module_install, R, [], [{only, presentation}]]),
1048
                TableAvailable,
1049
                ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [module_check, R])])],
1050
    {stop, Title ++ Res};
×
1051
web_page_node(Acc, _, _) ->
1052
    Acc.
×
1053

1054
make_table_installed(Node, R, RPath) ->
1055
    Columns = [<<"Name">>, <<"Summary">>, <<"">>, <<"">>],
×
1056
    ModulesInstalled =
×
1057
        ejabberd_cluster:call(Node,
1058
                              ejabberd_web_admin,
1059
                              make_command_raw_value,
1060
                              [modules_installed, R, []]),
1061
    Rows =
×
1062
        lists:map(fun({Name, Summary}) ->
1063
                     NameBin = misc:atom_to_binary(Name),
×
1064
                     Upgrade =
×
1065
                         ejabberd_cluster:call(Node,
1066
                                               ejabberd_web_admin,
1067
                                               make_command,
1068
                                               [module_upgrade,
1069
                                                R,
1070
                                                [{<<"module">>, NameBin}],
1071
                                                [{only, button}, {input_name_append, [NameBin]}]]),
1072
                     Uninstall =
×
1073
                         ejabberd_cluster:call(Node,
1074
                                               ejabberd_web_admin,
1075
                                               make_command,
1076
                                               [module_uninstall,
1077
                                                R,
1078
                                                [{<<"module">>, NameBin}],
1079
                                                [{only, button},
1080
                                                 {style, danger},
1081
                                                 {input_name_append, [NameBin]}]]),
1082
                     {?C(NameBin), ?C(list_to_binary(Summary)), Upgrade, Uninstall}
×
1083
                  end,
1084
                  ModulesInstalled),
1085
    ejabberd_web_admin:make_table(200, RPath, Columns, Rows).
×
1086

1087
make_table_available(Node, R, RPath) ->
1088
    Columns = [<<"Name">>, <<"Summary">>, <<"">>],
×
1089
    ModulesAll =
×
1090
        ejabberd_cluster:call(Node,
1091
                              ejabberd_web_admin,
1092
                              make_command_raw_value,
1093
                              [modules_available, R, []]),
1094
    ModulesInstalled =
×
1095
        ejabberd_cluster:call(Node,
1096
                              ejabberd_web_admin,
1097
                              make_command_raw_value,
1098
                              [modules_installed, R, []]),
1099
    ModulesNotInstalled =
×
1100
        lists:filter(fun({Mod, _}) -> not lists:keymember(Mod, 1, ModulesInstalled) end,
×
1101
                     ModulesAll),
1102
    Rows =
×
1103
        lists:map(fun({Name, Summary}) ->
1104
                     NameBin = misc:atom_to_binary(Name),
×
1105
                     Install =
×
1106
                         ejabberd_cluster:call(Node,
1107
                                               ejabberd_web_admin,
1108
                                               make_command,
1109
                                               [module_install,
1110
                                                R,
1111
                                                [{<<"module">>, NameBin}],
1112
                                                [{only, button}, {input_name_append, [NameBin]}]]),
1113
                     {?C(NameBin), ?C(list_to_binary(Summary)), Install}
×
1114
                  end,
1115
                  ModulesNotInstalled),
1116
    ejabberd_web_admin:make_table(200, RPath, Columns, Rows).
×
1117

1118
webadmin_node_contrib(Node, Query, Lang) ->
1119
    QueryRes = list_modules_parse_query(Query),
×
1120
    Contents = get_content(Node, Query, Lang),
×
1121
    Result =
×
1122
        case QueryRes of
1123
            ok ->
1124
                [?XREST(?T("Submitted"))];
×
1125
            nothing ->
1126
                []
×
1127
        end,
1128
    Result ++ Contents.
×
1129
%% @format-end
1130

1131
get_module_home(Module, Attrs) ->
1132
    case get_module_information(home, Attrs) of
×
1133
        "https://github.com/processone/ejabberd-contrib/tree/master/" = P1 ->
1134
            P1 ++ atom_to_list(Module);
×
1135
        Other ->
1136
            Other
×
1137
    end.
1138

1139
get_module_summary(Attrs) ->
1140
    get_module_information(summary, Attrs).
×
1141

1142
get_module_author(Attrs) ->
1143
    get_module_information(author, Attrs).
×
1144

1145
get_module_path(Attrs) ->
1146
    get_module_information(path, Attrs).
1✔
1147

1148
get_module_information(Attribute, Attrs) ->
1149
    case lists:keyfind(Attribute, 1, Attrs) of
1✔
1150
        false -> "";
×
1151
        {_, Value} -> Value
1✔
1152
    end.
1153

1154
get_installed_module_el({ModAtom, Attrs}, Lang) ->
1155
    Mod = misc:atom_to_binary(ModAtom),
×
1156
    Home = list_to_binary(get_module_home(ModAtom, Attrs)),
×
1157
    Summary = list_to_binary(get_module_summary(Attrs)),
×
1158
    Author = list_to_binary(get_module_author(Attrs)),
×
1159
    FromPath = get_module_path(Attrs),
×
1160
    FromFile = case filelib:find_file("COMMIT.json", FromPath) of
×
1161
                   {ok, FF} -> FF;
×
1162
                   {error, _} -> "dummypath"
×
1163
               end,
1164
    #{sha := CommitSha,
×
1165
      date := CommitDate,
1166
      message := CommitMessage,
1167
      author_name := CommitAuthorName,
1168
      commit_html_url := CommitHtmlUrl} = get_commit_details2(FromFile),
1169

1170
    [SourceSpec] = [S || {M, S} <- available(), M == ModAtom],
×
1171
    SourceFile = find_commit_json(SourceSpec),
×
1172
    #{sha := SourceSha,
×
1173
      date := SourceDate,
1174
      message := SourceMessage,
1175
      author_name := SourceAuthorName,
1176
      commit_html_url := SourceHtmlUrl} = get_commit_details2(SourceFile),
1177

1178
    UpgradeEls =
×
1179
        case CommitSha == SourceSha of
1180
            true ->
1181
                [];
×
1182
            false ->
1183
                SourceTitleEl = make_title_el(SourceDate, SourceMessage, SourceAuthorName),
×
1184
                [?XE(<<"td">>,
×
1185
                     [?INPUT(<<"checkbox">>, <<"selected_upgrade">>, Mod),
1186
                      ?C(<<" ">>),
1187
                      ?AXC(SourceHtmlUrl, [SourceTitleEl], binary:part(SourceSha, {0, 8}))
1188
                     ]
1189
                    )
1190
                ]
1191
        end,
1192

1193
    Started =
×
1194
        case gen_mod:is_loaded(hd(ejabberd_option:hosts()), ModAtom) of
1195
            false ->
1196
                [?C(<<" ">>)];
×
1197
            true ->
1198
                []
×
1199
        end,
1200
    TitleEl = make_title_el(CommitDate, CommitMessage, CommitAuthorName),
×
1201
    Status = get_module_status_el(ModAtom),
×
1202
    HomeTitleEl = make_home_title_el(Summary, Author),
×
1203
    ?XE(<<"tr">>,
×
1204
        [?XE(<<"td">>, [?AXC(Home, [HomeTitleEl], Mod)]),
1205
         ?XE(<<"td">>,
1206
             [?INPUTTD(<<"checkbox">>, <<"selected_uninstall">>, Mod),
1207
              ?C(<<" ">>),
1208
              get_commit_link(CommitHtmlUrl, TitleEl, CommitSha),
1209
              ?C(<<" - ">>)]
1210
             ++ Started
1211
             ++ Status)
1212
        | UpgradeEls]).
1213

1214
get_module_status_el(ModAtom) ->
1215
    case {get_module_status(ModAtom),
1✔
1216
          get_module_status(elixir_module_name(ModAtom))} of
1217
        {Str, unknown} when is_list(Str) ->
1218
            [?C(<<" ">>), ?C(Str)];
×
1219
        {unknown, Str} when is_list(Str) ->
1220
            [?C(<<" ">>), ?C(Str)];
×
1221
        {unknown, unknown} ->
1222
            []
1✔
1223
    end.
1224

1225
get_module_status(Module) ->
1226
    try Module:mod_status() of
2✔
1227
        Str when is_list(Str) ->
1228
            Str
×
1229
    catch
1230
        _:_ ->
1231
            unknown
2✔
1232
    end.
1233

1234
%% When a module named mod_whatever in ejabberd-modules
1235
%% is written in Elixir, its runtime name is 'Elixir.ModWhatever'
1236
get_runtime_module_name(Module) ->
1237
    case is_elixir_module(Module) of
2✔
1238
        true -> elixir_module_name(Module);
×
1239
        false -> Module
2✔
1240
    end.
1241

1242
is_elixir_module(Module) ->
1243
    LibDir = module_src_dir(Module),
2✔
1244
    Lib = filename:join(LibDir, "lib"),
2✔
1245
    Src = filename:join(LibDir, "src"),
2✔
1246
    case {filelib:wildcard(Lib++"/*.{ex}"),
2✔
1247
          filelib:wildcard(Src++"/*.{erl}")} of
1248
        {[_ | _], []} ->
1249
            true;
×
1250
        {[], _} ->
1251
            false
2✔
1252
    end.
1253

1254
%% Converts mod_some_thing to Elixir.ModSomeThing
1255
elixir_module_name(ModAtom) ->
1256
    list_to_atom("Elixir." ++ elixir_module_name("_" ++ atom_to_list(ModAtom), [])).
1✔
1257

1258
elixir_module_name([], Res) ->
1259
    lists:reverse(Res);
1✔
1260
elixir_module_name([$_, Char | Remaining], Res) ->
1261
    [Upper] = string:uppercase([Char]),
2✔
1262
    elixir_module_name(Remaining, [Upper | Res]);
2✔
1263
elixir_module_name([Char | Remaining], Res) ->
1264
    elixir_module_name(Remaining, [Char | Res]).
8✔
1265

1266
get_available_module_el({ModAtom, Attrs}) ->
1267
    Installed = installed(),
×
1268
    Mod = misc:atom_to_binary(ModAtom),
×
1269
    Home = list_to_binary(get_module_home(ModAtom, Attrs)),
×
1270
    Summary = list_to_binary(get_module_summary(Attrs)),
×
1271
    Author = list_to_binary(get_module_author(Attrs)),
×
1272
    HomeTitleEl = make_home_title_el(Summary, Author),
×
1273
    InstallCheckbox =
×
1274
        case lists:keymember(ModAtom, 1, Installed) of
1275
            false -> [?INPUT(<<"checkbox">>, <<"selected_install">>, Mod)];
×
1276
            true -> [?INPUTCHECKED(<<"checkbox">>, <<"selected_install">>, Mod)]
×
1277
        end,
1278
    ?XE(<<"tr">>,
×
1279
        [?XE(<<"td">>, InstallCheckbox ++ [?C(<<" ">>), ?AXC(Home, [HomeTitleEl], Mod)]),
1280
         ?XE(<<"td">>, [?C(Summary)])]).
1281

1282
get_installed_modules_table(Lang) ->
1283
    Modules = installed(),
×
1284
    Tail = [?XE(<<"tr">>,
×
1285
                [?XE(<<"td">>, []),
1286
                 ?XE(<<"td">>,
1287
                     [?INPUTTD(<<"submit">>, <<"uninstall">>, ?T("Uninstall"))]
1288
                    ),
1289
                 ?XE(<<"td">>,
1290
                     [?INPUTT(<<"submit">>, <<"upgrade">>, ?T("Upgrade"))]
1291
                    )
1292
                ]
1293
               )
1294
           ],
1295
    TBody = [get_installed_module_el(Module, Lang) || Module <- lists:sort(Modules)],
×
1296
    ?XAE(<<"table">>,
×
1297
         [],
1298
         [?XE(<<"tbody">>, TBody ++ Tail)]
1299
        ).
1300

1301
get_available_modules_table(Lang) ->
1302
    Modules = get_available_notinstalled(),
×
1303
    Tail = [?XE(<<"tr">>,
×
1304
                [?XE(<<"td">>,
1305
                     [?INPUTT(<<"submit">>, <<"install">>, ?T("Install"))]
1306
                    )
1307
                ]
1308
               )
1309
           ],
1310
    TBody = [get_available_module_el(Module) || Module <- lists:sort(Modules)],
×
1311
    ?XAE(<<"table">>,
×
1312
         [],
1313
         [?XE(<<"tbody">>, TBody ++ Tail)]
1314
        ).
1315

1316
make_title_el(Date, Message, AuthorName) ->
1317
    LinkTitle = <<Message/binary, "\n", AuthorName/binary, "\n", Date/binary>>,
×
1318
    {<<"title">>, LinkTitle}.
×
1319

1320
make_home_title_el(Summary, Author) ->
1321
    LinkTitle = <<Summary/binary, "\n", Author/binary>>,
×
1322
    {<<"title">>, LinkTitle}.
×
1323

1324
get_commit_link(_CommitHtmlUrl, _TitleErl, unknown_sha) ->
1325
    ?C(<<"Please Update Specs">>);
×
1326
get_commit_link(CommitHtmlUrl, TitleEl, CommitSha) ->
1327
    ?AXC(CommitHtmlUrl, [TitleEl], binary:part(CommitSha, {0, 8})).
×
1328

1329
get_content(Node, Query, Lang) ->
1330
    {{_CommandCtl}, _Res} =
×
1331
        case catch parse_and_execute(Query, Node) of
1332
            {'EXIT', _} -> {{""}, <<"">>};
×
1333
            Result_tuple -> Result_tuple
×
1334
        end,
1335

1336
    AvailableModulesEls = get_available_modules_table(Lang),
×
1337
    InstalledModulesEls = get_installed_modules_table(Lang),
×
1338

1339
    Sources = get_sources_list(),
×
1340
    SourceEls = (?XAE(<<"table">>,
×
1341
                      [],
1342
                      [?XE(<<"tbody">>,
1343
                           (lists:map(
1344
                              fun(Dirname) ->
1345
                                      #{sha := CommitSha,
×
1346
                                        date := CommitDate,
1347
                                        message := CommitMessage,
1348
                                        html := Html,
1349
                                        author_name := AuthorName,
1350
                                        commit_html_url := CommitHtmlUrl
1351
                                       } = get_commit_details(Dirname),
1352
                                      TitleEl = make_title_el(CommitDate, CommitMessage, AuthorName),
×
1353
                                      ?XE(<<"tr">>,
×
1354
                                          [?XE(<<"td">>, [?AC(Html, Dirname)]),
1355
                                           ?XE(<<"td">>,
1356
                                               [get_commit_link(CommitHtmlUrl, TitleEl, CommitSha)]
1357
                                              ),
1358
                                           ?XE(<<"td">>, [?C(CommitMessage)])
1359
                                          ])
1360
                              end,
1361
                              lists:sort(Sources)
1362
                             ))
1363
                          )
1364
                      ]
1365
                     )),
1366

1367
    [?XC(<<"p">>,
×
1368
         translate:translate(
1369
           Lang, ?T("Update specs to get modules source, then install desired ones.")
1370
          )
1371
        ),
1372
     ?XAE(<<"form">>,
1373
          [{<<"method">>, <<"post">>}],
1374
          [?XCT(<<"h3">>, ?T("Sources Specs:")),
1375
           SourceEls,
1376
           ?BR,
1377
           ?INPUTT(<<"submit">>,
1378
                   <<"updatespecs">>,
1379
                   translate:translate(Lang, ?T("Update Specs"))),
1380

1381
           ?XCT(<<"h3">>, ?T("Installed Modules:")),
1382
           InstalledModulesEls,
1383
           ?BR,
1384

1385
           ?XCT(<<"h3">>, ?T("Other Modules Available:")),
1386
           AvailableModulesEls
1387
          ]
1388
         )
1389
    ].
1390

1391
get_sources_list() ->
1392
    case file:list_dir(sources_dir()) of
×
1393
        {ok, Filenames} -> Filenames;
×
1394
        {error, enoent} -> []
×
1395
    end.
1396

1397
get_available_notinstalled() ->
1398
    Installed = installed(),
×
1399
    lists:filter(
×
1400
      fun({Mod, _}) ->
1401
              not lists:keymember(Mod, 1, Installed)
×
1402
      end,
1403
      available()
1404
     ).
1405

1406
parse_and_execute(Query, Node) ->
1407
    {[Exec], _} = lists:partition(
×
1408
                    fun(ExType) ->
1409
                            lists:keymember(ExType, 1, Query)
×
1410
                    end,
1411
                    [<<"updatespecs">>]
1412
                   ),
1413
    Commands = {get_val(<<"updatespecs">>, Query)},
×
1414
    {_, R} = parse1_command(Exec, Commands, Node),
×
1415
    {Commands, R}.
×
1416

1417
get_val(Val, Query) ->
1418
    {value, {_, R}} = lists:keysearch(Val, 1, Query),
×
1419
    binary_to_list(R).
×
1420

1421
parse1_command(<<"updatespecs">>, {_}, _Node) ->
1422
    Res = update(),
×
1423
    {oook, io_lib:format("~p", [Res])}.
×
1424

1425
list_modules_parse_query(Query) ->
1426
    case {lists:keysearch(<<"install">>, 1, Query),
×
1427
          lists:keysearch(<<"upgrade">>, 1, Query),
1428
          lists:keysearch(<<"uninstall">>, 1, Query)}
1429
    of
1430
        {{value, _}, _, _} -> list_modules_parse_install(Query);
×
1431
        {_, {value, _}, _} -> list_modules_parse_upgrade(Query);
×
1432
        {_, _, {value, _}} -> list_modules_parse_uninstall(Query);
×
1433
        _ -> nothing
×
1434
    end.
1435

1436
list_modules_parse_install(Query) ->
1437
    lists:foreach(
×
1438
      fun({Mod, _}) ->
1439
              ModBin = misc:atom_to_binary(Mod),
×
1440
              case lists:member({<<"selected_install">>, ModBin}, Query) of
×
1441
                  true -> install(Mod);
×
1442
                  _ -> ok
×
1443
              end
1444
      end,
1445
      get_available_notinstalled()),
1446
    ok.
×
1447

1448
list_modules_parse_upgrade(Query) ->
1449
    lists:foreach(
×
1450
      fun({Mod, _}) ->
1451
              ModBin = misc:atom_to_binary(Mod),
×
1452
              case lists:member({<<"selected_upgrade">>, ModBin}, Query) of
×
1453
                  true -> upgrade(Mod);
×
1454
                  _ -> ok
×
1455
              end
1456
      end,
1457
      installed()),
1458
    ok.
×
1459

1460
list_modules_parse_uninstall(Query) ->
1461
    lists:foreach(
×
1462
      fun({Mod, _}) ->
1463
              ModBin = misc:atom_to_binary(Mod),
×
1464
              case lists:member({<<"selected_uninstall">>, ModBin}, Query) of
×
1465
                  true -> uninstall(Mod);
×
1466
                  _ -> ok
×
1467
              end
1468
      end,
1469
      installed()),
1470
    ok.
×
1471

1472
install_contrib_modules(Modules, Config) ->
1473
    lists:filter(fun(Module) ->
×
1474
                         case install(misc:atom_to_binary(Module), Config) of
×
1475
                             {error, conflict} ->
1476
                                 false;
×
1477
                             ok ->
1478
                                 true
×
1479
                         end
1480
                 end,
1481
                 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

© 2025 Coveralls, Inc