Merge 0.9->0.10 (third time lucky)
[prosody.git] / core / configmanager.lua
1 -- Prosody IM
2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
4 --
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
7 --
8
9 local _G = _G;
10 local setmetatable, rawget, rawset, io, error, dofile, type, pairs, table =
11       setmetatable, rawget, rawset, io, error, dofile, type, pairs, table;
12 local format, math_max = string.format, math.max;
13
14 local fire_event = prosody and prosody.events.fire_event or function () end;
15
16 local envload = require"util.envload".envload;
17 local deps = require"util.dependencies";
18 local resolve_relative_path = require"util.paths".resolve_relative_path;
19 local glob_to_pattern = require"util.paths".glob_to_pattern;
20 local path_sep = package.config:sub(1,1);
21
22 local have_encodings, encodings = pcall(require, "util.encodings");
23 local nameprep = have_encodings and encodings.stringprep.nameprep or function (host) return host:lower(); end
24
25 module "configmanager"
26
27 _M.resolve_relative_path = resolve_relative_path; -- COMPAT
28
29 local parsers = {};
30
31 local config_mt = { __index = function (t, k) return rawget(t, "*"); end};
32 local config = setmetatable({ ["*"] = { } }, config_mt);
33
34 -- When host not found, use global
35 local host_mt = { __index = function(_, k) return config["*"][k] end }
36
37 function getconfig()
38         return config;
39 end
40
41 function get(host, key, _oldkey)
42         if key == "core" then
43                 key = _oldkey; -- COMPAT with code that still uses "core"
44         end
45         return config[host][key];
46 end
47 function _M.rawget(host, key, _oldkey)
48         if key == "core" then
49                 key = _oldkey; -- COMPAT with code that still uses "core"
50         end
51         local hostconfig = rawget(config, host);
52         if hostconfig then
53                 return rawget(hostconfig, key);
54         end
55 end
56
57 local function set(config, host, key, value)
58         if host and key then
59                 local hostconfig = rawget(config, host);
60                 if not hostconfig then
61                         hostconfig = rawset(config, host, setmetatable({}, host_mt))[host];
62                 end
63                 hostconfig[key] = value;
64                 return true;
65         end
66         return false;
67 end
68
69 function _M.set(host, key, value, _oldvalue)
70         if key == "core" then
71                 key, value = value, _oldvalue; --COMPAT with code that still uses "core"
72         end
73         return set(config, host, key, value);
74 end
75
76 function load(filename, format)
77         format = format or filename:match("%w+$");
78
79         if parsers[format] and parsers[format].load then
80                 local f, err = io.open(filename);
81                 if f then
82                         local new_config = setmetatable({ ["*"] = { } }, config_mt);
83                         local ok, err = parsers[format].load(f:read("*a"), filename, new_config);
84                         f:close();
85                         if ok then
86                                 config = new_config;
87                                 fire_event("config-reloaded", {
88                                         filename = filename,
89                                         format = format,
90                                         config = config
91                                 });
92                         end
93                         return ok, "parser", err;
94                 end
95                 return f, "file", err;
96         end
97
98         if not format then
99                 return nil, "file", "no parser specified";
100         else
101                 return nil, "file", "no parser for "..(format);
102         end
103 end
104
105 function save(filename, format)
106 end
107
108 function addparser(format, parser)
109         if format and parser then
110                 parsers[format] = parser;
111         end
112 end
113
114 -- _M needed to avoid name clash with local 'parsers'
115 function _M.parsers()
116         local p = {};
117         for format in pairs(parsers) do
118                 table.insert(p, format);
119         end
120         return p;
121 end
122
123 -- Built-in Lua parser
124 do
125         local pcall, setmetatable = _G.pcall, _G.setmetatable;
126         local rawget = _G.rawget;
127         parsers.lua = {};
128         function parsers.lua.load(data, config_file, config)
129                 local env;
130                 -- The ' = true' are needed so as not to set off __newindex when we assign the functions below
131                 env = setmetatable({
132                         Host = true, host = true, VirtualHost = true,
133                         Component = true, component = true,
134                         Include = true, include = true, RunScript = true }, {
135                                 __index = function (t, k)
136                                         return rawget(_G, k);
137                                 end,
138                                 __newindex = function (t, k, v)
139                                         set(config, env.__currenthost or "*", k, v);
140                                 end
141                 });
142
143                 rawset(env, "__currenthost", "*") -- Default is global
144                 function env.VirtualHost(name)
145                         name = nameprep(name);
146                         if rawget(config, name) and rawget(config[name], "component_module") then
147                                 error(format("Host %q clashes with previously defined %s Component %q, for services use a sub-domain like conference.%s",
148                                         name, config[name].component_module:gsub("^%a+$", { component = "external", muc = "MUC"}), name, name), 0);
149                         end
150                         rawset(env, "__currenthost", name);
151                         -- Needs at least one setting to logically exist :)
152                         set(config, name or "*", "defined", true);
153                         return function (config_options)
154                                 rawset(env, "__currenthost", "*"); -- Return to global scope
155                                 for option_name, option_value in pairs(config_options) do
156                                         set(config, name or "*", option_name, option_value);
157                                 end
158                         end;
159                 end
160                 env.Host, env.host = env.VirtualHost, env.VirtualHost;
161
162                 function env.Component(name)
163                         name = nameprep(name);
164                         if rawget(config, name) and rawget(config[name], "defined") and not rawget(config[name], "component_module") then
165                                 error(format("Component %q clashes with previously defined Host %q, for services use a sub-domain like conference.%s",
166                                         name, name, name), 0);
167                         end
168                         set(config, name, "component_module", "component");
169                         -- Don't load the global modules by default
170                         set(config, name, "load_global_modules", false);
171                         rawset(env, "__currenthost", name);
172                         local function handle_config_options(config_options)
173                                 rawset(env, "__currenthost", "*"); -- Return to global scope
174                                 for option_name, option_value in pairs(config_options) do
175                                         set(config, name or "*", option_name, option_value);
176                                 end
177                         end
178
179                         return function (module)
180                                         if type(module) == "string" then
181                                                 set(config, name, "component_module", module);
182                                                 return handle_config_options;
183                                         end
184                                         return handle_config_options(module);
185                                 end
186                 end
187                 env.component = env.Component;
188
189                 function env.Include(file)
190                         if file:match("[*?]") then
191                                 local lfs = deps.softreq "lfs";
192                                 if not lfs then
193                                         error(format("Error expanding wildcard pattern in Include %q - LuaFileSystem not available", file));
194                                 end
195                                 local path_pos, glob = file:match("()([^"..path_sep.."]+)$");
196                                 local path = file:sub(1, math_max(path_pos-2,0));
197                                 local config_path = config_file:gsub("[^"..path_sep.."]+$", "");
198                                 if #path > 0 then
199                                         path = resolve_relative_path(config_path, path);
200                                 else
201                                         path = config_path;
202                                 end
203                                 local patt = glob_to_pattern(glob);
204                                 for f in lfs.dir(path) do
205                                         if f:sub(1,1) ~= "." and f:match(patt) then
206                                                 env.Include(path..path_sep..f);
207                                         end
208                                 end
209                         else
210                                 local file = resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file);
211                                 local f, err = io.open(file);
212                                 if f then
213                                         local ret, err = parsers.lua.load(f:read("*a"), file, config);
214                                         if not ret then error(err:gsub("%[string.-%]", file), 0); end
215                                 end
216                                 if not f then error("Error loading included "..file..": "..err, 0); end
217                                 return f, err;
218                         end
219                 end
220                 env.include = env.Include;
221
222                 function env.RunScript(file)
223                         return dofile(resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file));
224                 end
225
226                 local chunk, err = envload(data, "@"..config_file, env);
227
228                 if not chunk then
229                         return nil, err;
230                 end
231
232                 local ok, err = pcall(chunk);
233
234                 if not ok then
235                         return nil, err;
236                 end
237
238                 return true;
239         end
240
241 end
242
243 return _M;