%%%---------------------------------------------------------------------- %%% File : ejabberd_web_admin.erl %%% Author : Alexey Shchepin %%% Purpose : Administration web interface %%% Created : 9 Apr 2004 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2010 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA %%% 02111-1307 USA %%% %%%---------------------------------------------------------------------- %%%% definitions -module(ejabberd_web_admin). -author('alexey@process-one.net'). %% External exports -export([process/2, list_users/4, list_users_in_diapason/4, term_to_id/1]). -include_lib("exmpp/include/exmpp.hrl"). -include("ejabberd.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -define(INPUTATTRS(Type, Name, Value, Attrs), ?XA("input", Attrs ++ [?XMLATTR('type', Type), ?XMLATTR('name', Name), ?XMLATTR('value', Value)])). %%%================================== %%%% get_acl_access %% @spec (Path::[string()], HttpMethod) -> {HostOfRule, [AccessRule]} %% HttpMethod = 'GET' | 'POST' %% All accounts can access those URLs get_acl_rule([],_) -> {"localhost", [all]}; get_acl_rule(["style.css"],_) -> {"localhost", [all]}; get_acl_rule(["logo.png"],_) -> {"localhost", [all]}; get_acl_rule(["logo-fill.png"],_) -> {"localhost", [all]}; get_acl_rule(["favicon.ico"],_) -> {"localhost", [all]}; get_acl_rule(["additions.js"],_) -> {"localhost", [all]}; %% This page only displays vhosts that the user is admin: get_acl_rule(["vhosts"],_) -> {"localhost", [all]}; %% The pages of a vhost are only accesible if the user is admin of that vhost: get_acl_rule(["server", VHost | _RPath], 'GET') -> {VHost, [configure, webadmin_view]}; get_acl_rule(["server", VHost | _RPath], 'POST') -> {VHost, [configure]}; %% Default rule: only global admins can access any other random page get_acl_rule(_RPath, 'GET') -> {global, [configure, webadmin_view]}; get_acl_rule(_RPath, 'POST') -> {global, [configure]}. is_acl_match(Host, Rules, Jid) -> lists:any( fun(Rule) -> allow == acl:match_rule(Host, Rule, Jid) end, Rules). %%%================================== %%%% Menu Items Access get_jid(Auth, HostHTTP, Method) -> case get_auth_admin(Auth, HostHTTP, [], Method) of {ok, {User, Server}} -> exmpp_jid:make(User, Server, ""); {unauthorized, Error} -> ?ERROR_MSG("Unauthorized ~p: ~p", [Auth, Error]), throw({unauthorized, Auth}) end. get_menu_items(global, cluster, Lang, JID) -> {Base, _, Items} = make_server_menu([], [], Lang, JID), lists:map( fun({URI, Name}) -> {Base++URI++"/", Name}; ({URI, Name, _SubMenu}) -> {Base++URI++"/", Name} end, Items ); get_menu_items(Host, cluster, Lang, JID) -> {Base, _, Items} = make_host_menu(Host, [], Lang, JID), lists:map( fun({URI, Name}) -> {Base++URI++"/", Name}; ({URI, Name, _SubMenu}) -> {Base++URI++"/", Name} end, Items ); get_menu_items(Host, Node, Lang, JID) -> {Base, _, Items} = make_host_node_menu(Host, Node, Lang, JID), lists:map( fun({URI, Name}) -> {Base++URI++"/", Name}; ({URI, Name, _SubMenu}) -> {Base++URI++"/", Name} end, Items ). is_allowed_path(BasePath, {Path, _}, JID) -> is_allowed_path(BasePath ++ [Path], JID); is_allowed_path(BasePath, {Path, _, _}, JID) -> is_allowed_path(BasePath ++ [Path], JID). is_allowed_path(["admin" | Path], JID) -> is_allowed_path(Path, JID); is_allowed_path(Path, JID) -> {HostOfRule, AccessRule} = get_acl_rule(Path, 'GET'), is_acl_match(HostOfRule, AccessRule, JID). %% @spec(Path) -> URL %% where Path = [string()] %% URL = string() %% Convert ["admin", "user", "tom"] -> "/admin/user/tom/" %%path_to_url(Path) -> %% "/" ++ string:join(Path, "/") ++ "/". %% @spec(URL) -> Path %% where Path = [string()] %% URL = string() %% Convert "admin/user/tom" -> ["admin", "user", "tom"] url_to_path(URL) -> string:tokens(URL, "/"). %%%================================== %%%% process/2 process(["doc", LocalFile], _Request) -> DocPath = case os:getenv("EJABBERD_DOC_PATH") of P when is_list(P) -> P; false -> "/share/doc/ejabberd/" end, %% Code based in mod_http_fileserver FileName = filename:join(DocPath, LocalFile), case file:read_file(FileName) of {ok, FileContents} -> ?DEBUG("Delivering content.", []), {200, [{"Server", "ejabberd"}], FileContents}; {error, Error} -> ?DEBUG("Delivering error: ~p", [Error]), Help = " " ++ FileName ++ " - Try to specify the path to ejabberd documentation " "with the environment variable EJABBERD_DOC_PATH. Check the ejabberd Guide for more information.", case Error of eacces -> {403, [], "Forbidden"++Help}; enoent -> {404, [], "Not found"++Help}; _Else -> {404, [], atom_to_list(Error)++Help} end end; process(["server", SHost | RPath] = Path, #request{auth = Auth, lang = Lang, host = HostHTTP, method = Method} = Request) -> Host = exmpp_stringprep:nameprep(SHost), case lists:member(Host, ?MYHOSTS) of true -> case get_auth_admin(Auth, HostHTTP, Path, Method) of {ok, {User, Server}} -> AJID = get_jid(Auth, HostHTTP, Method), process_admin(Host, Request#request{path = RPath, auth = {auth_jid, Auth, AJID}, us = {User, Server}}); {unauthorized, "no-auth-provided"} -> {401, [{"WWW-Authenticate", "basic realm=\"ejabberd\""}], ejabberd_web:make_xhtml([?XCT('h1', "Unauthorized")])}; {unauthorized, Error} -> ?WARNING_MSG("Access ~p failed with error: ~p", [Auth, Error]), {401, [{"WWW-Authenticate", "basic realm=\"auth error, retry login to ejabberd\""}], ejabberd_web:make_xhtml([?XCT('h1', "Unauthorized")])} end; false -> ejabberd_web:error(not_found) end; process(RPath, #request{auth = Auth, lang = Lang, host = HostHTTP, method = Method} = Request) -> case get_auth_admin(Auth, HostHTTP, RPath, Method) of {ok, {User, Server}} -> AJID = get_jid(Auth, HostHTTP, Method), process_admin(global, Request#request{path = RPath, auth = {auth_jid, Auth, AJID}, us = {User, Server}}); {unauthorized, "no-auth-provided"} -> {401, [{"WWW-Authenticate", "basic realm=\"ejabberd\""}], ejabberd_web:make_xhtml([?XCT('h1', "Unauthorized")])}; {unauthorized, Error} -> ?WARNING_MSG("Access ~p failed with error: ~p", [Auth, Error]), {401, [{"WWW-Authenticate", "basic realm=\"auth error, retry login to ejabberd\""}], ejabberd_web:make_xhtml([?XCT('h1', "Unauthorized")])} end. get_auth_admin(Auth, HostHTTP, RPath, Method) -> case Auth of {SJID, Pass} -> try {HostOfRule, AccessRule} = get_acl_rule(RPath, Method), JID = exmpp_jid:parse(SJID), User = exmpp_jid:node_as_list(JID), Server = exmpp_jid:domain_as_list(JID), case User == undefined of true -> %% If only specified username, not username@server get_auth_account(HostOfRule, AccessRule, User, HostHTTP, Pass); false -> get_auth_account(HostOfRule, AccessRule, User, Server, Pass) end catch _ -> {unauthorized, "badformed-jid"} end; _ -> {unauthorized, "no-auth-provided"} end. get_auth_account(HostOfRule, AccessRule, User, Server, Pass) -> case catch ejabberd_auth:check_password(User, Server, Pass) of true -> case is_acl_match(HostOfRule, AccessRule, exmpp_jid:make(User, Server)) of false -> {unauthorized, "unprivileged-account"}; true -> {ok, {User, Server}} end; false -> case ejabberd_auth:is_user_exists(User, Server) of true -> {unauthorized, "bad-password"}; false -> {unauthorized, "inexistent-account"} end; _ -> {unauthorized, "badformed-jid"} end. %%%================================== %%%% make_xhtml make_xhtml(Els, Host, Lang, JID) -> make_xhtml(Els, Host, cluster, Lang, JID). %% @spec (Els, Host, Node, Lang, JID) -> {200, [html], xmlelement()} %% where Host = global | string() %% Node = cluster | atom() %% JID = jid() make_xhtml(Els, Host, Node, Lang, JID) -> Base = get_base_path(Host, cluster), %% Enforcing 'cluster' on purpose here MenuItems = make_navigation(Host, Node, Lang, JID), {200, [html], #xmlel{ns = ?NS_XHTML, name = 'html', attrs = [ exmpp_xml:attribute(?NS_XML, 'lang', Lang), ?XMLATTR('lang', Lang)], children = [#xmlel{ns = ?NS_XHTML, name = 'head', children = [?XCT('title', "ejabberd Web Admin"), #xmlel{ns = ?NS_XHTML, name = 'meta', attrs = [ ?XMLATTR('http-equiv', <<"Content-Type">>), ?XMLATTR('content', <<"text/html; charset=utf-8">>)]}, #xmlel{ns = ?NS_XHTML, name = 'script', %% This children is to ensure exmpp puts: children = [?C(".")], attrs = [ ?XMLATTR('src', Base ++ "additions.js"), ?XMLATTR('type', <<"text/javascript">>)]}, #xmlel{ns = ?NS_XHTML, name = 'link', attrs = [ ?XMLATTR('href', Base ++ "favicon.ico"), ?XMLATTR('type', <<"image/x-icon">>), ?XMLATTR('rel', <<"shortcut icon">>)]}, #xmlel{ns = ?NS_XHTML, name = 'link', attrs = [ ?XMLATTR('href', Base ++ "style.css"), ?XMLATTR('type', <<"text/css">>), ?XMLATTR('rel', <<"stylesheet">>)]}]}, ?XE('body', [?XAE('div', [?XMLATTR('id', <<"container">>)], [?XAE('div', [?XMLATTR('id', <<"header">>)], [?XE('h1', [?ACT("/admin/", "ejabberd Web Admin")] )]), ?XAE('div', [?XMLATTR('id', <<"navigation">>)], [?XE('ul', MenuItems )]), ?XAE('div', [?XMLATTR('id', <<"content">>)], Els), ?XAE('div', [?XMLATTR('id', <<"clearcopyright">>)], [#xmlcdata{cdata = <<>>}])]), ?XAE('div', [?XMLATTR('id', <<"copyrightouter">>)], [?XAE('div', [?XMLATTR('id', <<"copyright">>)], [?XC('p', "ejabberd (c) 2002-2010 ProcessOne") ])])]) ]}}. get_base_path(global, cluster) -> "/admin/"; get_base_path(Host, cluster) -> "/admin/server/" ++ Host ++ "/"; get_base_path(global, Node) -> "/admin/node/" ++ atom_to_list(Node) ++ "/"; get_base_path(Host, Node) -> "/admin/server/" ++ Host ++ "/node/" ++ atom_to_list(Node) ++ "/". %%%================================== %%%% css & images additions_js() -> " function selectAll() { for(i=0;i Base = get_base_path(Host, cluster), " html,body { background: white; margin: 0; padding: 0; height: 100%; } #container { padding: 0; margin: 0; min-height: 100%; height: 100%; margin-bottom: -30px; } html>body #container { height: auto; } #header h1 { width: 100%; height: 55px; padding: 0; margin: 0; background: transparent url(\"" ++ Base ++ "logo-fill.png\"); } #header h1 a { position: absolute; top: 0; left: 0; width: 100%; height: 55px; padding: 0; margin: 0; background: transparent url(\"" ++ Base ++ "logo.png\") no-repeat; display: block; text-indent: -700em; } #clearcopyright { display: block; width: 100%; height: 30px; } #copyrightouter { display: table; width: 100%; height: 30px; } #copyright { display: table-cell; vertical-align: bottom; width: 100%; height: 30px; } #copyright p { margin-left: 0; margin-right: 0; margin-top: 5px; margin-bottom: 0; padding-left: 0; padding-right: 0; padding-top: 1px; padding-bottom: 1px; width: 100%; color: #ffffff; background-color: #fe8a00; font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 7pt; font-weight: bold; text-align: center; } #navigation ul { position: absolute; top: 65px; left: 0; padding: 0 1px 1px 1px; margin: 0; font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 8pt; font-weight: bold; border-top: 1px solid #d47911; width: 17em; } #navigation ul li { list-style: none; margin: 0; text-align: left; display: inline; } #navigation ul li a { margin: 0; display: block; padding: 3px 6px 3px 9px; border-left: 1em solid #ffc78c; border-right: 1px solid #d47911; border-bottom: 1px solid #d47911; background: #ffe3c9; text-decoration: none; } #navigation ul li a:link { color: #844; } #navigation ul li a:visited { color: #766; } #navigation ul li a:hover { border-color: #fc8800; color: #FFF; background: #332; } ul li #navhead a, ul li #navheadsub a, ul li #navheadsubsub a { text-align: center; border-top: 1px solid #d47911; border-bottom: 2px solid #d47911; background: #FED6A6; } #navheadsub, #navitemsub { border-left: 7px solid white; margin-left: 2px; } #navheadsubsub, #navitemsubsub { border-left: 14px solid white; margin-left: 4px; } #lastactivity li { font-weight: bold; border: 1px solid #d6760e; background-color: #fff2e8; padding: 2px; margin-bottom: -1px; } td.copy { color: #ffffff; background-color: #fe8a00; font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 7pt; font-weight: bold; text-align: center; } input { font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 10pt; border: 1px solid #d6760e; color: #723202; background-color: #fff2e8; vertical-align: middle; margin-bottom: 0px; padding: 0.1em; } input[type=submit] { font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 8pt; font-weight: bold; color: #ffffff; background-color: #fe8a00; border: 1px solid #d6760e; } textarea { font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 10pt; border: 1px solid #d6760e; color: #723202; background-color: #fff2e8; } select { border: 1px solid #d6760e; color: #723202; background-color: #fff2e8; vertical-align: middle; margin-bottom: 0px; padding: 0.1em; } thead { color: #000000; background-color: #ffffff; font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 10pt; font-weight: bold; } tr.head { color: #ffffff; background-color: #3b547a; font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 9pt; font-weight: bold; text-align: center; } tr.oddraw { color: #412c75; background-color: #ccd4df; font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 9pt; font-weight: normal; text-align: center; } tr.evenraw { color: #412c75; background-color: #dbe0e8; font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 9pt; font-weight: normal; text-align: center; } td.leftheader { color: #412c75; background-color: #ccccc1; font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 9pt; font-weight: bold; padding-left: 5px; padding-top: 2px; padding-bottom: 2px; margin-top: 0px; margin-bottom: 0px; } td.leftcontent { color: #000044; background-color: #e6e6df; font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 7pt; font-weight: normal; padding-left: 5px; padding-right: 5px; padding-top: 2px; padding-bottom: 2px; margin-top: 0px; margin-bottom: 0px; } td.rightcontent { color: #000044; font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 10pt; font-weight: normal; text-align: justify; padding-left: 10px; padding-right: 10px; padding-bottom: 5px; } h1 { color: #000044; font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 14pt; font-weight: bold; text-align: center; padding-top: 2px; padding-bottom: 2px; margin-top: 0px; margin-bottom: 0px; } h2 { color: #000044; font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 12pt; font-weight: bold; text-align: center; padding-top: 2px; padding-bottom: 2px; margin-top: 0px; margin-bottom: 0px; } h3 { color: #000044; font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 10pt; font-weight: bold; text-align: left; padding-top: 20px; padding-bottom: 2px; margin-top: 0px; margin-bottom: 0px; } #content a:link { color: #990000; font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 10pt; font-weight: bold; text-decoration: underline; } #content a:visited { color: #990000; font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 10pt; font-weight: bold; text-decoration: underline; } #content a:hover { color: #cc6600; font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 10pt; font-weight: bold; text-decoration: underline; } #content ul li { list-style-type: disc; font-size: 10pt; /*font-size: 7pt;*/ padding-left: 10px; } #content ul.noliststyle > li { list-style-type: none; } #content li.big { font-size: 10pt; } #content { font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 10pt; padding-left: 17em; padding-top: 5px; } div.guidelink { text-align: right; padding-right: 1em; } table.withtextareas>tbody>tr>td { vertical-align: top; } p.result { border: 1px; border-style: dashed; border-color: #FE8A02; padding: 1em; margin-right: 1em; background: #FFE3C9; } *.alignright { font-size: 10pt; text-align: right; } ". favicon() -> jlib:decode_base64( "AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAAAA" "AAAAAAAAAAAAAAAAAAAAAAAAJf+cAAIPsAAGC8gAVhecAAIr8ACiR7wBBmO" "cAUKPsAFun8ABhqeoAgLryAJLB8ACz1PcAv9r7AMvi+gAAAAAAAgICARMhI" "CAkJCQkQkFCQgICN2d2cSMgJCRevdvVQkICAlqYh5MgICQkXrRCQkJCMgI7" "kiAjICAUFF2swkFBQRQUXazCQUFBAgI7kiAgICAkJF60QkJCQgICOpiHkyA" "gJCRevdvlQkICAjdndnMgICQkJCRCQkJCAgICARAgICAAAAAAAAAAAAAAAA" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" "AAAAAAAAAAA"). logo() -> jlib:decode_base64( "iVBORw0KGgoAAAANSUhEUgAAAVcAAAA3CAMAAACPbPnEAAAAAXNSR0IArs4c" "6QAAAFFQTFRFcjICrFMI1nYO/ooC/o4G/o4O/pIS/pYa/pom/p4q/qIz/qY+" "/qpG/q5K/q5S/rJW/rZe/rZi/rpq/r5u/sJ2/sJ+/sqM/s6W/tam/tqu/ubG" "ry/SlQAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxIAAAsSAdLdfvwAAAAHdElN" "RQfaBBUNLS4ZElD3AAABoklEQVR42u2bSW7DMBAEHdqJszv79v+Hxp3WQQFD" "YSJ5JAioOvBgEQRdbAIj0t4UyGBTtiK93YgT9IyPM89smyPgFa94xStek7xC" "Uj2wP1K2uyN/rFndLpuVZecQsXEh8IrXlXnd/3jd4RWvq/AKOfXAtegsn4vm" "SpyJUJ7qPq1P6v7DmYgkJjKHcSNHbHQO8YpXvOIVr0leIaceuBPlSvzKa726" "w3mN52Nckk71NL6X4nl19e89b5N4xSte8YrXLK+QUw88inIryqVoJiCS1+Gq" "dM5EDs9hSl7rkX0m6DcB73+84hWveMVrllfIqQdeRXkQ5UaMPM+K30vNWb/G" "b63i38V1at+Gk3ovypPAK17xile8pnmFnHrgXZQXUQ6is98/qYnf+ky/3xoe" "Z8rTeFXa6unzPrf9vNrbsyhvAq94xSte8ZrmFXLqgU/RWbZx12H9O63IL12y" "/2GybFvvW7eu+73b7fBD4BWveMUrXtO8Qk498CU6yzbuE5nunqa3Nj7H+W9G" "157U1g2W7wV9Guh3AO98+8QrXvGKV7xmeYUMvgHvddyrncu7lwAAAABJRU5E" "rkJggg=="). logo_fill() -> jlib:decode_base64( "iVBORw0KGgoAAAANSUhEUgAAAAYAAAA3BAMAAADdxCZzAAAAAXNSR0IArs4c" "6QAAAB5QTFRF1nYO/ooC/o4O/pIS/p4q/q5K/rpq/sqM/tam/ubGzn/S/AAA" "AEFJREFUCNdlw0sRwCAQBUE+gSRHLGABC1jAAhbWAhZwC+88XdXOXb4UlFAr" "SmwN5ekdJY2BkudEec1QvrVQ/r3xOlK9HsTvertmAAAAAElFTkSuQmCC"). %%%================================== %%%% process_admin process_admin(global, #request{path = [], auth = {_, _, AJID}, lang = Lang}) -> %%Base = get_base_path(global, cluster), make_xhtml(?H1GL(?T("Administration"), "toc", "Contents") ++ [?XE('ul', [?LI([?ACT(MIU, MIN)]) || {MIU, MIN} <- get_menu_items(global, cluster, Lang, AJID)] ) ], global, Lang, AJID); process_admin(Host, #request{path = [], auth = {_, _Auth, AJID}, lang = Lang}) -> %%Base = get_base_path(Host, cluster), make_xhtml([?XCT('h1', "Administration"), ?XE('ul', [?LI([?ACT(MIU, MIN)]) || {MIU, MIN} <- get_menu_items(Host, cluster, Lang, AJID)] ) ], Host, Lang, AJID); process_admin(Host, #request{path = ["style.css"]}) -> {200, [{"Content-Type", "text/css"}, last_modified(), cache_control_public()], css(Host)}; process_admin(_Host, #request{path = ["favicon.ico"]}) -> {200, [{"Content-Type", "image/x-icon"}, last_modified(), cache_control_public()], favicon()}; process_admin(_Host, #request{path = ["logo.png"]}) -> {200, [{"Content-Type", "image/png"}, last_modified(), cache_control_public()], logo()}; process_admin(_Host, #request{path = ["logo-fill.png"]}) -> {200, [{"Content-Type", "image/png"}, last_modified(), cache_control_public()], logo_fill()}; process_admin(_Host, #request{path = ["additions.js"]}) -> {200, [{"Content-Type", "text/javascript"}, last_modified(), cache_control_public()], additions_js()}; process_admin(Host, #request{path = ["acls-raw"], q = Query, auth = {_, _Auth, AJID}, lang = Lang}) -> Res = case lists:keysearch("acls", 1, Query) of {value, {_, String}} -> case erl_scan:string(String) of {ok, Tokens, _} -> case erl_parse:parse_term(Tokens) of {ok, NewACLs} -> case acl:add_list(Host, NewACLs, true) of ok -> ok; _ -> error end; _ -> error end; _ -> error end; _ -> nothing end, ACLs = lists:keysort(2, ets:select(acl, [{{acl, {'$1', Host}, '$2'}, [], [{{acl, '$1', '$2'}}]}])), {NumLines, ACLsP} = term_to_paragraph(ACLs, 80), make_xhtml(?H1GL(?T("Access Control Lists"), "ACLDefinition", "ACL Definition") ++ case Res of ok -> [?XREST("Submitted")]; error -> [?XREST("Bad format")]; nothing -> [] end ++ [?XAE('form', [?XMLATTR('action', <<>>), ?XMLATTR('method', <<"post">>)], [?TEXTAREA("acls", integer_to_list(lists:max([16, NumLines])), "80", ACLsP++"."), ?BR, ?INPUTT("submit", "submit", "Submit") ]) ], Host, Lang, AJID); process_admin(Host, #request{method = Method, path = ["acls"], auth = {_, _Auth, AJID}, q = Query, lang = Lang}) -> ?DEBUG("query: ~p", [Query]), Res = case Method of 'POST' -> case catch acl_parse_query(Host, Query) of {'EXIT', _} -> error; NewACLs -> ?INFO_MSG("NewACLs at ~s: ~p", [Host, NewACLs]), case acl:add_list(Host, NewACLs, true) of ok -> ?INFO_MSG("NewACLs: ok", []), ok; _ -> error end end; _ -> nothing end, ACLs = lists:keysort( 2, ets:select(acl, [{{acl, {'$1', Host}, '$2'}, [], [{{acl, '$1', '$2'}}]}])), make_xhtml(?H1GL(?T("Access Control Lists"), "ACLDefinition", "ACL Definition") ++ case Res of ok -> [?XREST("Submitted")]; error -> [?XREST("Bad format")]; nothing -> [] end ++ [?XE('p', [?ACT("../acls-raw/", "Raw")])] ++ [?XAE('form', [?XMLATTR('action', <<>>), ?XMLATTR('method', <<"post">>)], [acls_to_xhtml(ACLs), ?BR, ?INPUTT("submit", "delete", "Delete Selected"), ?C(" "), ?INPUTT("submit", "submit", "Submit") ]) ], Host, Lang, AJID); process_admin(Host, #request{path = ["access-raw"], auth = {_, _Auth, AJID}, q = Query, lang = Lang}) -> SetAccess = fun(Rs) -> mnesia:transaction( fun() -> Os = mnesia:select( config, [{{config, {access, '$1', Host}, '$2'}, [], ['$_']}]), lists:foreach(fun(O) -> mnesia:delete_object(O) end, Os), lists:foreach( fun({access, Name, Rules}) -> mnesia:write({config, {access, Name, Host}, Rules}) end, Rs) end) end, Res = case lists:keysearch("access", 1, Query) of {value, {_, String}} -> case erl_scan:string(String) of {ok, Tokens, _} -> case erl_parse:parse_term(Tokens) of {ok, Rs} -> case SetAccess(Rs) of {atomic, _} -> ok; _ -> error end; _ -> error end; _ -> error end; _ -> nothing end, Access = ets:select(config, [{{config, {access, '$1', Host}, '$2'}, [], [{{access, '$1', '$2'}}]}]), {NumLines, AccessP} = term_to_paragraph(Access, 80), make_xhtml(?H1GL(?T("Access Rules"), "AccessRights", "Access Rights") ++ case Res of ok -> [?XREST("Submitted")]; error -> [?XREST("Bad format")]; nothing -> [] end ++ [?XAE('form', [?XMLATTR('action', <<>>), ?XMLATTR('method', <<"post">>)], [?TEXTAREA("access", integer_to_list(lists:max([16, NumLines])), "80", AccessP++"."), ?BR, ?INPUTT("submit", "submit", "Submit") ]) ], Host, Lang, AJID); process_admin(Host, #request{method = Method, path = ["access"], q = Query, auth = {_, _Auth, AJID}, lang = Lang}) -> ?DEBUG("query: ~p", [Query]), Res = case Method of 'POST' -> case catch access_parse_query(Host, Query) of {'EXIT', _} -> error; ok -> ok end; _ -> nothing end, AccessRules = ets:select(config, [{{config, {access, '$1', Host}, '$2'}, [], [{{access, '$1', '$2'}}]}]), make_xhtml(?H1GL(?T("Access Rules"), "AccessRights", "Access Rights") ++ case Res of ok -> [?XREST("Submitted")]; error -> [?XREST("Bad format")]; nothing -> [] end ++ [?XE('p', [?ACT("../access-raw/", "Raw")])] ++ [?XAE('form', [?XMLATTR('action', <<>>), ?XMLATTR('method', <<"post">>)], [access_rules_to_xhtml(AccessRules, Lang), ?BR, ?INPUTT("submit", "delete", "Delete Selected") ]) ], Host, Lang, AJID); process_admin(Host, #request{path = ["access", SName], q = Query, auth = {_, _Auth, AJID}, lang = Lang}) -> ?DEBUG("query: ~p", [Query]), Name = list_to_atom(SName), Res = case lists:keysearch("rules", 1, Query) of {value, {_, String}} -> case parse_access_rule(String) of {ok, Rs} -> ejabberd_config:add_global_option( {access, Name, Host}, Rs), ok; _ -> error end; _ -> nothing end, Rules = case ejabberd_config:get_global_option({access, Name, Host}) of undefined -> []; Rs1 -> Rs1 end, make_xhtml([?XC('h1', io_lib:format(?T("~s access rule configuration"), [SName]))] ++ case Res of ok -> [?XREST("Submitted")]; error -> [?XREST("Bad format")]; nothing -> [] end ++ [?XAE('form', [?XMLATTR('action', <<>>), ?XMLATTR('method', <<"post">>)], [access_rule_to_xhtml(Rules), ?BR, ?INPUTT("submit", "submit", "Submit") ]) ], Host, Lang, AJID); process_admin(global, #request{path = ["vhosts"], auth = {_, _Auth, AJID}, lang = Lang}) -> Res = list_vhosts(Lang, AJID), make_xhtml(?H1GL(?T("ejabberd virtual hosts"), "virtualhost", "Virtual Hosting") ++ Res, global, Lang, AJID); process_admin(Host, #request{path = ["users"], q = Query, auth = {_, _Auth, AJID}, lang = Lang}) when is_list(Host) -> Res = list_users(Host, Query, Lang, fun url_func/1), make_xhtml([?XCT('h1', "Users")] ++ Res, Host, Lang, AJID); process_admin(Host, #request{path = ["users", Diap], auth = {_, _Auth, AJID}, lang = Lang}) when is_list(Host) -> Res = list_users_in_diapason(Host, Diap, Lang, fun url_func/1), make_xhtml([?XCT('h1', "Users")] ++ Res, Host, Lang, AJID); process_admin(Host, #request{ path = ["online-users"], auth = {_, _Auth, AJID}, lang = Lang}) when is_list(Host) -> Res = list_online_users(Host, Lang), make_xhtml([?XCT('h1', "Online Users")] ++ Res, Host, Lang, AJID); process_admin(Host, #request{path = ["last-activity"], auth = {_, _Auth, AJID}, q = Query, lang = Lang}) when is_list(Host) -> ?DEBUG("query: ~p", [Query]), Month = case lists:keysearch("period", 1, Query) of {value, {_, Val}} -> Val; _ -> "month" end, Res = case lists:keysearch("ordinary", 1, Query) of {value, {_, _}} -> list_last_activity(Host, Lang, false, Month); _ -> list_last_activity(Host, Lang, true, Month) end, make_xhtml([?XCT('h1', "Users Last Activity")] ++ [?XAE('form', [?XMLATTR('action', <<>>), ?XMLATTR('method', <<"post">>)], [?CT("Period: "), ?XAE('select', [?XMLATTR('name', <<"period">>)], lists:map( fun({O, V}) -> Sel = if O == Month -> [?XMLATTR('selected', <<"selected">>)]; true -> [] end, ?XAC('option', Sel ++ [?XMLATTR('value', O)], V) end, [{"month", ?T("Last month")}, {"year", ?T("Last year")}, {"all", ?T("All activity")}])), ?C(" "), ?INPUTT("submit", "ordinary", "Show Ordinary Table"), ?C(" "), ?INPUTT("submit", "integral", "Show Integral Table") ])] ++ Res, Host, Lang, AJID); process_admin(Host, #request{path = ["stats"], auth = {_, _Auth, AJID}, lang = Lang}) -> Res = get_stats(Host, Lang), make_xhtml([?XCT('h1', "Statistics")] ++ Res, Host, Lang, AJID); process_admin(global = Host, #request{path = ["misc"], auth = {_, _Auth, AJID}, method = Method, q = Query, lang = Lang}) -> Res = get_miscopts(Lang, Method, Query), make_xhtml(Res, Host, Lang, AJID); process_admin(global = Host, #request{path = ["misc", Type, SName], q = Query, auth = {_, _Auth, AJID}, lang = Lang}) -> Res = get_miscopt(Lang, Query, Type, SName), make_xhtml(Res, Host, Lang, AJID); process_admin(Host, #request{path = ["user", U], auth = {_, _Auth, AJID}, q = Query, lang = Lang}) -> case ejabberd_auth:is_user_exists(U, Host) of true -> Res = user_info(U, Host, Query, Lang), make_xhtml(Res, Host, Lang, AJID); false -> make_xhtml([?XCT('h1', "Not Found")], Host, Lang, AJID) end; process_admin(Host, #request{path = ["nodes"], auth = {_, _Auth, AJID}, lang = Lang}) -> Res = get_nodes(Lang), make_xhtml(Res, Host, Lang, AJID); process_admin(Host, #request{path = ["node", SNode | NPath], auth = {_, _Auth, AJID}, q = Query, lang = Lang}) -> case search_running_node(SNode) of false -> make_xhtml([?XCT('h1', "Node not found")], Host, Lang, AJID); Node -> Res = get_node(Host, Node, NPath, Query, Lang), make_xhtml(Res, Host, Node, Lang, AJID) end; %%%================================== %%%% process_admin default case process_admin(Host, #request{lang = Lang, auth = {_, _Auth, AJID} } = Request) -> {Hook, Opts} = case Host of global -> {webadmin_page_main, [Request]}; Host -> {webadmin_page_host, [Host, Request]} end, case ejabberd_hooks:run_fold(Hook, list_to_binary(Host), [], Opts) of [] -> setelement(1, make_xhtml([?XC('h1', "Not Found")], Host, Lang, AJID), 404); [{xmlel, _, _, _, _, _} | _] = Res -> make_xhtml(Res, Host, Lang, AJID); [X | _] = Res when not is_tuple(X) -> {200, [{"Content-Type", "image/png"}, last_modified(), cache_control_public()], Res} end. %%%================================== %%%% acl acls_to_xhtml(ACLs) -> ?XAE('table', [], [?XE('tbody', lists:map( fun({acl, Name, Spec} = ACL) -> SName = atom_to_list(Name), ID = term_to_id(ACL), ?XE('tr', [?XE('td', [?INPUT("checkbox", "selected", ID)]), ?XC('td', SName)] ++ acl_spec_to_xhtml(ID, Spec) ) end, ACLs) ++ [?XE('tr', [?X('td'), ?XE('td', [?INPUT("text", "namenew", "")]) ] ++ acl_spec_to_xhtml("new", {user, ""}) )] )]). acl_spec_to_text({user, U}) -> {user, U}; acl_spec_to_text({server, S}) -> {server, S}; acl_spec_to_text({user, U, S}) -> {user, U ++ "@" ++ S}; acl_spec_to_text({user_regexp, RU}) -> {user_regexp, RU}; acl_spec_to_text({user_regexp, RU, S}) -> {user_regexp, RU ++ "@" ++ S}; acl_spec_to_text({server_regexp, RS}) -> {server_regexp, RS}; acl_spec_to_text({node_regexp, RU, RS}) -> {node_regexp, RU ++ "@" ++ RS}; acl_spec_to_text({user_glob, RU}) -> {user_glob, RU}; acl_spec_to_text({user_glob, RU, S}) -> {user_glob, RU ++ "@" ++ S}; acl_spec_to_text({server_glob, RS}) -> {server_glob, RS}; acl_spec_to_text({node_glob, RU, RS}) -> {node_glob, RU ++ "@" ++ RS}; acl_spec_to_text(all) -> {all, ""}; acl_spec_to_text(Spec) -> {raw, term_to_string(Spec)}. acl_spec_to_xhtml(ID, Spec) -> {Type, Str} = acl_spec_to_text(Spec), [acl_spec_select(ID, Type), ?ACLINPUT(Str)]. acl_spec_select(ID, Opt) -> ?XE('td', [?XAE('select', [?XMLATTR('name', "type" ++ ID)], lists:map( fun(O) -> Sel = if O == Opt -> [?XMLATTR('selected', <<"selected">>)]; true -> [] end, ?XAC('option', Sel ++ [?XMLATTR('value', O)], atom_to_list(O)) end, [user, server, user_regexp, server_regexp, node_regexp, user_glob, server_glob, node_glob, all, raw]))]). %% @spec (T::any()) -> StringLine::string() term_to_string(T) -> StringParagraph = lists:flatten(io_lib:format("~1000000p", [T])), %% Remove from the string all the carriage returns characters StringLine = re:replace(StringParagraph, "\\n ", "", [global,{return,list}]), StringLine. %% @spec (T::any(), Cols::integer()) -> {NumLines::integer(), Paragraph::string()} term_to_paragraph(T, Cols) -> Paragraph = erl_prettypr:format(erl_syntax:abstract(T), [{paper, Cols}]), FieldList = re:split(Paragraph, "\n", [{return, list}]), NumLines = length(FieldList), {NumLines, Paragraph}. term_to_id(T) -> jlib:encode_base64(binary_to_list(term_to_binary(T))). id_to_term(I) -> binary_to_term(list_to_binary(jlib:decode_base64(I))). acl_parse_query(Host, Query) -> ACLs = ets:select(acl, [{{acl, {'$1', Host}, '$2'}, [], [{{acl, '$1', '$2'}}]}]), case lists:keysearch("submit", 1, Query) of {value, _} -> acl_parse_submit(ACLs, Query); _ -> case lists:keysearch("delete", 1, Query) of {value, _} -> acl_parse_delete(ACLs, Query) end end. acl_parse_submit(ACLs, Query) -> NewACLs = lists:map( fun({acl, Name, Spec} = ACL) -> %%SName = atom_to_list(Name), ID = term_to_id(ACL), case {lists:keysearch("type" ++ ID, 1, Query), lists:keysearch("value" ++ ID, 1, Query)} of {{value, {_, T}}, {value, {_, V}}} -> {Type, Str} = acl_spec_to_text(Spec), case {atom_to_list(Type), Str} of {T, V} -> ACL; _ -> NewSpec = string_to_spec(T, V), {acl, Name, NewSpec} end; _ -> ACL end end, ACLs), NewACL = case {lists:keysearch("namenew", 1, Query), lists:keysearch("typenew", 1, Query), lists:keysearch("valuenew", 1, Query)} of {{value, {_, ""}}, _, _} -> []; {{value, {_, N}}, {value, {_, T}}, {value, {_, V}}} -> NewName = list_to_atom(N), NewSpec = string_to_spec(T, V), [{acl, NewName, NewSpec}]; _ -> [] end, NewACLs ++ NewACL. string_to_spec("user", Val) -> string_to_spec2(user, Val); string_to_spec("server", Val) -> {server, Val}; string_to_spec("user_regexp", Val) -> string_to_spec2(user_regexp, Val); string_to_spec("server_regexp", Val) -> {server_regexp, Val}; string_to_spec("node_regexp", Val) -> JID = exmpp_jid:parse(Val), U = exmpp_jid:prep_node_as_list(JID), S = exmpp_jid:prep_domain_as_list(JID), undefined = exmpp_jid:resource(JID), {node_regexp, U, S}; string_to_spec("user_glob", Val) -> string_to_spec2(user_glob, Val); string_to_spec("server_glob", Val) -> {server_glob, Val}; string_to_spec("node_glob", Val) -> JID = exmpp_jid:parse(Val), U = exmpp_jid:prep_node_as_list(JID), S = exmpp_jid:prep_domain_as_list(JID), undefined = exmpp_jid:resource(JID), {node_glob, U, S}; string_to_spec("all", _) -> all; string_to_spec("raw", Val) -> {ok, Tokens, _} = erl_scan:string(Val ++ "."), {ok, NewSpec} = erl_parse:parse_term(Tokens), NewSpec. string_to_spec2(ACLName, Val) -> JID = exmpp_jid:parse(Val), U = exmpp_jid:prep_node_as_list(JID), S = exmpp_jid:prep_domain_as_list(JID), undefined = exmpp_jid:resource(JID), case U of undefined -> {ACLName, S}; _ -> {ACLName, U, S} end. acl_parse_delete(ACLs, Query) -> NewACLs = lists:filter( fun({acl, _Name, _Spec} = ACL) -> ID = term_to_id(ACL), not lists:member({"selected", ID}, Query) end, ACLs), NewACLs. access_rules_to_xhtml(AccessRules, Lang) -> ?XAE('table', [], [?XE('tbody', lists:map( fun({access, Name, Rules} = Access) -> SName = atom_to_list(Name), ID = term_to_id(Access), ?XE('tr', [?XE('td', [?INPUT("checkbox", "selected", ID)]), ?XE('td', [?AC(SName ++ "/", SName)]), ?XC('td', term_to_string(Rules)) ] ) end, lists:sort(AccessRules)) ++ [?XE('tr', [?X('td'), ?XE('td', [?INPUT("text", "namenew", "")]), ?XE('td', [?INPUTT("submit", "addnew", "Add New")]) ] )] )]). access_parse_query(Host, Query) -> AccessRules = ets:select(config, [{{config, {access, '$1', Host}, '$2'}, [], [{{access, '$1', '$2'}}]}]), case lists:keysearch("addnew", 1, Query) of {value, _} -> access_parse_addnew(AccessRules, Host, Query); _ -> case lists:keysearch("delete", 1, Query) of {value, _} -> access_parse_delete(AccessRules, Host, Query) end end. access_parse_addnew(_AccessRules, Host, Query) -> case lists:keysearch("namenew", 1, Query) of {value, {_, String}} when String /= "" -> Name = list_to_atom(String), ejabberd_config:add_global_option({access, Name, Host}, []), ok end. access_parse_delete(AccessRules, Host, Query) -> lists:foreach( fun({access, Name, _Rules} = AccessRule) -> ID = term_to_id(AccessRule), case lists:member({"selected", ID}, Query) of true -> mnesia:transaction( fun() -> mnesia:delete({config, {access, Name, Host}}) end); _ -> ok end end, AccessRules), ok. access_rule_to_xhtml(Rules) -> Text = lists:flatmap( fun({Access, ACL} = _Rule) -> SAccess = element_to_list(Access), SACL = atom_to_list(ACL), SAccess ++ "\s\t" ++ SACL ++ "\n" end, Rules), ?XAC('textarea', [?XMLATTR('name', <<"rules">>), ?XMLATTR('rows', <<"16">>), ?XMLATTR('cols', <<"80">>)], Text). parse_access_rule(Text) -> Strings = string:tokens(Text, "\r\n"), case catch lists:flatmap( fun(String) -> case string:tokens(String, "\s\t") of [Access, ACL] -> [{list_to_element(Access), list_to_atom(ACL)}]; [] -> [] end end, Strings) of {'EXIT', _Reason} -> error; Rs -> {ok, Rs} end. %%%================================== %%%% list_vhosts list_vhosts(Lang, JID) -> Hosts = ?MYHOSTS, HostsAllowed = lists:filter( fun(Host) -> is_acl_match(Host, [configure, webadmin_view], JID) end, Hosts ), list_vhosts2(Lang, HostsAllowed). list_vhosts2(Lang, Hosts) -> SHosts = lists:sort(Hosts), [?XE('table', [?XE('thead', [?XE('tr', [?XCT('td', "Host"), ?XCT('td', "Registered Users"), ?XCT('td', "Online Users") ])]), ?XE('tbody', lists:map( fun(Host) -> OnlineUsers = length(ejabberd_sm:get_vh_session_list(list_to_binary(Host))), RegisteredUsers = ejabberd_auth:get_vh_registered_users_number(Host), ?XE('tr', [?XE('td', [?AC("../server/" ++ Host ++ "/", Host)]), ?XC('td', pretty_string_int(RegisteredUsers)), ?XC('td', pretty_string_int(OnlineUsers)) ]) end, SHosts) )])]. %%%================================== %%%% list_users list_users(Host, Query, Lang, URLFunc) -> Res = list_users_parse_query(Query, Host), Users = ejabberd_auth:get_vh_registered_users(Host), SUsers = lists:sort([{S, U} || {U, S} <- Users]), FUsers = case length(SUsers) of N when N =< 100 -> [list_given_users(Host, SUsers, "../", Lang, URLFunc)]; N -> NParts = trunc(math:sqrt(N * 0.618)) + 1, M = trunc(N / NParts) + 1, lists:flatmap( fun(K) -> L = K + M - 1, %%Node = integer_to_list(K) ++ "-" ++ integer_to_list(L), Last = if L < N -> su_to_list(lists:nth(L, SUsers)); true -> su_to_list(lists:last(SUsers)) end, Name = su_to_list(lists:nth(K, SUsers)) ++ [$\s, 226, 128, 148, $\s] ++ Last, [?AC(URLFunc({user_diapason, K, L}), Name), ?BR] end, lists:seq(1, N, M)) end, case Res of ok -> [?XREST("Submitted")]; error -> [?XREST("Bad format")]; nothing -> [] end ++ [?XAE('form', [?XMLATTR('action', <<>>), ?XMLATTR('method', <<"post">>)], [?XE('table', [?XE('tr', [?XC('td', ?T("User") ++ ":"), ?XE('td', [?INPUT("text", "newusername", "")]), ?XE('td', [?C([" @ ", Host])]) ]), ?XE('tr', [?XC('td', ?T("Password") ++ ":"), ?XE('td', [?INPUT("password", "newuserpassword", "")]), ?X('td') ]), ?XE('tr', [?X('td'), ?XAE('td', [?XMLATTR('class', <<"alignright">>)], [?INPUTT("submit", "addnewuser", "Add User")]), ?X('td') ])]), ?P] ++ FUsers)]. %% Parse user creation query and try register: list_users_parse_query(Query, Host) -> case lists:keysearch("addnewuser", 1, Query) of {value, _} -> {value, {_, Username}} = lists:keysearch("newusername", 1, Query), {value, {_, Password}} = lists:keysearch("newuserpassword", 1, Query), try JID = exmpp_jid:parse(Username++"@"++Host), User = exmpp_jid:node_as_list(JID), Server = exmpp_jid:domain_as_list(JID), case ejabberd_auth:try_register(User, Server, Password) of {error, _Reason} -> error; _ -> ok end catch _ -> error end; false -> nothing end. list_users_in_diapason(Host, Diap, Lang, URLFunc) -> Users = ejabberd_auth:get_vh_registered_users(Host), SUsers = lists:sort([{S, U} || {U, S} <- Users]), [S1, S2] = re:split(Diap, "-", [{return, list}]), N1 = list_to_integer(S1), N2 = list_to_integer(S2), Sub = lists:sublist(SUsers, N1, N2 - N1 + 1), [list_given_users(Host, Sub, "../../", Lang, URLFunc)]. list_given_users(Host, Users, Prefix, Lang, URLFunc) -> ModLast = get_lastactivity_module(Host), ModOffline = get_offlinemsg_module(Host), ?XE('table', [?XE('thead', [?XE('tr', [?XCT('td', "User"), ?XCT('td', "Offline Messages"), ?XCT('td', "Last Activity")])]), ?XE('tbody', lists:map( fun(_SU = {Server, User}) -> ServerB = list_to_binary(Server), UserB = list_to_binary(User), US = {UserB, ServerB}, QueueLenStr = get_offlinemsg_length(ModOffline, UserB, ServerB), FQueueLen = [?AC(URLFunc({users_queue, Prefix, User, Server}), QueueLenStr)], FLast = case ejabberd_sm:get_user_resources(UserB, ServerB) of [] -> case ModLast:get_last_info(User, Server) of not_found -> ?T("Never"); {ok, Shift, _Status} -> TimeStamp = {Shift div 1000000, Shift rem 1000000, 0}, {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(TimeStamp), lists:flatten( io_lib:format( "~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", [Year, Month, Day, Hour, Minute, Second])) end; _ -> ?T("Online") end, ?XE('tr', [?XE('td', [?AC(URLFunc({user, Prefix, ejabberd_http:url_encode(User), Server}), us_to_list(US))]), ?XE('td', FQueueLen), ?XC('td', FLast)]) end, Users) )]). get_offlinemsg_length(ModOffline, User, Server) -> case ModOffline of none -> "disabled"; _ -> pretty_string_int(ModOffline:get_queue_length(User, Server)) end. get_offlinemsg_module(Server) -> case [mod_offline, mod_offline_odbc] -- gen_mod:loaded_modules(Server) of [mod_offline, mod_offline_odbc] -> none; [mod_offline_odbc] -> mod_offline; [mod_offline] -> mod_offline_odbc end. get_lastactivity_module(Server) -> case lists:member(mod_last, gen_mod:loaded_modules(Server)) of true -> mod_last; _ -> mod_last_odbc end. get_lastactivity_menuitem_list(Server) -> case get_lastactivity_module(Server) of mod_last -> [{"last-activity", "Last Activity"}]; mod_last_odbc -> [] end. us_to_list({User, Server}) -> exmpp_jid:to_list(User, Server, undefined). su_to_list({Server, User}) -> exmpp_jid:to_list(User, Server, undefined). %%%================================== %%%% get_stats get_stats(global, Lang) -> OnlineUsers = mnesia:table_info(session, size), RegisteredUsers = mnesia:table_info(passwd, size), S2SConns = ejabberd_s2s:dirty_get_connections(), S2SConnections = length(S2SConns), S2SServers = length(lists:usort([element(2, C) || C <- S2SConns])), [?XAE('table', [], [?XE('tbody', [?XE('tr', [?XCT('td', "Registered Users:"), ?XC('td', pretty_string_int(RegisteredUsers))]), ?XE('tr', [?XCT('td', "Online Users:"), ?XC('td', pretty_string_int(OnlineUsers))]), ?XE('tr', [?XCT('td', "Outgoing s2s Connections:"), ?XC('td', pretty_string_int(S2SConnections))]), ?XE('tr', [?XCT('td', "Outgoing s2s Servers:"), ?XC('td', pretty_string_int(S2SServers))]) ]) ])]; get_stats(Host, Lang) -> OnlineUsers = length(ejabberd_sm:get_vh_session_list(list_to_binary(Host))), RegisteredUsers = ejabberd_auth:get_vh_registered_users_number(Host), [?XAE('table', [], [?XE('tbody', [?XE('tr', [?XCT('td', "Registered Users:"), ?XC('td', pretty_string_int(RegisteredUsers))]), ?XE('tr', [?XCT('td', "Online Users:"), ?XC('td', pretty_string_int(OnlineUsers))]) ]) ])]. list_online_users(Host, _Lang) -> Users = [{S, U} || {U, S, _R} <- ejabberd_sm:get_vh_session_list(list_to_binary(Host))], SUsers = lists:usort(Users), lists:flatmap( fun({_S, U} = SU) -> [?AC("../user/" ++ ejabberd_http:url_encode(binary_to_list(U)) ++ "/", su_to_list(SU)), ?BR] end, SUsers). user_info(User, Server, Query, Lang) -> UserB = list_to_binary(User), ServerB = list_to_binary(Server), LServer = exmpp_stringprep:nameprep(Server), US = {exmpp_stringprep:nodeprep(User), LServer}, Res = user_parse_query(User, Server, Query), Resources = ejabberd_sm:get_user_resources(UserB, ServerB), FResources = case Resources of [] -> [?CT("None")]; _ -> [?XE('ul', lists:map(fun(R) -> FIP = case ejabberd_sm:get_user_info( UserB, ServerB, R) of offline -> ""; Info when is_list(Info) -> Node = proplists:get_value(node, Info), Conn = proplists:get_value(conn, Info), {IP, Port} = proplists:get_value(ip, Info), ConnS = case Conn of c2s -> "plain"; c2s_tls -> "tls"; c2s_compressed -> "zlib"; c2s_compressed_tls -> "tls+zlib"; http_bind -> "http-bind"; http_poll -> "http-poll" end, " (" ++ ConnS ++ "://" ++ inet_parse:ntoa(IP) ++ ":" ++ integer_to_list(Port) ++ "#" ++ atom_to_list(Node) ++ ")" end, ?LI([?C(binary_to_list(R) ++ FIP)]) end, lists:sort(Resources)))] end, Password = ejabberd_auth:get_password_s(User, Server), FPassword = [?INPUT("password", "password", Password), ?C(" "), ?INPUTT("submit", "chpassword", "Change Password")], UserItems = ejabberd_hooks:run_fold(webadmin_user, list_to_binary(LServer), [], [User, Server, Lang]), %% Code copied from list_given_users/5: ModLast = get_lastactivity_module(Server), LastActivity = case ejabberd_sm:get_user_resources(UserB, ServerB) of [] -> case ModLast:get_last_info(UserB, ServerB) of not_found -> ?T("Never"); {ok, Shift, _Status} -> TimeStamp = {Shift div 1000000, Shift rem 1000000, 0}, {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(TimeStamp), lists:flatten( io_lib:format( "~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", [Year, Month, Day, Hour, Minute, Second])) end; _ -> ?T("Online") end, [?XC('h1', ?T("User ") ++ us_to_list(US))] ++ case Res of ok -> [?XREST("Submitted")]; error -> [?XREST("Bad format")]; nothing -> [] end ++ [?XAE('form', [?XMLATTR('action', <<>>), ?XMLATTR('method', <<"post">>)], [?XCT('h3', "Connected Resources:")] ++ FResources ++ [?XCT('h3', "Password:")] ++ FPassword ++ [?XCT('h3', "Last Activity")] ++ [?C(LastActivity)] ++ UserItems ++ [?P, ?INPUTT("submit", "removeuser", "Remove User")])]. user_parse_query(User, Server, Query) -> lists:foldl(fun({Action, _Value}, Acc) when Acc == nothing -> user_parse_query1(Action, User, Server, Query); ({_Action, _Value}, Acc) -> Acc end, nothing, Query). user_parse_query1("password", _User, _Server, _Query) -> nothing; user_parse_query1("chpassword", User, Server, Query) -> case lists:keysearch("password", 1, Query) of {value, {_, undefined}} -> error; {value, {_, Password}} -> ejabberd_auth:set_password(User, Server, Password), ok; _ -> error end; user_parse_query1("removeuser", User, Server, _Query) -> ejabberd_auth:remove_user(User, Server), ok; user_parse_query1(Action, User, Server, Query) -> case ejabberd_hooks:run_fold(webadmin_user_parse_query, list_to_binary(Server), [], [Action, User, Server, Query]) of [] -> nothing; Res -> Res end. list_last_activity(Host, Lang, Integral, Period) -> {MegaSecs, Secs, _MicroSecs} = now(), TimeStamp = MegaSecs * 1000000 + Secs, case Period of "all" -> TS = 0, Days = infinity; "year" -> TS = TimeStamp - 366 * 86400, Days = 366; _ -> TS = TimeStamp - 31 * 86400, Days = 31 end, case catch mnesia:dirty_select( last_activity, [{{last_activity, {'_', Host}, '$1', '_'}, [{'>', '$1', TS}], [{'trunc', {'/', {'-', TimeStamp, '$1'}, 86400}}]}]) of {'EXIT', _Reason} -> []; Vals -> Hist = histogram(Vals, Integral), if Hist == [] -> [?CT("No Data")]; true -> Left = if Days == infinity -> 0; true -> Days - length(Hist) end, Tail = if Integral -> lists:duplicate(Left, lists:last(Hist)); true -> lists:duplicate(Left, 0) end, Max = lists:max(Hist), [?XAE('ol', [?XMLATTR('id', <<"lastactivity">>), ?XMLATTR('start', <<"0">>)], [?XAE('li', [?XMLATTR('style', "width:" ++ integer_to_list( trunc(90 * V / Max)) ++ "%;")], [#xmlcdata{cdata = list_to_binary(pretty_string_int(V))}]) || V <- Hist ++ Tail])] end end. histogram(Values, Integral) -> histogram(lists:sort(Values), Integral, 0, 0, []). histogram([H | T], Integral, Current, Count, Hist) when Current == H -> histogram(T, Integral, Current, Count + 1, Hist); histogram([H | _] = Values, Integral, Current, Count, Hist) when Current < H -> if Integral -> histogram(Values, Integral, Current + 1, Count, [Count | Hist]); true -> histogram(Values, Integral, Current + 1, 0, [Count | Hist]) end; histogram([], _Integral, _Current, Count, Hist) -> if Count > 0 -> lists:reverse([Count | Hist]); true -> lists:reverse(Hist) end. %%%================================== %%%% get_miscopts get_miscopts(Lang, Method, Query) -> ?DEBUG("query: ~p", [Query]), Res = case Method of 'POST' -> case catch miscopts_parse_query(Query) of {'EXIT', _} -> error; ok -> ok end; _ -> nothing end, GlobalOptions = get_miscopts_list(config), LocalOptions = get_miscopts_list(local_config), ?H1GL(?T("Miscelanea Options"), "toc", "Configuring ejabberd") ++ case Res of ok -> [?XREST("Submitted")]; error -> [?XREST("Bad format")]; nothing -> [] end ++ [ ?XCT("h2", "Global"), ?XAE('form', [?XMLATTR('action', <<>>), ?XMLATTR('method', <<"post">>)], [options_to_xhtml("global", GlobalOptions, Lang), ?BR, ?INPUTT("submit", "deleteglobal", "Delete Selected") ]), ?XCT("h2", "Local"), ?XAE('form', [?XMLATTR('action', <<>>), ?XMLATTR('method', <<"post">>)], [options_to_xhtml("local", LocalOptions, Lang), ?BR, ?INPUTT("submit", "deletelocal", "Delete Selected") ]) ]. filter_is_miscopts(AllOpts) -> lists:filter(fun({Table, Key, _Value}) -> is_miscopts(Table, Key) end, AllOpts). is_miscopts(config, {access, _, _}) -> false; is_miscopts(local_config, listen) -> false; is_miscopts(local_config, node_start) -> false; is_miscopts(local_config, {modules, _}) -> false; is_miscopts(_, _) -> true. is_needrestart(hosts) -> true; is_needrestart(_Key) -> false. get_miscopts_list(Table) -> AllOpts = ets:tab2list(Table), Filtered = filter_is_miscopts(AllOpts), GetPrefix = fun(Tuple) when is_tuple(Tuple) -> element(1, Tuple); (Other) -> Other end, Prefixed = [{GetPrefix(Key), Op} || {_, Key, _} = Op <- Filtered], PrefixedSorted = lists:keysort(1, Prefixed), [Opt || {_Prefix, Opt} <- PrefixedSorted]. miscopts_parse_query(Query) -> case {lists:keysearch("addnewglobal", 1, Query), lists:keysearch("addnewlocal", 1, Query)} of {{value, _}, _} -> miscopts_parse_addnew(global, Query); {_, {value, _}} -> miscopts_parse_addnew(local, Query); _ -> case {lists:keysearch("deleteglobal", 1, Query), lists:keysearch("deletelocal", 1, Query)} of {{value, _}, _} -> miscopts_parse_delete(global, Query); {_, {value, _}} -> miscopts_parse_delete(local, Query) end end. miscopts_parse_addnew(Type, Query) -> case lists:keysearch("namenew", 1, Query) of {value, {_, String}} when String /= "" -> Key = string_to_term(String++"."), miscopt_operation(add, Type, {Key, []}), ok end. string_to_term(String) -> {ok, Tokens, _} = erl_scan:string(String), {ok, Term} = erl_parse:parse_term(Tokens), Term. miscopt_operation(get, global, Key) -> ejabberd_config:get_global_option(Key); miscopt_operation(get, local, Key) -> ejabberd_config:get_local_option(Key); miscopt_operation(add, global, {Key, Value}) -> ejabberd_config:add_global_option(Key, Value); miscopt_operation(add, local, {Key, Value}) -> ejabberd_config:add_local_option(Key, Value); miscopt_operation(del, global, Key) -> ejabberd_config:del_global_option(Key); miscopt_operation(del, local, Key) -> ejabberd_config:del_local_option(Key). miscopts_parse_delete(Type, Query) -> lists:foreach( fun({"selected", ID}) -> Term = id_to_term(ID), miscopt_operation(del, Type, Term); (_ )-> ok end, Query), ok. options_to_xhtml(SType, AccessRules, Lang) -> ?XAE("table", [], [?XE("tbody", lists:map( fun({_, Key, Value}) -> SKey = term_to_string(Key), SValue = term_to_string(Value), ID = term_to_id(Key), ?XE("tr", [?XE("td", [?INPUT("checkbox", "selected", ID)]), ?XE("td", [?AC(SType ++"/"++ SKey ++ "/", SKey)]), ?XC("td", SValue) ] ) end, AccessRules) ++ [?XE("tr", [?X("td"), ?XE("td", [?INPUT("text", "namenew", "")]), ?XE("td", [?INPUTT("submit","addnew"++SType,"Add New")]) ] )] )]). get_miscopt(Lang, Query, SType, OldSKey) -> ?DEBUG("query: ~p", [Query]), Type = list_to_atom(SType), UpSType = get_miscopt_type_upstring(SType), OldKey = string_to_term(OldSKey++"."), OldValue = miscopt_operation(get, Type, OldKey), {Res, Key, Value} = case lists:keysearch("miscopt", 1, Query) of {value, {_, String}} -> case string_to_term(String) of {NewKey, NewValue} -> miscopt_operation(del, Type, OldKey), miscopt_operation(add, Type, {NewKey, NewValue}), {ok, NewKey, NewValue}; _ -> {error, OldKey, OldValue} end; _ -> {nothing, OldKey, OldValue} end, Opt = {Key, Value}, {NumLines, SOpt} = term_to_paragraph(Opt, 40), ?H1GL(?T("Miscelanea Options"), "toc", "Configuring ejabberd") ++ case Res of ok -> [?XREST("Submitted")]; error -> [?XREST("Bad format")]; nothing -> [] end ++ [?XCT("h2", UpSType), ?XAE('form', [?XMLATTR('action', <<>>), ?XMLATTR('method', <<"post">>)], [?TEXTAREA("miscopt", integer_to_list(lists:max([16, NumLines])), "80", SOpt++"."), ?BR, maybe_needrestart(OldKey, Lang), ?INPUTT("submit", "submit", "Submit") ]) ]. get_miscopt_type_upstring("global") -> "Global"; get_miscopt_type_upstring("local") -> "Local". maybe_needrestart(Key, Lang) -> case is_needrestart(Key) of true -> ?XCT("p", "Please note: any change in this option only takes effect after ejabberd restart."); false -> ?BR end. %%%================================== %%%% get_nodes get_nodes(Lang) -> RunningNodes = mnesia:system_info(running_db_nodes), StoppedNodes = lists:usort(mnesia:system_info(db_nodes) ++ mnesia:system_info(extra_db_nodes)) -- RunningNodes, FRN = if RunningNodes == [] -> ?CT("None"); true -> ?XE('ul', lists:map( fun(N) -> S = atom_to_list(N), ?LI([?AC("../node/" ++ S ++ "/", S)]) end, lists:sort(RunningNodes))) end, FSN = if StoppedNodes == [] -> ?CT("None"); true -> ?XE('ul', lists:map( fun(N) -> S = atom_to_list(N), ?LI([?C(S)]) end, lists:sort(StoppedNodes))) end, [?XCT('h1', "Nodes"), ?XCT('h3', "Running Nodes"), FRN, ?XCT('h3', "Stopped Nodes"), FSN]. search_running_node(SNode) -> search_running_node(SNode, mnesia:system_info(running_db_nodes)). search_running_node(_, []) -> false; search_running_node(SNode, [Node | Nodes]) -> case atom_to_list(Node) of SNode -> Node; _ -> search_running_node(SNode, Nodes) end. get_node(global, Node, [], Query, Lang) -> Res = node_parse_query(Node, Query), Base = get_base_path(global, Node), MenuItems2 = make_menu_items(global, Node, Base, Lang), [?XC('h1', ?T("Node ") ++ atom_to_list(Node))] ++ case Res of ok -> [?XREST("Submitted")]; error -> [?XREST("Bad format")]; nothing -> [] end ++ [?XE('ul', [?LI([?ACT(Base ++ "db/", "Database")]), ?LI([?ACT(Base ++ "backup/", "Backup")]), ?LI([?ACT(Base ++ "ports/", "Listened Ports")]), ?LI([?ACT(Base ++ "stats/", "Statistics")]), ?LI([?ACT(Base ++ "update/", "Update")]) ] ++ MenuItems2), ?XAE('form', [?XMLATTR('action', <<>>), ?XMLATTR('method', <<"post">>)], [?INPUTT('submit', "restart", "Restart"), ?C(" "), ?INPUTT('submit', "stop", "Stop")]) ]; get_node(Host, Node, [], _Query, Lang) -> Base = get_base_path(Host, Node), MenuItems2 = make_menu_items(Host, Node, Base, Lang), [?XC('h1', ?T("Node ") ++ atom_to_list(Node)), ?XE('ul', [?LI([?ACT(Base ++ "modules/", "Modules")])] ++ MenuItems2) ]; get_node(global, Node, ["db"], Query, Lang) -> case rpc:call(Node, mnesia, system_info, [tables]) of {badrpc, _Reason} -> [?XCT('h1', "RPC Call Error")]; Tables -> ResS = case node_db_parse_query(Node, Tables, Query) of nothing -> []; ok -> [?XREST("Submitted")] end, STables = lists:sort(Tables), Rows = lists:map( fun(Table) -> STable = atom_to_list(Table), TInfo = case rpc:call(Node, mnesia, table_info, [Table, all]) of {badrpc, _} -> []; I -> I end, {Type, Size, Memory} = case {lists:keysearch(storage_type, 1, TInfo), lists:keysearch(size, 1, TInfo), lists:keysearch(memory, 1, TInfo)} of {{value, {storage_type, T}}, {value, {size, S}}, {value, {memory, M}}} -> {T, S, M}; _ -> {unknown, 0, 0} end, ?XE('tr', [?XC('td', STable), ?XE('td', [db_storage_select( STable, Type, Lang)]), ?XAC('td', [?XMLATTR('class', <<"alignright">>)], pretty_string_int(Size)), ?XAC('td', [?XMLATTR('class', <<"alignright">>)], pretty_string_int(Memory)) ]) end, STables), [?XC('h1', ?T("Database Tables at ") ++ atom_to_list(Node))] ++ ResS ++ [?XAE('form', [?XMLATTR('action', <<>>), ?XMLATTR('method', <<"post">>)], [?XAE('table', [], [?XE('thead', [?XE('tr', [?XCT('td', "Name"), ?XCT('td', "Storage Type"), ?XCT('td', "Elements"), %% Elements/items/records inserted in the table ?XCT('td', "Memory") %% Words or Bytes allocated to the table on this node ])]), ?XE('tbody', Rows ++ [?XE('tr', [?XAE('td', [?XMLATTR('colspan', <<"4">>), ?XMLATTR('class', <<"alignright">>)], [?INPUTT("submit", "submit", "Submit")]) ])] )])])] end; get_node(global, Node, ["backup"], Query, Lang) -> HomeDirRaw = case {os:getenv("HOME"), os:type()} of {EnvHome, _} when is_list(EnvHome) -> EnvHome; {false, win32} -> "C:/"; {false, {win32, _Osname}} -> "C:/"; {false, _} -> "/tmp/" end, HomeDir = filename:nativename(HomeDirRaw), ResS = case node_backup_parse_query(Node, Query) of nothing -> []; ok -> [?XREST("Submitted")]; {error, Error} -> [?XRES(?T("Error") ++": " ++ io_lib:format("~p", [Error]))] end, [?XC('h1', ?T("Backup of ") ++ atom_to_list(Node))] ++ ResS ++ [?XCT('p', "Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately."), ?XAE('form', [?XMLATTR('action', <<>>), ?XMLATTR('method', <<"post">>)], [?XAE('table', [], [?XE('tbody', [?XE('tr', [?XCT('td', "Store binary backup:"), ?XE('td', [?INPUT("text", "storepath", filename:join(HomeDir, "ejabberd.backup"))]), ?XE('td', [?INPUTT("submit", "store", "OK")]) ]), ?XE('tr', [?XCT('td', "Restore binary backup immediately:"), ?XE('td', [?INPUT("text", "restorepath", filename:join(HomeDir, "ejabberd.backup"))]), ?XE('td', [?INPUTT("submit", "restore", "OK")]) ]), ?XE('tr', [?XCT('td', "Restore binary backup after next ejabberd restart (requires less memory):"), ?XE('td', [?INPUT("text", "fallbackpath", filename:join(HomeDir, "ejabberd.backup"))]), ?XE('td', [?INPUTT("submit", "fallback", "OK")]) ]), ?XE('tr', [?XCT('td', "Store plain text backup:"), ?XE('td', [?INPUT("text", "dumppath", filename:join(HomeDir, "ejabberd.dump"))]), ?XE('td', [?INPUTT("submit", "dump", "OK")]) ]), ?XE('tr', [?XCT('td', "Restore plain text backup immediately:"), ?XE('td', [?INPUT("text", "loadpath", filename:join(HomeDir, "ejabberd.dump"))]), ?XE('td', [?INPUTT("submit", "load", "OK")]) ]), ?XE("tr", [?XCT("td", "Import users data from a PIEFXIS file (XEP-0227):"), ?XE("td", [?INPUT("text", "import_piefxis_filepath", filename:join(HomeDir, "users.xml"))]), ?XE("td", [?INPUTT("submit", "import_piefxis_file", "OK")]) ]), ?XE("tr", [?XCT("td", "Export data of all users in the server to PIEFXIS files (XEP-0227):"), ?XE("td", [?INPUT("text", "export_piefxis_dirpath", HomeDir)]), ?XE("td", [?INPUTT("submit", "export_piefxis_dir", "OK")]) ]), ?XE("tr", [?XE("td", [?CT("Export data of users in a host to PIEFXIS files (XEP-0227):"), ?C(" "), ?INPUT("text", "export_piefxis_host_dirhost", ?MYNAME)]), ?XE("td", [?INPUT("text", "export_piefxis_host_dirpath", HomeDir)]), ?XE("td", [?INPUTT("submit", "export_piefxis_host_dir", "OK")]) ]), ?XE("tr", [?XCT("td", "Import user data from jabberd14 spool file:"), ?XE("td", [?INPUT("text", "import_filepath", filename:join(HomeDir, "user1.xml"))]), ?XE("td", [?INPUTT("submit", "import_file", "OK")]) ]), ?XE("tr", [?XCT("td", "Import users data from jabberd14 spool directory:"), ?XE("td", [?INPUT("text", "import_dirpath", "/var/spool/jabber/")]), ?XE("td", [?INPUTT("submit", "import_dir", "OK")]) ]) ]) ])])]; get_node(global, Node, ["ports"], Query, Lang) -> Ports = rpc:call(Node, ejabberd_config, get_local_option, [listen]), Res = case catch node_ports_parse_query(Node, Ports, Query) of submitted -> ok; {'EXIT', _Reason} -> error; {is_added, ok} -> ok; {is_added, {error, Reason}} -> {error, io_lib:format("~p", [Reason])}; _ -> nothing end, %% TODO: This sorting does not work when [{{Port, IP}, Module, Opts}] NewPorts = lists:sort( rpc:call(Node, ejabberd_config, get_local_option, [listen])), H1String = ?T("Listened Ports at ") ++ atom_to_list(Node), ?H1GL(H1String, "listened", "Listening Ports") ++ case Res of ok -> [?XREST("Submitted")]; error -> [?XREST("Bad format")]; {error, ReasonT} -> [?XRES(?T("Error") ++ ": " ++ ReasonT)]; nothing -> [] end ++ [?XAE('form', [?XMLATTR('action', <<>>), ?XMLATTR('method', <<"post">>)], [node_ports_to_xhtml(NewPorts, Lang)]) ]; get_node(Host, Node, ["modules"], Query, Lang) when is_list(Host) -> Modules = rpc:call(Node, gen_mod, loaded_modules_with_opts, [Host]), Res = case catch node_modules_parse_query(Host, Node, Modules, Query) of submitted -> ok; {'EXIT', Reason} -> ?INFO_MSG("~p~n", [Reason]), error; _ -> nothing end, NewModules = lists:sort( rpc:call(Node, gen_mod, loaded_modules_with_opts, [Host])), H1String = ?T("Modules at ") ++ atom_to_list(Node), ?H1GL(H1String, "modoverview", "Modules Overview") ++ case Res of ok -> [?XREST("Submitted")]; error -> [?XREST("Bad format")]; nothing -> [] end ++ [?XAE('form', [?XMLATTR('action', <<>>), ?XMLATTR('method', <<"post">>)], [node_modules_to_xhtml(NewModules, Lang)]) ]; get_node(global, Node, ["stats"], _Query, Lang) -> UpTime = rpc:call(Node, erlang, statistics, [wall_clock]), UpTimeS = io_lib:format("~.3f", [element(1, UpTime)/1000]), CPUTime = rpc:call(Node, erlang, statistics, [runtime]), CPUTimeS = io_lib:format("~.3f", [element(1, CPUTime)/1000]), OnlineUsers = mnesia:table_info(session, size), TransactionsCommitted = rpc:call(Node, mnesia, system_info, [transaction_commits]), TransactionsAborted = rpc:call(Node, mnesia, system_info, [transaction_failures]), TransactionsRestarted = rpc:call(Node, mnesia, system_info, [transaction_restarts]), TransactionsLogged = rpc:call(Node, mnesia, system_info, [transaction_log_writes]), [?XC('h1', io_lib:format(?T("Statistics of ~p"), [Node])), ?XAE('table', [], [?XE('tbody', [?XE('tr', [?XCT('td', "Uptime:"), ?XAC('td', [?XMLATTR('class', <<"alignright">>)], UpTimeS)]), ?XE('tr', [?XCT('td', "CPU Time:"), ?XAC('td', [?XMLATTR('class', <<"alignright">>)], CPUTimeS)]), ?XE('tr', [?XCT('td', "Online Users:"), ?XAC('td', [?XMLATTR('class', <<"alignright">>)], pretty_string_int(OnlineUsers))]), ?XE('tr', [?XCT('td', "Transactions Committed:"), ?XAC('td', [?XMLATTR('class', <<"alignright">>)], pretty_string_int(TransactionsCommitted))]), ?XE('tr', [?XCT('td', "Transactions Aborted:"), ?XAC('td', [?XMLATTR('class', <<"alignright">>)], pretty_string_int(TransactionsAborted))]), ?XE('tr', [?XCT('td', "Transactions Restarted:"), ?XAC('td', [?XMLATTR('class', <<"alignright">>)], pretty_string_int(TransactionsRestarted))]), ?XE('tr', [?XCT('td', "Transactions Logged:"), ?XAC('td', [?XMLATTR('class', <<"alignright">>)], pretty_string_int(TransactionsLogged))]) ]) ])]; get_node(global, Node, ["update"], Query, Lang) -> rpc:call(Node, code, purge, [ejabberd_update]), Res = node_update_parse_query(Node, Query), rpc:call(Node, code, load_file, [ejabberd_update]), {ok, _Dir, UpdatedBeams, Script, LowLevelScript, Check} = rpc:call(Node, ejabberd_update, update_info, []), Mods = case UpdatedBeams of [] -> ?CT("None"); _ -> BeamsLis = lists:map( fun(Beam) -> BeamString = atom_to_list(Beam), ?LI([ ?INPUT("checkbox", "selected", BeamString), %% If we want checkboxes selected by default: %%?XA("input", [{"checked", ""}, %% {"type", "checkbox"}, %% {"name", "selected"}, %% {"value", BeamString}]), ?C(BeamString)]) end, UpdatedBeams), SelectButtons = [?BR, ?INPUTATTRS(<<"button">>, <<"selectall">>, <<"Select All">>, [?XMLATTR('onClick', <<"selectAll()">>)]), ?C(" "), ?INPUTATTRS(<<"button">>, <<"unselectall">>, <<"Unselect All">>, [?XMLATTR('onClick', <<"unSelectAll()">>)])], %%?XE("ul", BeamsLis) ?XAE('ul', [?XMLATTR('class', <<"noliststyle">>)], BeamsLis ++ SelectButtons) end, FmtScript = ?XC('pre', io_lib:format("~p", [Script])), FmtLowLevelScript = ?XC('pre', io_lib:format("~p", [LowLevelScript])), [?XC('h1', ?T("Update ") ++ atom_to_list(Node))] ++ case Res of ok -> [?XREST("Submitted")]; error -> [?XREST("Bad format")]; nothing -> [] end ++ [?XAE('form', [?XMLATTR('action', <<>>), ?XMLATTR('method', <<"post">>)], [ ?XCT('h2', "Update plan"), ?XCT('h3', "Modified modules"), Mods, ?XCT('h3', "Update script"), FmtScript, ?XCT('h3', "Low level update script"), FmtLowLevelScript, ?XCT('h3', "Script check"), ?XC("pre", atom_to_list(Check)), ?BR, ?INPUTT("submit", "update", "Update") ]) ]; get_node(Host, Node, NPath, Query, Lang) -> {Hook, Opts} = case Host of global -> {webadmin_page_node, [Node, NPath, Query, Lang]}; Host -> {webadmin_page_hostnode, [Host, Node, NPath, Query, Lang]} end, case ejabberd_hooks:run_fold(Hook, list_to_binary(Host), [], Opts) of [] -> [?XC('h1', "Not Found")]; Res -> Res end. %%%================================== %%%% node parse node_parse_query(Node, Query) -> case lists:keysearch("restart", 1, Query) of {value, _} -> case rpc:call(Node, init, restart, []) of {badrpc, _Reason} -> error; _ -> ok end; _ -> case lists:keysearch("stop", 1, Query) of {value, _} -> case rpc:call(Node, init, stop, []) of {badrpc, _Reason} -> error; _ -> ok end; _ -> nothing end end. db_storage_select(ID, Opt, Lang) -> ?XAE('select', [?XMLATTR('name', "table" ++ ID)], lists:map( fun({O, Desc}) -> Sel = if O == Opt -> [?XMLATTR('selected', <<"selected">>)]; true -> [] end, ?XACT('option', Sel ++ [?XMLATTR('value', O)], Desc) end, [{ram_copies, "RAM copy"}, {disc_copies, "RAM and disc copy"}, {disc_only_copies, "Disc only copy"}, {unknown, "Remote copy"}, {delete_content, "Delete content"}, {delete_table, "Delete table"}])). node_db_parse_query(_Node, _Tables, [{nokey,[]}]) -> nothing; node_db_parse_query(Node, Tables, Query) -> lists:foreach( fun(Table) -> STable = atom_to_list(Table), case lists:keysearch("table" ++ STable, 1, Query) of {value, {_, SType}} -> Type = case SType of "unknown" -> unknown; "ram_copies" -> ram_copies; "disc_copies" -> disc_copies; "disc_only_copies" -> disc_only_copies; "delete_content" -> delete_content; "delete_table" -> delete_table; _ -> false end, if Type == false -> ok; Type == delete_content -> mnesia:clear_table(Table); Type == delete_table -> mnesia:delete_table(Table); Type == unknown -> mnesia:del_table_copy(Table, Node); true -> case mnesia:add_table_copy(Table, Node, Type) of {aborted, _} -> mnesia:change_table_copy_type( Table, Node, Type); _ -> ok end end; _ -> ok end end, Tables), ok. node_backup_parse_query(_Node, [{nokey,[]}]) -> nothing; node_backup_parse_query(Node, Query) -> lists:foldl( fun(Action, nothing) -> case lists:keysearch(Action, 1, Query) of {value, _} -> case lists:keysearch(Action ++ "path", 1, Query) of {value, {_, Path}} -> Res = case Action of "store" -> rpc:call(Node, mnesia, backup, [Path]); "restore" -> rpc:call(Node, ejabberd_admin, restore, [Path]); "fallback" -> rpc:call(Node, mnesia, install_fallback, [Path]); "dump" -> rpc:call(Node, ejabberd_admin, dump_to_textfile, [Path]); "load" -> rpc:call(Node, mnesia, load_textfile, [Path]); "import_piefxis_file" -> rpc:call(Node, ejabberd_piefxis, import_file, [Path]); "export_piefxis_dir" -> rpc:call(Node, ejabberd_piefxis, export_server, [Path]); "export_piefxis_host_dir" -> {value, {_, Host}} = lists:keysearch(Action ++ "host", 1, Query), rpc:call(Node, ejabberd_piefxis, export_host, [Path, Host]); "import_file" -> rpc:call(Node, ejabberd_admin, import_file, [Path]); "import_dir" -> rpc:call(Node, ejabberd_admin, import_dir, [Path]) end, case Res of {error, Reason} -> {error, Reason}; {badrpc, Reason} -> {badrpc, Reason}; _ -> ok end; OtherError -> {error, OtherError} end; _ -> nothing end; (_Action, Res) -> Res end, nothing, ["store", "restore", "fallback", "dump", "load", "import_file", "import_dir", "import_piefxis_file", "export_piefxis_dir", "export_piefxis_host_dir"]). node_ports_to_xhtml(Ports, Lang) -> ?XAE('table', [?XMLATTR('class', <<"withtextareas">>)], [?XE('thead', [?XE('tr', [?XCT('td', "Port"), ?XCT('td', "IP"), ?XCT('td', "Protocol"), ?XCT('td', "Module"), ?XCT('td', "Options") ])]), ?XE('tbody', lists:map( fun({PortIP, Module, Opts} = _E) -> {_Port, SPort, _TIP, SIP, SSPort, NetProt, OptsClean} = get_port_data(PortIP, Opts), SModule = atom_to_list(Module), {NumLines, SOptsClean} = term_to_paragraph(OptsClean, 40), %%ID = term_to_id(E), ?XE('tr', [?XAE('td', [?XMLATTR('size', <<"6">>)], [?C(SPort)]), ?XAE('td', [?XMLATTR('size', <<"15">>)], [?C(SIP)]), ?XAE('td', [?XMLATTR('size', <<"4">>)], [?C(atom_to_list(NetProt))]), ?XE('td', [?INPUTS("text", "module" ++ SSPort, SModule, "15")]), ?XE('td', [?TEXTAREA("opts" ++ SSPort, integer_to_list(NumLines), "35", SOptsClean)]), ?XE('td', [?INPUTT("submit", "add" ++ SSPort, "Update")]), ?XE('td', [?INPUTT("submit", "delete" ++ SSPort, "Delete")]) ] ) end, Ports) ++ [?XE('tr', [?XE('td', [?INPUTS("text", "portnew", "", "6")]), ?XE('td', [?INPUTS("text", "ipnew", "0.0.0.0", "15")]), ?XE("td", [make_netprot_html("tcp")]), ?XE('td', [?INPUTS("text", "modulenew", "", "15")]), ?XE('td', [?TEXTAREA("optsnew", "2", "35", "[]")]), ?XAE('td', [?XMLATTR("colspan", "2")], [?INPUTT("submit", "addnew", "Add New")]) ] )] )]). make_netprot_html(NetProt) -> ?XAE('select', [?XMLATTR('name', "netprotnew")], lists:map( fun(O) -> Sel = if O == NetProt -> [?XMLATTR('selected', <<"selected">>)]; true -> [] end, ?XAC('option', Sel ++ [?XMLATTR('value', O)], O) end, ["tcp", "udp"])). get_port_data(PortIP, Opts) -> {Port, IPT, IPS, _IPV, NetProt, OptsClean} = ejabberd_listener:parse_listener_portip(PortIP, Opts), SPort = io_lib:format("~p", [Port]), SSPort = lists:flatten( lists:map( fun(N) -> io_lib:format("~.16b", [N]) end, binary_to_list(crypto:md5(SPort++IPS++atom_to_list(NetProt))))), {Port, SPort, IPT, IPS, SSPort, NetProt, OptsClean}. node_ports_parse_query(Node, Ports, Query) -> lists:foreach( fun({PortIpNetp, Module1, Opts1}) -> {Port, _SPort, TIP, _SIP, SSPort, NetProt, _OptsClean} = get_port_data(PortIpNetp, Opts1), case lists:keysearch("add" ++ SSPort, 1, Query) of {value, _} -> PortIpNetp2 = {Port, TIP, NetProt}, {{value, {_, SModule}}, {value, {_, SOpts}}} = {lists:keysearch("module" ++ SSPort, 1, Query), lists:keysearch("opts" ++ SSPort, 1, Query)}, Module = list_to_atom(SModule), {ok, Tokens, _} = erl_scan:string(SOpts ++ "."), {ok, Opts} = erl_parse:parse_term(Tokens), rpc:call(Node, ejabberd_listener, delete_listener, [PortIpNetp2, Module1]), R=rpc:call(Node, ejabberd_listener, add_listener, [PortIpNetp2, Module, Opts]), throw({is_added, R}); _ -> case lists:keysearch("delete" ++ SSPort, 1, Query) of {value, _} -> rpc:call(Node, ejabberd_listener, delete_listener, [PortIpNetp, Module1]), throw(submitted); _ -> ok end end end, Ports), case lists:keysearch("addnew", 1, Query) of {value, _} -> {{value, {_, SPort}}, {value, {_, STIP}}, %% It is a string that may represent a tuple {value, {_, SNetProt}}, {value, {_, SModule}}, {value, {_, SOpts}}} = {lists:keysearch("portnew", 1, Query), lists:keysearch("ipnew", 1, Query), lists:keysearch("netprotnew", 1, Query), lists:keysearch("modulenew", 1, Query), lists:keysearch("optsnew", 1, Query)}, {ok, Toks, _} = erl_scan:string(SPort ++ "."), {ok, Port2} = erl_parse:parse_term(Toks), {ok, ToksIP, _} = erl_scan:string(STIP ++ "."), STIP2 = case erl_parse:parse_term(ToksIP) of {ok, IPTParsed} -> IPTParsed; {error, _} -> STIP end, Module = list_to_atom(SModule), NetProt2 = list_to_atom(SNetProt), {ok, Tokens, _} = erl_scan:string(SOpts ++ "."), {ok, Opts} = erl_parse:parse_term(Tokens), {Port2, _SPort, IP2, _SIP, _SSPort, NetProt2, OptsClean} = get_port_data({Port2, STIP2, NetProt2}, Opts), R=rpc:call(Node, ejabberd_listener, add_listener, [{Port2, IP2, NetProt2}, Module, OptsClean]), throw({is_added, R}); _ -> ok end. node_modules_to_xhtml(Modules, Lang) -> ?XAE('table', [?XMLATTR('class', <<"withtextareas">>)], [?XE('thead', [?XE('tr', [?XCT('td', "Module"), ?XCT('td', "Options") ])]), ?XE('tbody', lists:map( fun({Module, Opts} = _E) -> SModule = atom_to_list(Module), {NumLines, SOpts} = term_to_paragraph(Opts, 40), %%ID = term_to_id(E), ?XE('tr', [?XC('td', SModule), ?XE('td', [?TEXTAREA("opts" ++ SModule, integer_to_list(NumLines), "40", SOpts)]), ?XE('td', [?INPUTT("submit", "restart" ++ SModule, "Restart")]), ?XE('td', [?INPUTT("submit", "stop" ++ SModule, "Stop")]) ] ) end, Modules) ++ [?XE('tr', [?XE('td', [?INPUT("text", "modulenew", "")]), ?XE('td', [?TEXTAREA("optsnew", "2", "40", "[]")]), ?XAE('td', [?XMLATTR("colspan", "2")], [?INPUTT("submit", "start", "Start")]) ] )] )]). node_modules_parse_query(Host, Node, Modules, Query) -> lists:foreach( fun({Module, _Opts1}) -> SModule = atom_to_list(Module), case lists:keysearch("restart" ++ SModule, 1, Query) of {value, _} -> {value, {_, SOpts}} = lists:keysearch("opts" ++ SModule, 1, Query), {ok, Tokens, _} = erl_scan:string(SOpts ++ "."), {ok, Opts} = erl_parse:parse_term(Tokens), rpc:call(Node, gen_mod, stop_module, [Host, Module]), rpc:call(Node, gen_mod, start_module, [Host, Module, Opts]), throw(submitted); _ -> case lists:keysearch("stop" ++ SModule, 1, Query) of {value, _} -> rpc:call(Node, gen_mod, stop_module, [Host, Module]), throw(submitted); _ -> ok end end end, Modules), case lists:keysearch("start", 1, Query) of {value, _} -> {{value, {_, SModule}}, {value, {_, SOpts}}} = {lists:keysearch("modulenew", 1, Query), lists:keysearch("optsnew", 1, Query)}, Module = list_to_atom(SModule), {ok, Tokens, _} = erl_scan:string(SOpts ++ "."), {ok, Opts} = erl_parse:parse_term(Tokens), rpc:call(Node, gen_mod, start_module, [Host, Module, Opts]), throw(submitted); _ -> ok end. node_update_parse_query(Node, Query) -> case lists:keysearch("update", 1, Query) of {value, _} -> ModulesToUpdateStrings = proplists:get_all_values("selected",Query), ModulesToUpdate = [list_to_atom(M) || M <- ModulesToUpdateStrings], case rpc:call(Node, ejabberd_update, update, [ModulesToUpdate]) of {ok, _} -> ok; {error, Error} -> ?ERROR_MSG("~p~n", [Error]); {badrpc, Error} -> ?ERROR_MSG("~p~n", [Error]) end; _ -> nothing end. element_to_list(X) when is_atom(X) -> atom_to_list(X); element_to_list(X) when is_integer(X) -> integer_to_list(X). list_to_element(List) -> {ok, Tokens, _} = erl_scan:string(List), [{_, _, Element}] = Tokens, Element. url_func({user_diapason, From, To}) -> integer_to_list(From) ++ "-" ++ integer_to_list(To) ++ "/"; url_func({users_queue, Prefix, User, _Server}) -> Prefix ++ "user/" ++ User ++ "/queue/"; url_func({user, Prefix, User, _Server}) -> Prefix ++ "user/" ++ User ++ "/". last_modified() -> {"Last-Modified", "Mon, 25 Feb 2008 13:23:30 GMT"}. cache_control_public() -> {"Cache-Control", "public"}. %% Transform 1234567890 into "1,234,567,890" pretty_string_int(Integer) when is_integer(Integer) -> pretty_string_int(integer_to_list(Integer)); pretty_string_int(String) when is_list(String) -> {_, Result} = lists:foldl( fun(NewNumber, {3, Result}) -> {1, [NewNumber, $, | Result]}; (NewNumber, {CountAcc, Result}) -> {CountAcc+1, [NewNumber | Result]} end, {0, ""}, lists:reverse(String)), Result. %%%================================== %%%% navigation menu %% @spec (Host, Node, Lang, JID::jid()) -> [LI] make_navigation(Host, Node, Lang, JID) -> Menu = make_navigation_menu(Host, Node, Lang, JID), make_menu_items(Lang, Menu). %% @spec (Host, Node, Lang, JID::jid()) -> Menu %% where Host = global | string() %% Node = cluster | string() %% Lang = string() %% Menu = {URL, Title} | {URL, Title, [Menu]} %% URL = string() %% Title = string() make_navigation_menu(Host, Node, Lang, JID) -> HostNodeMenu = make_host_node_menu(Host, Node, Lang, JID), HostMenu = make_host_menu(Host, HostNodeMenu, Lang, JID), NodeMenu = make_node_menu(Host, Node, Lang), make_server_menu(HostMenu, NodeMenu, Lang, JID). %% @spec (Host, Node, Base, Lang) -> [LI] make_menu_items(global, cluster, Base, Lang) -> HookItems = get_menu_items_hook(server, Lang), make_menu_items(Lang, {Base, "", HookItems}); make_menu_items(global, Node, Base, Lang) -> HookItems = get_menu_items_hook({node, Node}, Lang), make_menu_items(Lang, {Base, "", HookItems}); make_menu_items(Host, cluster, Base, Lang) -> HookItems = get_menu_items_hook({host, Host}, Lang), make_menu_items(Lang, {Base, "", HookItems}); make_menu_items(Host, Node, Base, Lang) -> HookItems = get_menu_items_hook({hostnode, Host, Node}, Lang), make_menu_items(Lang, {Base, "", HookItems}). make_host_node_menu(global, _, _Lang, _JID) -> {"", "", []}; make_host_node_menu(_, cluster, _Lang, _JID) -> {"", "", []}; make_host_node_menu(Host, Node, Lang, JID) -> HostNodeBase = get_base_path(Host, Node), HostNodeFixed = [{"modules/", "Modules"}] ++ get_menu_items_hook({hostnode, Host, Node}, Lang), HostNodeBasePath = url_to_path(HostNodeBase), HostNodeFixed2 = [Tuple || Tuple <- HostNodeFixed, is_allowed_path(HostNodeBasePath, Tuple, JID)], {HostNodeBase, atom_to_list(Node), HostNodeFixed2}. make_host_menu(global, _HostNodeMenu, _Lang, _JID) -> {"", "", []}; make_host_menu(Host, HostNodeMenu, Lang, JID) -> HostBase = get_base_path(Host, cluster), HostFixed = [{"acls", "Access Control Lists"}, {"access", "Access Rules"}, {"users", "Users"}, {"online-users", "Online Users"}] ++ get_lastactivity_menuitem_list(Host) ++ [{"nodes", "Nodes", HostNodeMenu}, {"misc", "Miscelanea Options"}, {"stats", "Statistics"}] ++ get_menu_items_hook({host, Host}, Lang), HostBasePath = url_to_path(HostBase), HostFixed2 = [Tuple || Tuple <- HostFixed, is_allowed_path(HostBasePath, Tuple, JID)], {HostBase, Host, HostFixed2}. make_node_menu(_Host, cluster, _Lang) -> {"", "", []}; make_node_menu(global, Node, Lang) -> NodeBase = get_base_path(global, Node), NodeFixed = [{"db/", "Database"}, {"backup/", "Backup"}, {"ports/", "Listened Ports"}, {"stats/", "Statistics"}, {"update/", "Update"}] ++ get_menu_items_hook({node, Node}, Lang), {NodeBase, atom_to_list(Node), NodeFixed}; make_node_menu(_Host, _Node, _Lang) -> {"", "", []}. make_server_menu(HostMenu, NodeMenu, Lang, JID) -> Base = get_base_path(global, cluster), Fixed = [{"acls", "Access Control Lists"}, {"access", "Access Rules"}, {"vhosts", "Virtual Hosts", HostMenu}, {"nodes", "Nodes", NodeMenu}, {"misc", "Miscelanea Options"}, {"stats", "Statistics"}] ++ get_menu_items_hook(server, Lang), BasePath = url_to_path(Base), Fixed2 = [Tuple || Tuple <- Fixed, is_allowed_path(BasePath, Tuple, JID)], {Base, "ejabberd", Fixed2}. get_menu_items_hook({hostnode, Host, Node}, Lang) -> ejabberd_hooks:run_fold(webadmin_menu_hostnode, list_to_binary(Host), [], [Host, Node, Lang]); get_menu_items_hook({host, Host}, Lang) -> ejabberd_hooks:run_fold(webadmin_menu_host, list_to_binary(Host), [], [Host, Lang]); get_menu_items_hook({node, Node}, Lang) -> ejabberd_hooks:run_fold(webadmin_menu_node, [], [Node, Lang]); get_menu_items_hook(server, Lang) -> ejabberd_hooks:run_fold(webadmin_menu_main, [], [Lang]). %% @spec (Lang::string(), Menu) -> [LI] %% where Menu = {MURI::string(), MName::string(), Items::[Item]} %% Item = {IURI::string(), IName::string()} | {IURI::string(), IName::string(), Menu} make_menu_items(Lang, Menu) -> lists:reverse(make_menu_items2(Lang, 1, Menu)). make_menu_items2(Lang, Deep, {MURI, MName, _} = Menu) -> Res = case MName of "" -> []; _ -> [make_menu_item(header, Deep, MURI, MName, Lang) ] end, make_menu_items2(Lang, Deep, Menu, Res). make_menu_items2(_, _Deep, {_, _, []}, Res) -> Res; make_menu_items2(Lang, Deep, {MURI, MName, [Item | Items]}, Res) -> Res2 = case Item of {IURI, IName} -> [make_menu_item(item, Deep, MURI++IURI++"/", IName, Lang) | Res]; {IURI, IName, SubMenu} -> %%ResTemp = [?LI([?ACT(MURI ++ IURI ++ "/", IName)]) | Res], ResTemp = [make_menu_item(item, Deep, MURI++IURI++"/", IName, Lang) | Res], ResSubMenu = make_menu_items2(Lang, Deep+1, SubMenu), ResSubMenu ++ ResTemp end, make_menu_items2(Lang, Deep, {MURI, MName, Items}, Res2). make_menu_item(header, 1, URI, Name, _Lang) -> ?LI([?XAE('div', [?XMLATTR('id', <<"navhead">>)], [?AC(URI, Name)] )]); make_menu_item(header, 2, URI, Name, _Lang) -> ?LI([?XAE('div', [?XMLATTR('id', <<"navheadsub">>)], [?AC(URI, Name)] )]); make_menu_item(header, 3, URI, Name, _Lang) -> ?LI([?XAE('div', [?XMLATTR('id', <<"navheadsubsub">>)], [?AC(URI, Name)] )]); make_menu_item(item, 1, URI, Name, Lang) -> ?LI([?XAE('div', [?XMLATTR('id', <<"navitem">>)], [?ACT(URI, Name)] )]); make_menu_item(item, 2, URI, Name, Lang) -> ?LI([?XAE('div', [?XMLATTR('id', <<"navitemsub">>)], [?ACT(URI, Name)] )]); make_menu_item(item, 3, URI, Name, Lang) -> ?LI([?XAE('div', [?XMLATTR('id', <<"navitemsubsub">>)], [?ACT(URI, Name)] )]). %%%================================== %%% vim: set foldmethod=marker foldmarker=%%%%,%%%=: