mod_admin_telnet: Don't rely on getpeerchain returning an empty list
[prosody.git] / core / modulemanager.lua
index bbe24e32071a79b7799328a3b1b2eec890caf0fb..535c227b13e908d25ad9346c78c7f63c9ac0c85a 100644 (file)
@@ -9,23 +9,17 @@
 local logger = require "util.logger";
 local log = logger.init("modulemanager");
 local config = require "core.configmanager";
-local multitable_new = require "util.multitable".new;
-local st = require "util.stanza";
 local pluginloader = require "util.pluginloader";
+local set = require "util.set";
+
+local new_multitable = require "util.multitable".new;
 
 local hosts = hosts;
 local prosody = prosody;
-local prosody_events = prosody.events;
-
-local loadfile, pcall, xpcall = loadfile, pcall, xpcall;
-local setmetatable, setfenv, getfenv = setmetatable, setfenv, getfenv;
-local pairs, ipairs = pairs, ipairs;
-local t_insert, t_concat = table.insert, table.concat;
-local type = type;
-local next = next;
-local rawget = rawget;
-local error = error;
-local tostring, tonumber = tostring, tonumber;
+
+local pcall, xpcall = pcall, xpcall;
+local setmetatable, rawget = setmetatable, rawget;
+local ipairs, pairs, type, tostring, t_insert = ipairs, pairs, type, tostring, table.insert;
 
 local debug_traceback = debug.traceback;
 local unpack, select = unpack, select;
@@ -35,10 +29,8 @@ pcall = function(f, ...)
        return xpcall(function() return f(unpack(params, 1, n)) end, function(e) return tostring(e).."\n"..debug_traceback(); end);
 end
 
-local array, set = require "util.array", require "util.set";
-
-local autoload_modules = {"presence", "message", "iq", "offline"};
-local component_inheritable_modules = {"tls", "dialback", "iq"};
+local autoload_modules = {"presence", "message", "iq", "offline", "c2s", "s2s"};
+local component_inheritable_modules = {"tls", "dialback", "iq", "s2s"};
 
 -- We need this to let modules access the real global namespace
 local _G = _G;
@@ -47,22 +39,17 @@ module "modulemanager"
 
 local api = _G.require "core.moduleapi"; -- Module API container
 
+-- [host] = { [module] = module_env }
 local modulemap = { ["*"] = {} };
 
-local modulehelpers = setmetatable({}, { __index = _G });
-
-local hooks = multitable_new();
-
-local NULL = {};
-
 -- Load modules when a host is activated
 function load_modules_for_host(host)
-       local component = config.get(host, "core", "component_module");
+       local component = config.get(host, "component_module");
        
-       local global_modules_enabled = config.get("*", "core", "modules_enabled");
-       local global_modules_disabled = config.get("*", "core", "modules_disabled");
-       local host_modules_enabled = config.get(host, "core", "modules_enabled");
-       local host_modules_disabled = config.get(host, "core", "modules_disabled");
+       local global_modules_enabled = config.get("*", "modules_enabled");
+       local global_modules_disabled = config.get("*", "modules_disabled");
+       local host_modules_enabled = config.get(host, "modules_enabled");
+       local host_modules_disabled = config.get(host, "modules_disabled");
        
        if host_modules_enabled == global_modules_enabled then host_modules_enabled = nil; end
        if host_modules_disabled == global_modules_disabled then host_modules_disabled = nil; end
@@ -87,117 +74,134 @@ function load_modules_for_host(host)
                load(host, module);
        end
 end
-prosody_events.add_handler("host-activated", load_modules_for_host);
---
+prosody.events.add_handler("host-activated", load_modules_for_host);
+prosody.events.add_handler("host-deactivated", function (host)
+       modulemap[host] = nil;
+end);
+
+--- Private helpers ---
 
-function load(host, module_name, config)
+local function do_unload_module(host, name)
+       local mod = get_module(host, name);
+       if not mod then return nil, "module-not-loaded"; end
+       
+       if module_has_method(mod, "unload") then
+               local ok, err = call_module_method(mod, "unload");
+               if (not ok) and err then
+                       log("warn", "Non-fatal error unloading module '%s' on '%s': %s", name, host, err);
+               end
+       end
+       
+       for object, event, handler in mod.module.event_handlers:iter(nil, nil, nil) do
+               object.remove_handler(event, handler);
+       end
+       
+       if mod.module.items then -- remove items
+               local events = (host == "*" and prosody.events) or hosts[host].events;
+               for key,t in pairs(mod.module.items) do
+                       for i = #t,1,-1 do
+                               local value = t[i];
+                               t[i] = nil;
+                               events.fire_event("item-removed/"..key, {source = mod.module, item = value});
+                       end
+               end
+       end
+       mod.module.loaded = false;
+       modulemap[host][name] = nil;
+       return true;
+end
+
+local function do_load_module(host, module_name, state)
        if not (host and module_name) then
                return nil, "insufficient-parameters";
