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

processone / ejabberd / 747

27 Jun 2024 01:43PM UTC coverage: 32.123% (+0.8%) from 31.276%
747

push

github

badlop
Set version to 24.06

14119 of 43953 relevant lines covered (32.12%)

614.73 hits per line

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

5.46
/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_file/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()),
×
165
    Jungles = lists:foldl(fun({Package, Spec}, Acc) ->
×
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) ->
×
171
                Update = add_sources(Repo),
×
172
                ?INFO_MSG("Update packages from repo ~ts: ~p", [Repo, Update]),
×
173
                case Update of
×
174
                    ok -> Acc;
×
175
                    Error -> [{repository, Repo, Error}|Acc]
×
176
                end
177
        end, [], Jungles),
178
    Res = lists:foldl(fun({Package, Spec}, Acc) ->
×
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
×
188
        [] -> ok;
×
189
        [Error|_] -> Error
×
190
    end.
191

192
available() ->
193
    Jungle = modules_spec(sources_dir(), "*/*"),
×
194
    Standalone = modules_spec(sources_dir(), "*"),
×
195
    lists:keysort(1,
×
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(), "*").
8✔
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())],
×
214
    lists:member(Package, Installed).
×
215

216
installed_command() ->
217
    [short_spec(Item) || Item <- installed()].
×
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).
×
223

224
install(Package, Config) when is_binary(Package) ->
225
    Spec = [S || {Mod, S} <- available(), misc:atom_to_binary(Mod)==Package],
×
226
    case {Spec, installed(Package), is_contrib_allowed(Config)} of
×
227
        {_, _, false} ->
228
            {error, not_allowed};
×
229
        {[], _, _} ->
230
            {error, not_available};
×
231
        {_, true, _} ->
232
            {error, conflict};
×
233
        {[Attrs], _, _} ->
234
            Module = misc:binary_to_atom(Package),
×
235
            case compile_and_install(Module, Attrs, Config) of
×
236
                ok ->
237
                    code:add_pathsz([module_ebin_dir(Module)|module_deps_dirs(Module)]),
×
238
                    ejabberd_config_reload(Config),
×
239
                    copy_commit_json(Package, Attrs),
×
240
                    ModuleRuntime = get_runtime_module_name(Module),
×
241
                    case erlang:function_exported(ModuleRuntime, post_install, 0) of
×
242
                        true -> ModuleRuntime:post_install();
×
243
                        _ -> ok
×
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().
×
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
×
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}
×
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).
×
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(),
×
297
    RepDir = filename:join(DestDir, module_name(Path)),
×
298
    delete_path(RepDir, binary_to_list(Package)),
×
299
    case filelib:ensure_dir(RepDir) of
×
300
        ok ->
301
            case {string:left(Path, 4), string:right(Path, 2)} of
×
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);
×
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
×
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
×
352
    end,
353
    User = case getenv("PROXY_USER", "", ":") of
×
354
        [U, Pass] -> [{proxy_auth, {U, Pass}}];
×
355
        _ -> []
×
356
    end,
357
    UA = {"User-Agent", "ejabberd/ext_mod"},
×
358
    case httpc:request(get, {Url, [UA]}, User, [{body_format, binary}], ext_mod) of
×
359
        {ok, {{_, 200, _}, Headers, Response}} ->
360
            {ok, Headers, Response};
×
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, "").
10✔
371
getenv(Env, Default) ->
372
    case os:getenv(Env) of
21✔
373
        false -> Default;
11✔
374
        "" -> Default;
×
375
        Value -> Value
10✔
376
    end.
377
getenv(Env, Default, Separator) ->
378
    string:tokens(getenv(Env, Default), Separator).
×
379

380
extract(zip, {ok, _, Body}, DestDir) ->
381
    extract(zip, iolist_to_binary(Body), DestDir);
×
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
×
388
        {ok, _} -> ok;
×
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
×
400
        ["git@github.com", T1] -> "https://github.com/"++T1;
×
401
        _ -> Repos
×
402
    end,
403
    Url = case lists:reverse(Base) of
×
404
        [$t,$i,$g,$.|T2] -> lists:reverse(T2);
×
405
        _ -> Base
×
406
    end,
407
    case extract(zip, geturl(Url++"/archive/master.zip"), DestDir) of
×
408
        ok ->
409
            RepDir = filename:join(DestDir, module_name(Repos)),
×
410
            RepDirSpec = filename:join(DestDir, module_spec_name(RepDir)),
×
411
            file:rename(RepDir++"-master", RepDirSpec),
×
412
            maybe_write_commit_json(Url, RepDirSpec);
×
413
        Error ->
414
            Error
×
415
    end.
416

417
copy(From, To) ->
418
    case filelib:is_dir(From) of
×
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),
×
432
            case file:copy(From, To) of
×
433
                {ok, _} -> ok;
×
434
                Error -> Error
×
435
            end
436
    end.
437

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

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

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

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

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

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

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

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

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

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

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

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

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

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

512
short_spec({Module, Attrs}) when is_atom(Module), is_list(Attrs) ->
513
    {Module, proplists:get_value(summary, Attrs, "")}.
×
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().
×
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),
×
566
    LibDir = module_lib_dir(Module),
×
567
    case filelib:is_dir(SrcDir) of
×
568
        true ->
569
            case compile_deps(SrcDir) of
×
570
                ok ->
571
                    case compile(SrcDir) of
×
572
                        ok -> install(Module, Spec, SrcDir, LibDir, Config);
×
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"),
×
588
    case filelib:is_dir(Deps) of
×
589
        true -> ok;  % assume deps are included
×
590
        false -> fetch_rebar_deps(LibDir)
×
591
    end,
592
    Rs = [compile(Dep) || Dep <- filelib:wildcard(filename:join(Deps, "*"))],
×
593
    compile_result(Rs).
×
594

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

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

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

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

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

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

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

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

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

671
-ifdef(ELIXIR_ENABLED).
672
compile_elixir_file(Dest, File) when is_list(Dest) and is_list(File) ->
673
  compile_elixir_file(list_to_binary(Dest), list_to_binary(File));
674

675
compile_elixir_file(Dest, File) ->
676
  try 'Elixir.Kernel.ParallelCompiler':files_to_path([File], Dest, []) of
677
    Modules when is_list(Modules) -> {ok, Modules}
678
  catch
679
    _ -> {error, {compilation_failed, File}}
680
  end.
681
-else.
682
compile_elixir_file(_, File) ->
683
    {error, {compilation_failed, File}}.
×
684
-endif.
685

686
install(Module, Spec, SrcDir, LibDir, Config) ->
687
    {ok, CurDir} = file:get_cwd(),
×
688
    file:set_cwd(SrcDir),
×
689
    Files1 = [{File, copy(File, filename:join(LibDir, File))}
×
690
                  || File <- filelib:wildcard("{ebin,priv,conf,include}/**")],
×
691
    Files2 = [{File, copy(File, filename:join(LibDir, filename:join(lists:nthtail(2,filename:split(File)))))}
×
692
                  || File <- filelib:wildcard("deps/*/ebin/**")],
×
693
    Files3 = [{File, copy(File, filename:join(LibDir, File))}
×
694
                  || File <- filelib:wildcard("deps/*/priv/**")],
×
695
    Errors = lists:dropwhile(fun({_, ok}) -> true;
×
696
                                (_) -> false
×
697
            end, Files1++Files2++Files3),
698
    inform_module_configuration(Module, LibDir, Files1, Config),
×
699
    Result = case Errors of
×
700
        [{F, {error, E}}|_] ->
701
            {error, {F, E}};
×
702
        [] ->
703
            SpecPath = proplists:get_value(path, Spec),
×
704
            SpecFile = filename:flatten([Module, ".spec"]),
×
705
            copy(filename:join(SpecPath, SpecFile), filename:join(LibDir, SpecFile))
×
706
    end,
707
    file:set_cwd(CurDir),
×
708
    Result.
×
709

710
inform_module_configuration(Module, LibDir, Files1, Config) ->
711
    Res = lists:filter(fun({[$c, $o, $n, $f |_], ok}) -> true;
×
712
                          (_) -> false
×
713
            end, Files1),
714
    AlreadyConfigured = lists:keymember(Module, 1, get_modules(Config)),
×
715
    case {Res, AlreadyConfigured} of
×
716
        {[{ConfigPath, ok}], false} ->
717
            FullConfigPath = filename:join(LibDir, ConfigPath),
×
718
            io:format("Module ~p has been installed and started.~n"
×
719
                      "It's configured in the file:~n  ~s~n"
720
                      "Configure the module in that file, or remove it~n"
721
                      "and configure in your main ejabberd.yml~n",
722
                      [Module, FullConfigPath]);
723
        {[{ConfigPath, ok}], true} ->
724
            FullConfigPath = filename:join(LibDir, ConfigPath),
×
725
            file:rename(FullConfigPath, FullConfigPath++".example"),
×
726
            io:format("Module ~p has been installed and started.~n"
×
727
                      "The ~p configuration in your ejabberd.yml is used.~n",
728
                      [Module, Module]);
729
        {[], _} ->
730
            io:format("Module ~p has been installed.~n"
×
731
                      "Now you can configure it in your ejabberd.yml~n",
732
                      [Module])
733
    end.
734

735
get_modules(Config) when is_list(Config) ->
736
    {modules, Modules} = lists:keyfind(modules, 1, Config),
×
737
    Modules;
×
738
get_modules(undefined) ->
739
    ejabberd_config:get_option(modules).
×
740

741
%% -- minimalist rebar spec parser, only support git
742

743
fetch_rebar_deps(SrcDir) ->
744
    case rebar_deps(filename:join(SrcDir, "rebar.config"))
×
745
      ++ rebar_deps(filename:join(SrcDir, "rebar.config.script")) of
746
        [] ->
747
            ok;
×
748
        Deps ->
749
            {ok, CurDir} = file:get_cwd(),
×
750
            file:set_cwd(SrcDir),
×
751
            filelib:ensure_dir(filename:join("deps", ".")),
×
752
            lists:foreach(fun({_App, Cmd}) ->
×
753
                        os:cmd("cd deps; "++Cmd++"; cd ..")
×
754
                end, Deps),
