Software /
code /
prosody
Comparison
plugins/mod_storage_xep0227.lua @ 11789:f3085620b6ff
mod_storage_xep0227: Update for XEP-0227 r1.1: Support for SCRAM, MAM, PEP
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Sun, 12 Sep 2021 11:38:47 +0100 |
parent | 8352:6ff50541d2a6 |
child | 11840:5e9e75c277a2 |
comparison
equal
deleted
inserted
replaced
11788:1ceee8becb1a | 11789:f3085620b6ff |
---|---|
1 | 1 |
2 local ipairs, pairs = ipairs, pairs; | 2 local ipairs, pairs = ipairs, pairs; |
3 local setmetatable = setmetatable; | 3 local setmetatable = setmetatable; |
4 local tostring = tostring; | 4 local tostring = tostring; |
5 local next = next; | 5 local next, unpack = next, table.unpack or unpack; --luacheck: ignore 113/unpack |
6 local t_remove = table.remove; | 6 local t_remove = table.remove; |
7 local os_remove = os.remove; | 7 local os_remove = os.remove; |
8 local io_open = io.open; | 8 local io_open = io.open; |
9 | 9 local jid_bare = require "util.jid".bare; |
10 local jid_prep = require "util.jid".prep; | |
11 | |
12 local array = require "util.array"; | |
13 local base64 = require "util.encodings".base64; | |
14 local dt = require "util.datetime"; | |
15 local hex = require "util.hex"; | |
16 local it = require "util.iterators"; | |
10 local paths = require"util.paths"; | 17 local paths = require"util.paths"; |
18 local set = require "util.set"; | |
11 local st = require "util.stanza"; | 19 local st = require "util.stanza"; |
12 local parse_xml_real = require "util.xml".parse; | 20 local parse_xml_real = require "util.xml".parse; |
21 | |
22 local lfs = require "lfs"; | |
13 | 23 |
14 local function getXml(user, host) | 24 local function getXml(user, host) |
15 local jid = user.."@"..host; | 25 local jid = user.."@"..host; |
16 local path = paths.join(prosody.paths.data, jid..".xml"); | 26 local path = paths.join(prosody.paths.data, jid..".xml"); |
17 local f = io_open(path); | 27 local f, err = io_open(path); |
18 if not f then return; end | 28 if not f then |
29 module:log("debug", "Unable to load XML file for <%s>: %s", jid, err); | |
30 return; | |
31 end | |
32 module:log("debug", "Loaded %s", path); | |
19 local s = f:read("*a"); | 33 local s = f:read("*a"); |
20 f:close(); | 34 f:close(); |
21 return parse_xml_real(s); | 35 return parse_xml_real(s); |
22 end | 36 end |
23 local function setXml(user, host, xml) | 37 local function setXml(user, host, xml) |
43 if user and user.name == "user" then | 57 if user and user.name == "user" then |
44 return user; | 58 return user; |
45 end | 59 end |
46 end | 60 end |
47 end | 61 end |
62 module:log("warn", "Unable to find user element"); | |
48 end | 63 end |
49 local function createOuterXml(user, host) | 64 local function createOuterXml(user, host) |
50 return st.stanza("server-data", {xmlns='urn:xmpp:pie:0'}) | 65 return st.stanza("server-data", {xmlns='urn:xmpp:pie:0'}) |
51 :tag("host", {jid=host}) | 66 :tag("host", {jid=host}) |
52 :tag("user", {name = user}); | 67 :tag("user", {name = user}); |
53 end | 68 end |
54 local function removeFromArray(array, value) | 69 local function removeFromArray(arr, value) |
55 for i,item in ipairs(array) do | 70 for i,item in ipairs(arr) do |
56 if item == value then | 71 if item == value then |
57 t_remove(array, i); | 72 t_remove(arr, i); |
58 return; | 73 return; |
59 end | 74 end |
60 end | 75 end |
61 end | 76 end |
62 local function removeStanzaChild(s, child) | 77 local function removeStanzaChild(s, child) |
63 removeFromArray(s.tags, child); | 78 removeFromArray(s.tags, child); |
64 removeFromArray(s, child); | 79 removeFromArray(s, child); |
65 end | 80 end |
66 | 81 |
82 local function hex_to_base64(s) | |
83 return base64.encode(hex.from(s)); | |
84 end | |
85 | |
86 local function base64_to_hex(s) | |
87 return base64.encode(hex.from(s)); | |
88 end | |
89 | |
67 local handlers = {}; | 90 local handlers = {}; |
68 | 91 |
69 -- In order to support mod_auth_internal_hashed | 92 -- In order to support custom account properties |
70 local extended = "http://prosody.im/protocol/extended-xep0227\1"; | 93 local extended = "http://prosody.im/protocol/extended-xep0227\1"; |
94 | |
95 local scram_hash_name = module:get_option_string("password_hash", "SHA-1"); | |
96 local scram_properties = set.new({ "server_key", "stored_key", "iteration_count", "salt" }); | |
71 | 97 |
72 handlers.accounts = { | 98 handlers.accounts = { |
73 get = function(self, user) | 99 get = function(self, user) |
74 user = getUserElement(getXml(user, self.host)); | 100 user = getUserElement(getXml(user, self.host)); |
75 if user and user.attr.password then | 101 local scram_credentials = user and user:get_child_with_attr( |
102 "scram-credentials", "urn:xmpp:pie:0#scram", | |
103 "mechanism", "SCRAM-"..scram_hash_name | |
104 ); | |
105 if scram_credentials then | |
106 return { | |
107 iteration_count = tonumber(scram_credentials:get_child_text("iter-count")); | |
108 server_key = base64_to_hex(scram_credentials:get_child_text("server-key")); | |
109 stored_key = base64_to_hex(scram_credentials:get_child_text("stored-key")); | |
110 salt = base64.decode(scram_credentials:get_child_text("salt")); | |
111 }; | |
112 elseif user and user.attr.password then | |
76 return { password = user.attr.password }; | 113 return { password = user.attr.password }; |
77 elseif user then | 114 elseif user then |
78 local data = {}; | 115 local data = {}; |
79 for k, v in pairs(user.attr) do | 116 for k, v in pairs(user.attr) do |
80 if k:sub(1, #extended) == extended then | 117 if k:sub(1, #extended) == extended then |
83 end | 120 end |
84 return data; | 121 return data; |
85 end | 122 end |
86 end; | 123 end; |
87 set = function(self, user, data) | 124 set = function(self, user, data) |
88 if data then | 125 if not data then |
89 local xml = getXml(user, self.host); | |
90 if not xml then xml = createOuterXml(user, self.host); end | |
91 local usere = getUserElement(xml); | |
92 for k, v in pairs(data) do | |
93 if k == "password" then | |
94 usere.attr.password = v; | |
95 else | |
96 usere.attr[extended..k] = v; | |
97 end | |
98 end | |
99 return setXml(user, self.host, xml); | |
100 else | |
101 return setXml(user, self.host, nil); | 126 return setXml(user, self.host, nil); |
102 end | 127 end |
128 | |
129 local xml = getXml(user, self.host); | |
130 if not xml then xml = createOuterXml(user, self.host); end | |
131 local usere = getUserElement(xml); | |
132 | |
133 local account_properties = set.new(it.to_array(it.keys(data))); | |
134 | |
135 -- Include SCRAM credentials if known | |
136 if account_properties:contains_set(scram_properties) then | |
137 local scram_el = st.stanza("scram-credentials", { xmlns = "urn:xmpp:pie:0#scram", mechanism = "SCRAM-"..scram_hash_name }) | |
138 :text_tag("server-key", hex_to_base64(data.server_key)) | |
139 :text_tag("stored-key", hex_to_base64(data.stored_key)) | |
140 :text_tag("iter-count", ("%d"):format(data.iteration_count)) | |
141 :text_tag("salt", base64.encode(data.salt)); | |
142 xml:add_child(scram_el); | |
143 account_properties:exclude(scram_properties); | |
144 end | |
145 | |
146 -- Include the password if present | |
147 if account_properties:contains("password") then | |
148 usere.attr.password = data.password; | |
149 account_properties:remove("password"); | |
150 end | |
151 | |
152 -- Preserve remaining properties as namespaced attributes | |
153 for property in account_properties do | |
154 usere.attr[extended..property] = data[property]; | |
155 end | |
156 | |
157 return setXml(user, self.host, xml); | |
103 end; | 158 end; |
104 }; | 159 }; |
105 handlers.vcard = { | 160 handlers.vcard = { |
106 get = function(self, user) | 161 get = function(self, user) |
107 user = getUserElement(getXml(user, self.host)); | 162 user = getUserElement(getXml(user, self.host)); |
234 end | 289 end |
235 return true; | 290 return true; |
236 end; | 291 end; |
237 }; | 292 }; |
238 | 293 |
294 -- PEP node configuration/etc. (not items) | |
295 local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner"; | |
296 local lib_pubsub = module:require "pubsub"; | |
297 handlers.pep = { | |
298 get = function (self, user) | |
299 local xml = getXml(user, self.host); | |
300 local user_el = xml and getUserElement(xml); | |
301 if not user_el then | |
302 return nil; | |
303 end | |
304 local nodes = { | |
305 --[[ | |
306 [node_name] = { | |
307 name = node_name; | |
308 config = {}; | |
309 affiliations = {}; | |
310 subscribers = {}; | |
311 }; | |
312 ]] | |
313 }; | |
314 local owner_el = user_el:get_child("pubsub", xmlns_pubsub_owner); | |
315 for node_el in owner_el:childtags() do | |
316 local node_name = node_el.attr.node; | |
317 local node = nodes[node_name]; | |
318 if not node then | |
319 node = { | |
320 name = node_name; | |
321 config = {}; | |
322 affiliations = {}; | |
323 subscribers = {}; | |
324 }; | |
325 nodes[node_name] = node; | |
326 end | |
327 if node_el.name == "configure" then | |
328 local form = node_el:get_child("x", "jabber:x:data"); | |
329 if form then | |
330 node.config = lib_pubsub.node_config_form:data(form); | |
331 end | |
332 elseif node_el.name == "affiliations" then | |
333 for affiliation_el in node_el:childtags("affiliation") do | |
334 local aff_jid = jid_prep(affiliation_el.attr.jid); | |
335 local aff_value = affiliation_el.attr.affiliation; | |
336 if aff_jid and aff_value then | |
337 node.affiliations[aff_jid] = aff_value; | |
338 end | |
339 end | |
340 elseif node_el.name == "subscriptions" then | |
341 for subscription_el in node_el:childtags("subscription") do | |
342 local sub_jid = jid_prep(subscription_el.attr.jid); | |
343 local sub_state = subscription_el.attr.subscription; | |
344 if sub_jid and sub_state == "subscribed" then | |
345 local options; | |
346 local subscription_options_el = subscription_el:get_child("options"); | |
347 if subscription_options_el then | |
348 local options_form = subscription_options_el:get_child("x", "jabber:x:data"); | |
349 if options_form then | |
350 options = lib_pubsub.subscription_options_form:data(options_form); | |
351 end | |
352 end | |
353 node.subscribers[sub_jid] = options or true; | |
354 end | |
355 end | |
356 else | |
357 module:log("warn", "Ignoring unknown pubsub element: %s", node_el.name); | |
358 end | |
359 end | |
360 return nodes; | |
361 end; | |
362 set = function(self, user, data) | |
363 local xml = getXml(user, self.host); | |
364 local user_el = xml and getUserElement(xml); | |
365 if not user_el then | |
366 return true; | |
367 end | |
368 -- Remove existing data, if any | |
369 user_el:remove_children("pubsub", xmlns_pubsub_owner); | |
370 | |
371 -- Generate new data | |
372 local owner_el = st.stanza("pubsub", { xmlns = xmlns_pubsub_owner }); | |
373 | |
374 for node_name, node_data in pairs(data) do | |
375 local configure_el = st.stanza("configure", { node = node_name }) | |
376 :add_child(lib_pubsub.node_config_form:form(node_data.config, "submit")); | |
377 owner_el:add_child(configure_el); | |
378 if node_data.affiliations and next(node_data.affiliations) ~= nil then | |
379 local affiliations_el = st.stanza("affiliations", { node = node_name }); | |
380 for aff_jid, aff_value in pairs(node_data.affiliations) do | |
381 affiliations_el:tag("affiliation", { jid = aff_jid, affiliation = aff_value }):up(); | |
382 end | |
383 owner_el:add_child(affiliations_el); | |
384 end | |
385 if node_data.subscribers and next(node_data.subscribers) ~= nil then | |
386 local subscriptions_el = st.stanza("subscriptions", { node = node_name }); | |
387 for sub_jid, sub_data in pairs(node_data.subscribers) do | |
388 local sub_el = st.stanza("subscription", { jid = sub_jid, subscribed = "subscribed" }); | |
389 if sub_data ~= true then | |
390 local options_form = lib_pubsub.subscription_options_form:form(sub_data, "submit"); | |
391 sub_el:tag("options"):add_child(options_form):up(); | |
392 end | |
393 subscriptions_el:add_child(sub_el); | |
394 end | |
395 owner_el:add_child(subscriptions_el); | |
396 end | |
397 end | |
398 | |
399 user_el:add_child(owner_el); | |
400 | |
401 return setXml(user, self.host, xml); | |
402 end; | |
403 }; | |
404 | |
405 -- PEP items | |
406 local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; | |
407 handlers.pep_ = { | |
408 _stores = function (self, xml) --luacheck: ignore 212/self | |
409 local store_names = set.new(); | |
410 | |
411 local user_el = xml and getUserElement(xml); | |
412 if not user_el then | |
413 return store_names; | |
414 end | |
415 | |
416 -- Locate existing pubsub element, if any | |
417 local pubsub_el = user_el:get_child("pubsub", xmlns_pubsub); | |
418 if not pubsub_el then | |
419 return store_names; | |
420 end | |
421 | |
422 -- Find node items element, if any | |
423 for items_el in pubsub_el:childtags("items") do | |
424 store_names:add("pep_"..items_el.attr.node); | |
425 end | |
426 return store_names; | |
427 end; | |
428 find = function (self, user, query) | |
429 -- query keys: limit, reverse, key (id) | |
430 | |
431 local xml = getXml(user, self.host); | |
432 local user_el = xml and getUserElement(xml); | |
433 if not user_el then | |
434 return nil, "no 227 user element found"; | |
435 end | |
436 | |
437 local node_name = self.datastore:match("^pep_(.+)$"); | |
438 | |
439 -- Locate existing pubsub element, if any | |
440 local pubsub_el = user_el:get_child("pubsub", xmlns_pubsub); | |
441 if not pubsub_el then | |
442 return nil; | |
443 end | |
444 | |
445 -- Find node items element, if any | |
446 local node_items_el; | |
447 for items_el in pubsub_el:childtags("items") do | |
448 if items_el.attr.node == node_name then | |
449 node_items_el = items_el; | |
450 break; | |
451 end | |
452 end | |
453 | |
454 if not node_items_el then | |
455 return nil; | |
456 end | |
457 | |
458 local user_jid = user.."@"..self.host; | |
459 | |
460 local results = {}; | |
461 for item_el in node_items_el:childtags("item") do | |
462 if query and query.key then | |
463 if item_el.attr.id == query.key then | |
464 table.insert(results, { item_el.attr.id, item_el.tags[1], 0, user_jid }); | |
465 break; | |
466 end | |
467 else | |
468 table.insert(results, { item_el.attr.id, item_el.tags[1], 0, user_jid }); | |
469 end | |
470 if query and query.limit and #results >= query.limit then | |
471 break; | |
472 end | |
473 end | |
474 if query and query.reverse then | |
475 return array.reverse(results); | |
476 end | |
477 local i = 0; | |
478 return function () | |
479 i = i + 1; | |
480 local v = results[i]; | |
481 if v == nil then return nil; end | |
482 return unpack(v, 1, 4); | |
483 end; | |
484 end; | |
485 append = function (self, user, key, payload, when, with) --luacheck: ignore 212/when 212/with 212/key | |
486 local xml = getXml(user, self.host); | |
487 local user_el = xml and getUserElement(xml); | |
488 if not user_el then | |
489 return true; | |
490 end | |
491 | |
492 local node_name = self.datastore:match("^pep_(.+)$"); | |
493 | |
494 -- Locate existing pubsub element, if any | |
495 local pubsub_el = user_el:get_child("pubsub", xmlns_pubsub); | |
496 if not pubsub_el then | |
497 pubsub_el = st.stanza("pubsub", { xmlns = xmlns_pubsub }); | |
498 user_el:add_child(pubsub_el); | |
499 end | |
500 | |
501 -- Find node items element, if any | |
502 local node_items_el; | |
503 for items_el in pubsub_el:childtags("items") do | |
504 if items_el.attr.node == node_name then | |
505 node_items_el = items_el; | |
506 break; | |
507 end | |
508 end | |
509 | |
510 if not node_items_el then | |
511 -- Doesn't exist yet, create one | |
512 node_items_el = st.stanza("items", { node = node_name }); | |
513 pubsub_el:add_child(node_items_el); | |
514 end | |
515 | |
516 -- Append item to pubsub_el | |
517 local item_el = st.stanza("item", { id = key }) | |
518 :add_child(payload); | |
519 node_items_el:add_child(item_el); | |
520 | |
521 return setXml(user, self.host, xml); | |
522 end; | |
523 delete = function (self, user, query) | |
524 -- query keys: limit, reverse, key (id) | |
525 | |
526 local xml = getXml(user, self.host); | |
527 local user_el = xml and getUserElement(xml); | |
528 if not user_el then | |
529 return nil, "no 227 user element found"; | |
530 end | |
531 | |
532 local node_name = self.datastore:match("^pep_(.+)$"); | |
533 | |
534 -- Locate existing pubsub element, if any | |
535 local pubsub_el = user_el:get_child("pubsub", xmlns_pubsub); | |
536 if not pubsub_el then | |
537 return nil; | |
538 end | |
539 | |
540 -- Find node items element, if any | |
541 local node_items_el; | |
542 for items_el in pubsub_el:childtags("items") do | |
543 if items_el.attr.node == node_name then | |
544 node_items_el = items_el; | |
545 break; | |
546 end | |
547 end | |
548 | |
549 if not node_items_el then | |
550 return nil; | |
551 end | |
552 | |
553 local results = array(); | |
554 for item_el in pubsub_el:childtags("item") do | |
555 if query and query.key then | |
556 if item_el.attr.id == query.key then | |
557 table.insert(results, item_el); | |
558 break; | |
559 end | |
560 else | |
561 table.insert(results, item_el); | |
562 end | |
563 if query and query.limit and #results >= query.limit then | |
564 break; | |
565 end | |
566 end | |
567 if query and query.truncate then | |
568 results:sub(-query.truncate); | |
569 end | |
570 | |
571 -- Actually remove the matching items | |
572 local delete_keys = set.new(results:map(function (item) return item.attr.id; end)); | |
573 pubsub_el:maptags(function (item_el) | |
574 if delete_keys:contains(item_el.attr.id) then | |
575 return nil; | |
576 end | |
577 return item_el; | |
578 end); | |
579 return setXml(user, self.host, xml); | |
580 end; | |
581 }; | |
582 | |
583 -- MAM archives | |
584 local xmlns_pie_mam = "urn:xmpp:pie:0#mam"; | |
585 handlers.archive = { | |
586 find = function (self, user, query) | |
587 assert(query == nil, "XEP-0313 queries are not supported on XEP-0227 files"); | |
588 | |
589 local xml = getXml(user, self.host); | |
590 local user_el = xml and getUserElement(xml); | |
591 if not user_el then | |
592 return nil, "no 227 user element found"; | |
593 end | |
594 | |
595 -- Locate existing archive element, if any | |
596 local archive_el = user_el:get_child("archive", xmlns_pie_mam); | |
597 if not archive_el then | |
598 return nil; | |
599 end | |
600 | |
601 local user_jid = user.."@"..self.host; | |
602 | |
603 | |
604 local f, s, result_el = archive_el:childtags("result", "urn:xmpp:mam:2"); | |
605 return function () | |
606 result_el = f(s, result_el); | |
607 if not result_el then return nil; end | |
608 | |
609 local id = result_el.attr.id; | |
610 local item = result_el:find("{urn:xmpp:forward:0}forwarded/{jabber:client}message"); | |
611 assert(item, "Invalid stanza in XEP-0227 archive"); | |
612 local when = dt.parse(result_el:find("{urn:xmpp:forward:0}forwarded/{urn:xmpp:delay}delay@stamp")); | |
613 local to_bare, from_bare = jid_bare(item.attr.to), jid_bare(item.attr.from); | |
614 local with = to_bare == user_jid and from_bare or to_bare; | |
615 -- id, item, when, with | |
616 return id, item, when, with; | |
617 end; | |
618 end; | |
619 append = function (self, user, key, payload, when, with) --luacheck: ignore 212/when 212/with 212/key | |
620 local xml = getXml(user, self.host); | |
621 local user_el = xml and getUserElement(xml); | |
622 if not user_el then | |
623 return true; | |
624 end | |
625 | |
626 -- Locate existing archive element, if any | |
627 local archive_el = user_el:get_child("archive", xmlns_pie_mam); | |
628 if not archive_el then | |
629 archive_el = st.stanza("archive", { xmlns = xmlns_pie_mam }); | |
630 user_el:add_child(archive_el); | |
631 end | |
632 | |
633 local item = st.clone(payload); | |
634 item.attr.xmlns = "jabber:client"; | |
635 | |
636 local result_el = st.stanza("result", { xmlns = "urn:xmpp:mam:2", id = key }) | |
637 :tag("forwarded", { xmlns = "urn:xmpp:forward:0" }) | |
638 :tag("delay", { xmlns = "urn:xmpp:delay", stamp = dt.datetime(when) }):up() | |
639 :add_child(item) | |
640 :up(); | |
641 | |
642 -- Append item to archive_el | |
643 archive_el:add_child(result_el); | |
644 | |
645 return setXml(user, self.host, xml); | |
646 end; | |
647 }; | |
239 | 648 |
240 ----------------------------- | 649 ----------------------------- |
241 local driver = {}; | 650 local driver = {}; |
242 | 651 |
652 local function users(self) | |
653 local file_patt = "^.*@"..(self.host:gsub("%p", "%%%1")).."%.xml$"; | |
654 | |
655 local f, s, filename = lfs.dir(prosody.paths.data); | |
656 | |
657 return function () | |
658 filename = f(s, filename); | |
659 while filename and not filename:match(file_patt) do | |
660 filename = f(s, filename); | |
661 end | |
662 if not filename then return nil; end | |
663 return filename:match("^[^@]+"); | |
664 end; | |
665 end | |
666 | |
243 function driver:open(datastore, typ) -- luacheck: ignore 212/self | 667 function driver:open(datastore, typ) -- luacheck: ignore 212/self |
244 if typ and typ ~= "keyval" then return nil, "unsupported-store"; end | 668 if typ and typ ~= "keyval" and typ ~= "archive" then return nil, "unsupported-store"; end |
245 local handler = handlers[datastore]; | 669 local handler = handlers[datastore]; |
670 if not handler and datastore:match("^pep_") then | |
671 handler = handlers.pep_; | |
672 end | |
246 if not handler then return nil, "unsupported-datastore"; end | 673 if not handler then return nil, "unsupported-datastore"; end |
247 local instance = setmetatable({ host = module.host; datastore = datastore; }, { __index = handler }); | 674 local instance = setmetatable({ host = module.host; datastore = datastore; users = users; }, { __index = handler }); |
248 if instance.init then instance:init(); end | 675 if instance.init then instance:init(); end |
249 return instance; | 676 return instance; |
250 end | 677 end |
251 | 678 |
679 local function get_store_names(self, path) | |
680 local stores = set.new(); | |
681 local f, err = io_open(paths.join(prosody.paths.data, path)); | |
682 if not f then | |
683 module:log("warn", "Unable to load XML file for <%s>: %s", "store listing", err); | |
684 return stores; | |
685 end | |
686 module:log("info", "Loaded %s", path); | |
687 local s = f:read("*a"); | |
688 f:close(); | |
689 local xml = parse_xml_real(s); | |
690 for _, handler_funcs in pairs(handlers) do | |
691 if handler_funcs._stores then | |
692 stores:include(handler_funcs._stores(self, xml)); | |
693 end | |
694 end | |
695 return stores; | |
696 end | |
697 | |
698 function driver:stores(username) | |
699 local store_dir = prosody.paths.data; | |
700 | |
701 local mode, err = lfs.attributes(store_dir, "mode"); | |
702 if not mode then | |
703 return function() module:log("debug", "Could not iterate over stores in %s: %s", store_dir, err); end | |
704 end | |
705 | |
706 local file_patt = "^.*@"..(module.host:gsub("%p", "%%%1")).."%.xml$"; | |
707 | |
708 local all_users = username == true; | |
709 | |
710 local store_names = set.new(); | |
711 | |
712 for filename in lfs.dir(prosody.paths.data) do | |
713 if filename:match(file_patt) then | |
714 if all_users or filename == username.."@"..module.host..".xml" then | |
715 store_names:include(get_store_names(self, filename)); | |
716 if not all_users then break; end | |
717 end | |
718 end | |
719 end | |
720 | |
721 return store_names:items(); | |
722 end | |
723 | |
252 module:provides("storage", driver); | 724 module:provides("storage", driver); |