-       elseif not hosts[host] then
+       elseif not hosts[host] and host ~= "*"then
                return nil, "unknown-host";
        end
        
        if not modulemap[host] then
-               modulemap[host] = {};
+               modulemap[host] = hosts[host].modules;
        end
        
        if modulemap[host][module_name] then
                log("warn", "%s is already loaded for %s, so not loading again", module_name, host);
                return nil, "module-already-loaded";
        elseif modulemap["*"][module_name] then
+               local mod = modulemap["*"][module_name];
+               if module_has_method(mod, "add_host") then
+                       local _log = logger.init(host..":"..module_name);
+                       local host_module_api = setmetatable({
+                               host = host, event_handlers = new_multitable(), items = {};
+                               _log = _log, log = function (self, ...) return _log(...); end;
+                       },{
+                               __index = modulemap["*"][module_name].module;
+                       });
+                       local host_module = setmetatable({ module = host_module_api }, { __index = mod });
+                       host_module_api.environment = host_module;
+                       modulemap[host][module_name] = host_module;
+                       local ok, result, module_err = call_module_method(mod, "add_host", host_module_api);
+                       if not ok or result == false then
+                               modulemap[host][module_name] = nil;
+                               return nil, ok and module_err or result;
+                       end
+                       return host_module;
+               end
                return nil, "global-module-already-loaded";
        end
        
 
-       local mod, err = pluginloader.load_code(module_name);
-       if not mod then
-               log("error", "Unable to load module '%s': %s", module_name or "nil", err or "nil");
-               return nil, err;
-       end
 
        local _log = logger.init(host..":"..module_name);
-       local api_instance = setmetatable({ name = module_name, host = host, path = err, _log = _log, log = function (self, ...) return _log(...); end }, { __index = api });
+       local api_instance = setmetatable({ name = module_name, host = host,
+               _log = _log, log = function (self, ...) return _log(...); end, event_handlers = new_multitable(),
+               reloading = not not state, saved_state = state~=true and state or nil }
+               , { __index = api });
 
        local pluginenv = setmetatable({ module = api_instance }, { __index = _G });
        api_instance.environment = pluginenv;
        
-       setfenv(mod, pluginenv);
-       hosts[host].modules = modulemap[host];
+       local mod, err = pluginloader.load_code(module_name, nil, pluginenv);
+       if not mod then
+               log("error", "Unable to load module '%s': %s", module_name or "nil", err or "nil");
+               return nil, err;
+       end
+
+       api_instance.path = err;
+
        modulemap[host][module_name] = pluginenv;
-       
-       local success, err = pcall(mod);
-       if success then
+       local ok, err = pcall(mod);
+       if ok then
+               -- Call module's "load"
                if module_has_method(pluginenv, "load") then
-                       success, err = call_module_method(pluginenv, "load");
-                       if not success then
+                       ok, err = call_module_method(pluginenv, "load");
+                       if not ok then
                                log("warn", "Error loading module '%s' on '%s': %s", module_name, host, err or "nil");
                        end
                end
+               api_instance.reloading, api_instance.saved_state = nil, nil;
 
-               -- Use modified host, if the module set one
-               if api_instance.host == "*" and host ~= "*" then
+               if api_instance.host == "*" then
+                       if not api_instance.global then -- COMPAT w/pre-0.9
+                               if host ~= "*" then
+                                       log("warn", "mod_%s: Setting module.host = '*' deprecated, call module:set_global() instead", module_name);
+                               end
+                               api_instance:set_global();
+                       end
                        modulemap[host][module_name] = nil;