755
            file:set_cwd(CurDir)
×
756
    end.
757

758
rebar_deps(Script) ->
759
    case file:script(Script) of
×
760
        {ok, Config} when is_list(Config) ->
761
            [rebar_dep(Dep) || Dep <- proplists:get_value(deps, Config, [])];
×
762
        {ok, {deps, Deps}} ->
763
            [rebar_dep(Dep) || Dep <- Deps];
×
764
        _ ->
765
            []
×
766
    end.
767
rebar_dep({App, _, {git, Url}}) ->
768
    {App, "git clone "++Url++" "++filename:basename(App)};
×
769
rebar_dep({App, _, {git, Url, {branch, Ref}}}) ->
770
    {App, "git clone -n "++Url++" "++filename:basename(App)++
×
771
     "; (cd "++filename:basename(App)++
772
     "; git checkout -q origin/"++Ref++")"};
773
rebar_dep({App, _, {git, Url, {tag, Ref}}}) ->
774
    {App, "git clone -n "++Url++" "++filename:basename(App)++
×
775
     "; (cd "++filename:basename(App)++
776
     "; git checkout -q "++Ref++")"};
777
rebar_dep({App, _, {git, Url, Ref}}) ->
778
    {App, "git clone -n "++Url++" "++filename:basename(App)++
×
779
     "; (cd "++filename:basename(App)++
780
     "; git checkout -q "++Ref++")"}.
781

782
module_deps_dirs(Module) ->
783
    SrcDir = module_src_dir(Module),
×
784
    LibDir = module_lib_dir(Module),
×
785
    DepsDir = filename:join(LibDir, "deps"),
×
786
    Deps = rebar_deps(filename:join(SrcDir, "rebar.config"))
×
787
      ++ rebar_deps(filename:join(SrcDir, "rebar.config.script")),
788
    [filename:join(DepsDir, App) || {App, _Cmd} <- Deps].
×
789

790
%% -- YAML spec parser
791

792
consult(File) ->
793
    case fast_yaml:decode_from_file(File, [plain_as_atom]) of
×
794
        {ok, []} -> {ok, []};
×
795
        {ok, [Doc|_]} -> {ok, [format(Spec) || Spec <- Doc]};
×
796
        {error, Err} -> {error, fast_yaml:format_error(Err)}
×
797
    end.
798

799
format({Key, Val}) when is_binary(Val) ->
800
    {Key, binary_to_list(Val)};
×
801
format({Key, Val}) -> % TODO: improve Yaml parsing
802
    {Key, Val}.
×
803

804
%% -- COMMIT.json
805

806
maybe_write_commit_json(Url, RepDir) ->
807
    case (os:getenv("GITHUB_ACTIONS") == "true") of
×
808
        true ->
809
            ok;
×
810
        false ->
811
            write_commit_json(Url, RepDir)
×
812
    end.
813

814
write_commit_json(Url, RepDir) ->
815
    Url2 = string_replace(Url, "https://github.com", "https://api.github.com/repos"),
×
816
    BranchUrl = lists:flatten(Url2 ++ "/branches/master"),
×
817
    case geturl(BranchUrl) of
×
818
        {ok, _Headers, Body} ->
819
            {ok, F} = file:open(filename:join(RepDir, "COMMIT.json"), [raw, write]),
×
820
            file:write(F, Body),
×
821
            file:close(F);
×
822
        {error, Reason} ->
823
            Reason
×
824
    end.
825

826
find_commit_json(Attrs) ->
827
    FromPath = get_module_path(Attrs),
×
828
    case {find_commit_json_path(FromPath),
×
829
          find_commit_json_path(filename:join(FromPath, ".."))}
830
    of
831
        {{ok, FromFile}, _} ->
832
            FromFile;
×
833
        {_, {ok, FromFile}} ->
834
            FromFile;
×
835
        _ ->
836
            not_found
×
837
    end.
838

839
-ifdef(HAVE_URI_STRING). %% Erlang/OTP 20 or higher can use this:
840
string_replace(Subject, Pattern, Replacement) ->
841
    string:replace(Subject, Pattern, Replacement).
×
842

843
find_commit_json_path(Path) ->
844
    filelib:find_file("COMMIT.json", Path).
×
845
-else. % Workaround for Erlang/OTP older than 20:
846
string_replace(Subject, Pattern, Replacement) ->
847
    B = binary:replace(list_to_binary(Subject),
848
                       list_to_binary(Pattern),
849
                       list_to_binary(Replacement)),
850
    binary_to_list(B).
851

852
find_commit_json_path(Path) ->
853
    case filelib:wildcard("COMMIT.json", Path) of
854
        [] ->
855
            {error, commit_json_not_found};
856
        ["COMMIT.json"] = File ->
857
            {ok, filename:join(Path, File)}
858
    end.
859
-endif.
860

861
copy_commit_json(Package, Attrs) ->
862
    DestPath = module_lib_dir(Package),
×
863
    case find_commit_json(Attrs) of
×
864
        not_found ->
865
            ok;
×
866
        FromFile ->
867
            file:copy(FromFile, filename:join(DestPath, "COMMIT.json"))
×
868
    end.
869

870
get_commit_details(Dirname) ->
871
    RepDir = filename:join(sources_dir(), Dirname),
×
872
    get_commit_details2(filename:join(RepDir, "COMMIT.json")).
×
873

874
get_commit_details2(Path) ->
875
    case file:read_file(Path) of
×
876
        {ok, Body} ->
877
            parse_details(Body);
×
878
        _ ->
879
            #{sha => unknown_sha,
×
880
              date => <<>>,
881
              message => <<>>,
882
              html => <<>>,
883
              author_name => <<>>,
884
              commit_html_url => <<>>}
885
    end.
886

887
parse_details(Body) ->
888
    Contents = misc:json_decode(Body),
×
889

890
    {ok, Commit} = maps:find(<<"commit">>, Contents),
×
891
    {ok, Sha} = maps:find(<<"sha">>, Commit),
×
892
    {ok, CommitHtmlUrl} = maps:find(<<"html_url">>, Commit),
×
893

894
    {ok, Commit2} = maps:find(<<"commit">>, Commit),
×
895
    {ok, Message} = maps:find(<<"message">>, Commit2),
×
896
    {ok, Author} = maps:find(<<"author">>, Commit2),
×
897
    {ok, AuthorName} = maps:find(<<"name">>, Author),
×
898
    {ok, Committer} = maps:find(<<"committer">>, Commit2),
×
899
    {ok, Date} = maps:find(<<"date">>, Committer),
×
900

901
    {ok, Links} = maps:find(<<"_links">>, Contents),
×
902
    {ok, Html} = maps:find(<<"html">>, Links),
×
903

904
    #{sha => Sha,
×
905
      date => Date,
906
      message => Message,
907
      html => Html,
908
      author_name => AuthorName,
909
      commit_html_url => CommitHtmlUrl}.
910

911
%% -- Web Admin
912

913
-define(AXC(URL, Attributes, Text),
914
        ?XAE(<<"a">>, [{<<"href">>, URL} | Attributes], [?C(Text)])
915
       ).
916

917
-define(INPUTCHECKED(Type, Name, Value),
918
        ?XA(<<"input">>,
919
            [{<<"type">>, Type},
920
             {<<"name">>, Name},
921
             {<<"disabled">>, <<"true">>},
922
             {<<"checked">>, <<"true">>},
923
             {<<"value">>, Value}
924
            ]
925
           )
926
       ).
927

928
%% @format-begin
929

930
web_menu_node(Acc, _Node, _Lang) ->
931
    Acc
932
    ++ [{<<"contrib">>, <<"Contrib Modules (Detailed)">>},
×
933
        {<<"contrib-api">>, <<"Contrib Modules (API)">>}].
934

935
web_page_node(_,
936
              Node,
937
              #request{path = [<<"contrib">>],
938
                       q = Query,
939
                       lang = Lang} =
940
                  R) ->
941
    Title =
×
942
        ?H1GL(<<"Contrib Modules (Detailed)">>,
943
              <<"../../developer/extending-ejabberd/modules/#ejabberd-contrib">>,
944
              <<"ejabberd-contrib">>),
945
    Res = [ejabberd_cluster:call(Node,
×
946
                                 ejabberd_web_admin,
947
                                 make_command,
948
                                 [webadmin_node_contrib,
949
                                  R,
950
                                  [{<<"node">>, Node}, {<<"query">>, Query}, {<<"lang">>, Lang}],
951
                                  []])],
952
    {stop, Title ++ Res};
×
953
web_page_node(_, Node, #request{path = [<<"contrib-api">> | RPath]} = R) ->
954
    Title =
×
955
        ?H1GL(<<"Contrib Modules (API)">>,
956
              <<"../../developer/extending-ejabberd/modules/#ejabberd-contrib">>,
957
              <<"ejabberd-contrib">>),
958
    _TableInstalled = make_table_installed(Node, R, RPath),
×
959
    _TableAvailable = make_table_available(Node, R, RPath),
×
960
    TableInstalled = make_table_installed(Node, R, RPath),
×
961
    TableAvailable = make_table_available(Node, R, RPath),
×
962
    Res = [?X(<<"hr">>),
×
963
           ?XAC(<<"h2">>, [{<<"id">>, <<"specs">>}], <<"Specs">>),
964
           ?XE(<<"blockquote">>,
965
               [ejabberd_cluster:call(Node,
966
                                      ejabberd_web_admin,
967
                                      make_command,
968
                                      [modules_update_specs, R])]),
969
           ?X(<<"hr">>),
970
           ?XAC(<<"h2">>, [{<<"id">>, <<"installed">>}], <<"Installed">>),
971
           ?XE(<<"blockquote">>,
972
               [ejabberd_cluster:call(Node,
973
                                      ejabberd_web_admin,
974
                                      make_command,
975
                                      [modules_installed, R, [], [{only, presentation}]]),
976
                ejabberd_cluster:call(Node,
977
                                      ejabberd_web_admin,
978
                                      make_command,
979
                                      [module_uninstall, R, [], [{only, presentation}]]),
980
                ejabberd_cluster:call(Node,
981
                                      ejabberd_web_admin,
982
                                      make_command,
983
                                      [module_upgrade, R, [], [{only, presentation}]]),
984
                TableInstalled]),
985
           ?X(<<"hr">>),
986
           ?XAC(<<"h2">>, [{<<"id">>, <<"available">>}], <<"Available">>),
987
           ?XE(<<"blockquote">>,
988
               [ejabberd_cluster:call(Node,
989
                                      ejabberd_web_admin,
990
                                      make_command,
991
                                      [modules_available, R, [], [{only, presentation}]]),
992
                ejabberd_cluster:call(Node,
993
                                      ejabberd_web_admin,
994
                                      make_command,
995
                                      [module_install, R, [], [{only, presentation}]]),
996
                TableAvailable,
997
                ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [module_check, R])])],