-                       modulemap["*"][module_name] = pluginenv;
-                       api_instance:set_global();
-               end
-       else
-               log("error", "Error initializing module '%s' on '%s': %s", module_name, host, err or "nil");
-       end
-       if success then
-               (hosts[api_instance.host] or prosody).events.fire_event("module-loaded", { module = module_name, host = host });
-               return true;
-       else -- load failed, unloading
-               unload(api_instance.host, module_name);
-               return nil, err;
-       end
-end
-
-function get_module(host, name)
-       return modulemap[host] and modulemap[host][name];
-end
-
-function is_loaded(host, name)
-       return modulemap[host] and modulemap[host][name] and true;
-end
-
-function unload(host, name, ...)
-       local mod = get_module(host, name);
-       if not mod then return nil, "module-not-loaded"; end
-       
-       if module_has_method(mod, "unload") then
-               local ok, err = call_module_method(mod, "unload");
-               if (not ok) and err then
-                       log("warn", "Non-fatal error unloading module '%s' on '%s': %s", name, host, err);
-               end
-       end
-       -- unhook event handlers hooked by module:hook
-       for event, handlers in pairs(hooks:get(host, name) or NULL) do
-               for handler in pairs(handlers or NULL) do
-                       (hosts[host] or prosody).events.remove_handler(event, handler);
-               end
-       end
-       -- unhook event handlers hooked by module:hook_global
-       for event, handlers in pairs(hooks:get("*", name) or NULL) do
-               for handler in pairs(handlers or NULL) do
-                       prosody.events.remove_handler(event, handler);
-               end
-       end
-       hooks:remove(host, name);
-       if mod.module.items then -- remove items
-               for key,t in pairs(mod.module.items) do
-                       for i = #t,1,-1 do
-                               local value = t[i];
-                               t[i] = nil;
-                               hosts[host].events.fire_event("item-removed/"..key, {source = mod.module, item = value});
+                       modulemap[api_instance.host][module_name] = pluginenv;
+                       if host ~= api_instance.host and module_has_method(pluginenv, "add_host") then
+                               -- Now load the module again onto the host it was originally being loaded on
+                               ok, err = do_load_module(host, module_name);
                        end
                end
        end
-       modulemap[host][name] = nil;
-       (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host });
-       return true;
+       if not ok then
+               modulemap[api_instance.host][module_name] = nil;
+               log("error", "Error initializing module '%s' on '%s': %s", module_name, host, err or "nil");
+       end
+       return ok and pluginenv, err;
 end
 
-function reload(host, name, ...)
+local function do_reload_module(host, name)
        local mod = get_module(host, name);
        if not mod then return nil, "module-not-loaded"; end
 
@@ -208,14 +212,13 @@ function reload(host, name, ...)
        end
 
        local saved;
-
        if module_has_method(mod, "save") then
                local ok, ret, err = call_module_method(mod, "save");
                if ok then
                        saved = ret;
                else
                        log("warn", "Error saving module '%s:%s' state: %s", host, name, ret);
-                       if not config.get(host, "core", "force_module_reload") then
+                       if not config.get(host, "force_module_reload") then
                                log("warn", "Aborting reload due to error, set force_module_reload to ignore this");
                                return nil, "save-state-failed";
                        else
@@ -224,8 +227,9 @@ function reload(host, name, ...)
                end
        end
 
-       unload(host, name, ...);
-       local ok, err = load(host, name, ...);
+       mod.module.reloading = true;
+       do_unload_module(host, name);
+       local ok, err = do_load_module(host, name, saved or true);
        if ok then
                mod = get_module(host, name);
                if module_has_method(mod, "restore") then
@@ -234,18 +238,78 @@ function reload(host, name, ...)
                                log("warn", "Error restoring module '%s' from '%s': %s", name, host, err);
                        end
                end
-               return true;
+       end
+       return ok and mod, err;
+end
+
+--- Public API ---
+
+-- Load a module and fire module-loaded event
+function load(host, name)
+       local mod, err = do_load_module(host, name);
+       if mod then
+               (hosts[mod.module.host] or prosody).events.fire_event("module-loaded", { module = name, host = mod.module.host });
+       end
+       return mod, err;
+end
+
+-- Unload a module and fire module-unloaded
+function unload(host, name)
+       local ok, err = do_unload_module(host, name);
+       if ok then
+               (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host });
        end
        return ok, err;
 end
 
+function reload(host, name)
+       local mod, err = do_reload_module(host, name);
+       if mod then
+               modulemap[host][name].module.reloading = true;
+               (hosts[host] or prosody).events.fire_event("module-reloaded", { module = name, host = host });
+               mod.module.reloading = nil;
+       elseif not is_loaded(host, name) then
+               (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host });
+       end
+       return mod, err;
+end
+
+function get_module(host, name)
+       return modulemap[host] and modulemap[host][name];
+end
+
+function get_items(key, host)
+       local result = {};
+       local modules = modulemap[host];
+       if not key or not host or not modules then return nil; end
+
+       for _, module in pairs(modules) do
+               local mod = module.module;
+               if mod.items and mod.items[key] then
+                       for _, value in ipairs(mod.items[key]) do
+                               t_insert(result, value);
+                       end
+               end
+       end
+
+       return result;
+end
+
+function get_modules(host)
+       return modulemap[host];
+end
+
+function is_loaded(host, name)
+       return modulemap[host] and modulemap[host][name] and true;
+end
+
 function module_has_method(module, method)
-       return type(module.module[method]) == "function";
+       return type(rawget(module.module, method)) == "function";
 end
 
 function call_module_method(module, method, ...)
-       if module_has_method(module, method) then
-               local f = module.module[method];
+       local f = rawget(module.module, method);
+       if type(f) == "function" then
                return pcall(f, ...);
        else
                return false, "no-such-method";