998
    {stop, Title ++ Res};
×
999
web_page_node(Acc, _, _) ->
1000
    Acc.
×
1001

1002
make_table_installed(Node, R, RPath) ->
1003
    Columns = [<<"Name">>, <<"Summary">>, <<"">>, <<"">>],
×
1004
    ModulesInstalled =
×
1005
        ejabberd_cluster:call(Node,
1006
                              ejabberd_web_admin,
1007
                              make_command_raw_value,
1008
                              [modules_installed, R, []]),
1009
    Rows =
×
1010
        lists:map(fun({Name, Summary}) ->
1011
                     NameBin = misc:atom_to_binary(Name),
×
1012
                     Upgrade =
×
1013
                         ejabberd_cluster:call(Node,
1014
                                               ejabberd_web_admin,
1015
                                               make_command,
1016
                                               [module_upgrade,
1017
                                                R,
1018
                                                [{<<"module">>, NameBin}],
1019
                                                [{only, button}, {input_name_append, [NameBin]}]]),
1020
                     Uninstall =
×
1021
                         ejabberd_cluster:call(Node,
1022
                                               ejabberd_web_admin,
1023
                                               make_command,
1024
                                               [module_uninstall,
1025
                                                R,
1026
                                                [{<<"module">>, NameBin}],
1027
                                                [{only, button},
1028
                                                 {style, danger},
1029
                                                 {input_name_append, [NameBin]}]]),
1030
                     {?C(NameBin), ?C(list_to_binary(Summary)), Upgrade, Uninstall}
×
1031
                  end,
1032
                  ModulesInstalled),
1033
    ejabberd_web_admin:make_table(200, RPath, Columns, Rows).
×
1034

1035
make_table_available(Node, R, RPath) ->
1036
    Columns = [<<"Name">>, <<"Summary">>, <<"">>],
×
1037
    ModulesAll =
×
1038
        ejabberd_cluster:call(Node,
1039
                              ejabberd_web_admin,
1040
                              make_command_raw_value,
1041
                              [modules_available, R, []]),
1042
    ModulesInstalled =
×
1043
        ejabberd_cluster:call(Node,
1044
                              ejabberd_web_admin,
1045
                              make_command_raw_value,
1046
                              [modules_installed, R, []]),
1047
    ModulesNotInstalled =
×
1048
        lists:filter(fun({Mod, _}) -> not lists:keymember(Mod, 1, ModulesInstalled) end,
×
1049
                     ModulesAll),
1050
    Rows =
×
1051
        lists:map(fun({Name, Summary}) ->
1052
                     NameBin = misc:atom_to_binary(Name),
×
1053
                     Install =
×
1054
                         ejabberd_cluster:call(Node,
1055
                                               ejabberd_web_admin,
1056
                                               make_command,
1057
                                               [module_install,
1058
                                                R,
1059
                                                [{<<"module">>, NameBin}],
1060
                                                [{only, button}, {input_name_append, [NameBin]}]]),
1061
                     {?C(NameBin), ?C(list_to_binary(Summary)), Install}
×
1062
                  end,
1063
                  ModulesNotInstalled),
1064
    ejabberd_web_admin:make_table(200, RPath, Columns, Rows).
×
1065

1066
webadmin_node_contrib(Node, Query, Lang) ->
1067
    QueryRes = list_modules_parse_query(Query),
×
1068
    Contents = get_content(Node, Query, Lang),
×
1069
    Result =
×
1070
        case QueryRes of
1071
            ok ->
1072
                [?XREST(?T("Submitted"))];
×
1073
            nothing ->
1074
                []
×
1075
        end,
1076
    Result ++ Contents.
×
1077
%% @format-end
1078

1079
get_module_home(Module, Attrs) ->
1080
    case get_module_information(home, Attrs) of
×
1081
        "https://github.com/processone/ejabberd-contrib/tree/master/" = P1 ->
1082
            P1 ++ atom_to_list(Module);
×
1083
        Other ->
1084
            Other
×
1085
    end.
1086

1087
get_module_summary(Attrs) ->
1088
    get_module_information(summary, Attrs).
×
1089

1090
get_module_author(Attrs) ->
1091
    get_module_information(author, Attrs).
×
1092

1093
get_module_path(Attrs) ->
1094
    get_module_information(path, Attrs).
×
1095

1096
get_module_information(Attribute, Attrs) ->
1097
    case lists:keyfind(Attribute, 1, Attrs) of
×
1098
        false -> "";
×
1099
        {_, Value} -> Value
×
1100
    end.
1101

1102
get_installed_module_el({ModAtom, Attrs}, Lang) ->
1103
    Mod = misc:atom_to_binary(ModAtom),
×
1104
    Home = list_to_binary(get_module_home(ModAtom, Attrs)),
×
1105
    Summary = list_to_binary(get_module_summary(Attrs)),
×
1106
    Author = list_to_binary(get_module_author(Attrs)),
×
1107
    FromPath = get_module_path(Attrs),
×
1108
    FromFile = case find_commit_json_path(FromPath) of
×
1109
                   {ok, FF} -> FF;
×
1110
                   {error, _} -> "dummypath"
×
1111
               end,
1112
    #{sha := CommitSha,
×
1113
      date := CommitDate,
1114
      message := CommitMessage,
1115
      author_name := CommitAuthorName,
1116
      commit_html_url := CommitHtmlUrl} = get_commit_details2(FromFile),
1117

1118
    [SourceSpec] = [S || {M, S} <- available(), M == ModAtom],
×
1119
    SourceFile = find_commit_json(SourceSpec),
×
1120
    #{sha := SourceSha,
×
1121
      date := SourceDate,
1122
      message := SourceMessage,
1123
      author_name := SourceAuthorName,
1124
      commit_html_url := SourceHtmlUrl} = get_commit_details2(SourceFile),
1125

1126
    UpgradeEls =
×
1127
        case CommitSha == SourceSha of
1128
            true ->
1129
                [];
×
1130
            false ->
1131
                SourceTitleEl = make_title_el(SourceDate, SourceMessage, SourceAuthorName),
×
1132
                [?XE(<<"td">>,
×
1133
                     [?INPUT(<<"checkbox">>, <<"selected_upgrade">>, Mod),
1134
                      ?C(<<" ">>),
1135
                      ?AXC(SourceHtmlUrl, [SourceTitleEl], binary:part(SourceSha, {0, 8}))
1136
                     ]
1137
                    )
1138
                ]
1139
        end,
1140

1141
    Started =
×
1142
        case gen_mod:is_loaded(hd(ejabberd_option:hosts()), ModAtom) of
1143
            false ->
1144
                [?C(<<" ">>)];
×
1145
            true ->
1146
                []
×
1147
        end,
1148
    TitleEl = make_title_el(CommitDate, CommitMessage, CommitAuthorName),
×
1149
    Status = get_module_status_el(ModAtom),
×
1150
    HomeTitleEl = make_home_title_el(Summary, Author),
×
1151
    ?XE(<<"tr">>,
×
1152
        [?XE(<<"td">>, [?AXC(Home, [HomeTitleEl], Mod)]),
1153
         ?XE(<<"td">>,
1154
             [?INPUTTD(<<"checkbox">>, <<"selected_uninstall">>, Mod),
1155
              ?C(<<" ">>),
1156
              get_commit_link(CommitHtmlUrl, TitleEl, CommitSha),
1157
              ?C(<<" - ">>)]
1158
             ++ Started
1159
             ++ Status)
1160
        | UpgradeEls]).
1161

1162
get_module_status_el(ModAtom) ->
1163
    case {get_module_status(ModAtom),
×
1164
          get_module_status(elixir_module_name(ModAtom))} of
1165
        {Str, unknown} when is_list(Str) ->
1166
            [?C(<<" ">>), ?C(Str)];
×
1167
        {unknown, Str} when is_list(Str) ->
1168
            [?C(<<" ">>), ?C(Str)];
×
1169
        {unknown, unknown} ->
1170
            []
×
1171
    end.
1172

1173
get_module_status(Module) ->
1174
    try Module:mod_status() of
×
1175
        Str when is_list(Str) ->
1176
            Str
×
1177
    catch
1178
        _:_ ->
1179
            unknown
×
1180
    end.
1181

1182
%% When a module named mod_whatever in ejabberd-modules
1183
%% is written in Elixir, its runtime name is 'Elixir.ModWhatever'
1184
get_runtime_module_name(Module) ->
1185
    case is_elixir_module(Module) of
×
1186
        true -> elixir_module_name(Module);
×
1187
        false -> Module
×
1188
    end.
1189

1190
is_elixir_module(Module) ->
1191
    LibDir = module_src_dir(Module),
×
1192
    Lib = filename:join(LibDir, "lib"),
×
1193
    Src = filename:join(LibDir, "src"),
×
1194
    case {filelib:wildcard(Lib++"/*.{ex}"),
×
1195
          filelib:wildcard(Src++"/*.{erl}")} of
1196
        {[_ | _], []} ->
1197
            true;
×
1198
        {[], [_ | _]} ->
1199
            false
×
1200
    end.
1201

1202
%% Converts mod_some_thing to Elixir.ModSomeThing
1203
elixir_module_name(ModAtom) ->
1204
    list_to_atom("Elixir." ++ elixir_module_name("_" ++ atom_to_list(ModAtom), [])).
×
1205

1206
elixir_module_name([], Res) ->
1207
    lists:reverse(Res);
×
1208
elixir_module_name([$_, Char | Remaining], Res) ->
1209
    [Upper] = uppercase([Char]),
×
1210
    elixir_module_name(Remaining, [Upper | Res]);
×
1211
elixir_module_name([Char | Remaining], Res) ->
1212
    elixir_module_name(Remaining, [Char | Res]).
×
1213

1214
-ifdef(HAVE_URI_STRING).
1215
uppercase(String) ->
1216
    string:uppercase(String). % OTP 20 or higher
×
1217
-else.
1218
uppercase(String) ->
1219
    string:to_upper(String). % OTP older than 20
1220
-endif.
1221

1222
get_available_module_el({ModAtom, Attrs}) ->
1223
    Installed = installed(),
×
1224
    Mod = misc:atom_to_binary(ModAtom),
×
1225
    Home = list_to_binary(get_module_home(ModAtom, Attrs)),
×
1226
    Summary = list_to_binary(get_module_summary(Attrs)),
×
1227
    Author = list_to_binary(get_module_author(Attrs)),
×
1228
    HomeTitleEl = make_home_title_el(Summary, Author),
×
1229
    InstallCheckbox =
×
1230
        case lists:keymember(ModAtom, 1, Installed) of
1231
            false -> [?INPUT(<<"checkbox">>, <<"selected_install">>, Mod)];
×
1232
            true -> [?INPUTCHECKED(<<"checkbox">>, <<"selected_install">>, Mod)]
×
1233
        end,
1234
    ?XE(<<"tr">>,
×
1235
        [?XE(<<"td">>, InstallCheckbox ++ [?C(<<" ">>), ?AXC(Home, [HomeTitleEl], Mod)]),
1236
         ?XE(<<"td">>, [?C(Summary)])]).
1237

1238
get_installed_modules_table(Lang) ->
1239
    Modules = installed(),
×
1240
    Tail = [?XE(<<"tr">>,
×
1241
                [?XE(<<"td">>, []),
1242
                 ?XE(<<"td">>,
1243
                     [?INPUTTD(<<"submit">>, <<"uninstall">>, ?T("Uninstall"))]
1244
                    ),
1245
                 ?XE(<<"td">>,
1246
                     [?INPUTT(<<"submit">>, <<"upgrade">>, ?T("Upgrade"))]
1247
                    )
1248
                ]
1249
               )
1250
           ],
1251
    TBody = [get_installed_module_el(Module, Lang) || Module <- lists:sort(Modules)],
×
1252
    ?XAE(<<"table">>,
×
1253
         [],
1254
         [?XE(<<"tbody">>, TBody ++ Tail)]
1255
        ).
1256

1257
get_available_modules_table(Lang) ->
1258
    Modules = get_available_notinstalled(),
×
1259
    Tail = [?XE(<<"tr">>,
×
1260
                [?XE(<<"td">>,
1261
                     [?INPUTT(<<"submit">>, <<"install">>, ?T("Install"))]
1262
                    )
1263
                ]
1264
               )
1265
           ],
1266
    TBody = [get_available_module_el(Module) || Module <- lists:sort(Modules)],
×
1267
    ?XAE(<<"table">>,
×
1268
         [],
1269
         [?XE(<<"tbody">>, TBody ++ Tail)]
1270
        ).
1271

1272
make_title_el(Date, Message, AuthorName) ->
1273
    LinkTitle = <<Message/binary, "\n", AuthorName/binary, "\n", Date/binary>>,
×
1274
    {<<"title">>, LinkTitle}.
×
1275

1276
make_home_title_el(Summary, Author) ->
1277
    LinkTitle = <<Summary/binary, "\n", Author/binary>>,
×
1278
    {<<"title">>, LinkTitle}.
×
1279

1280
get_commit_link(_CommitHtmlUrl, _TitleErl, unknown_sha) ->
1281
    ?C(<<"Please Update Specs">>);
×
1282
get_commit_link(CommitHtmlUrl, TitleEl, CommitSha) ->
1283
    ?AXC(CommitHtmlUrl, [TitleEl], binary:part(CommitSha, {0, 8})).
×
1284

1285
get_content(Node, Query, Lang) ->
1286
    {{_CommandCtl}, _Res} =
×
1287
        case catch parse_and_execute(Query, Node) of
1288
            {'EXIT', _} -> {{""}, <<"">>};
×
1289
            Result_tuple -> Result_tuple
×
1290
        end,
1291

1292
    AvailableModulesEls = get_available_modules_table(Lang),
×
1293
    InstalledModulesEls = get_installed_modules_table(Lang),
×
1294

1295
    Sources = get_sources_list(),
×
1296
    SourceEls = (?XAE(<<"table">>,
×
1297
                      [],
1298
                      [?XE(<<"tbody">>,
1299
                           (lists:map(
1300
                              fun(Dirname) ->
1301
                                      #{sha := CommitSha,
×
1302
                                        date := CommitDate,
1303
                                        message := CommitMessage,
1304
                                        html := Html,
1305
                                        author_name := AuthorName,
1306
                                        commit_html_url := CommitHtmlUrl
1307
                                       } = get_commit_details(Dirname),
1308
                                      TitleEl = make_title_el(CommitDate, CommitMessage, AuthorName),
×
1309
                                      ?XE(<<"tr">>,
×
1310
                                          [?XE(<<"td">>, [?AC(Html, Dirname)]),
1311
                                           ?XE(<<"td">>,
1312
                                               [get_commit_link(CommitHtmlUrl, TitleEl, CommitSha)]
1313
                                              ),
1314
                                           ?XE(<<"td">>, [?C(CommitMessage)])
1315
                                          ])
1316
                              end,
1317
                              lists:sort(Sources)
1318
                             ))
1319
                          )
1320
                      ]
1321
                     )),
1322

1323
    [?XC(<<"p">>,
×
1324
         translate:translate(
1325
           Lang, ?T("Update specs to get modules source, then install desired ones.")
1326
          )
1327
        ),
1328
     ?XAE(<<"form">>,
1329
          [{<<"method">>, <<"post">>}],
1330
          [?XCT(<<"h3">>, ?T("Sources Specs:")),
1331
           SourceEls,
1332
           ?BR,
1333
           ?INPUTT(<<"submit">>,
1334
                   <<"updatespecs">>,
1335
                   translate:translate(Lang, ?T("Update Specs"))),
1336

1337
           ?XCT(<<"h3">>, ?T("Installed Modules:")),
1338
           InstalledModulesEls,
1339
           ?BR,
1340

1341
           ?XCT(<<"h3">>, ?T("Other Modules Available:")),
1342
           AvailableModulesEls
1343
          ]
1344
         )
1345
    ].
1346

1347
get_sources_list() ->
1348
    case file:list_dir(sources_dir()) of
×
1349
        {ok, Filenames} -> Filenames;
×
1350
        {error, enoent} -> []
×
1351
    end.
1352

1353
get_available_notinstalled() ->
1354
    Installed = installed(),
×
1355
    lists:filter(
×
1356
      fun({Mod, _}) ->
1357
              not lists:keymember(Mod, 1, Installed)
×
1358
      end,
1359
      available()
1360
     ).
1361

1362
parse_and_execute(Query, Node) ->
1363
    {[Exec], _} = lists:partition(
×
1364
                    fun(ExType) ->
1365
                            lists:keymember(ExType, 1, Query)
×
1366
                    end,
1367
                    [<<"updatespecs">>]
1368
                   ),
1369
    Commands = {get_val(<<"updatespecs">>, Query)},
×
1370
    {_, R} = parse1_command(Exec, Commands, Node),
×
1371
    {Commands, R}.
×
1372

1373
get_val(Val, Query) ->
1374
    {value, {_, R}} = lists:keysearch(Val, 1, Query),
×
1375
    binary_to_list(R).
×
1376

1377
parse1_command(<<"updatespecs">>, {_}, _Node) ->
1378
    Res = update(),
×
1379
    {oook, io_lib:format("~p", [Res])}.
×
1380

1381
list_modules_parse_query(Query) ->
1382
    case {lists:keysearch(<<"install">>, 1, Query),
×
1383
          lists:keysearch(<<"upgrade">>, 1, Query),
1384
          lists:keysearch(<<"uninstall">>, 1, Query)}
1385
    of
1386
        {{value, _}, _, _} -> list_modules_parse_install(Query);
×
1387
        {_, {value, _}, _} -> list_modules_parse_upgrade(Query);
×
1388
        {_, _, {value, _}} -> list_modules_parse_uninstall(Query);
×
1389
        _ -> nothing
×
1390
    end.
1391

1392
list_modules_parse_install(Query) ->
1393
    lists:foreach(
×
1394
      fun({Mod, _}) ->
1395
              ModBin = misc:atom_to_binary(Mod),
×
1396
              case lists:member({<<"selected_install">>, ModBin}, Query) of
×
1397
                  true -> install(Mod);
×
1398
                  _ -> ok
×
1399
              end
1400
      end,
1401
      get_available_notinstalled()),
1402
    ok.
×
1403

1404
list_modules_parse_upgrade(Query) ->
1405
    lists:foreach(
×
1406
      fun({Mod, _}) ->
1407
              ModBin = misc:atom_to_binary(Mod),
×
1408
              case lists:member({<<"selected_upgrade">>, ModBin}, Query) of
×
1409
                  true -> upgrade(Mod);
×
1410
                  _ -> ok
×
1411
              end
1412
      end,
1413
      installed()),
1414
    ok.
×
1415

1416
list_modules_parse_uninstall(Query) ->
1417
    lists:foreach(
×
1418
      fun({Mod, _}) ->
1419
              ModBin = misc:atom_to_binary(Mod),
×
1420
              case lists:member({<<"selected_uninstall">>, ModBin}, Query) of
×
1421
                  true -> uninstall(Mod);
×
1422
                  _ -> ok
×
1423
              end
1424
      end,
1425
      installed()),
1426
    ok.
×
1427

1428
install_contrib_modules(Modules, Config) ->
1429
    lists:filter(fun(Module) ->
×
1430
                         case install(misc:atom_to_binary(Module), Config) of
×
1431
                             {error, conflict} ->
1432
                                 false;
×
1433
                             ok ->
1434
                                 true
×
1435
                         end
1436
                 end,
1437
                 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