plugins/mod_bookmarks.lua
changeset 12152 b63bb2c4b6d9
child 12153 bbbf0dd90b6d
equal deleted inserted replaced
12151:02481502c3dc 12152:b63bb2c4b6d9
       
     1 local mm = require "core.modulemanager";
       
     2 if mm.get_modules_for_host(module.host):contains("bookmarks2") then
       
     3 	error("mod_bookmarks and mod_bookmarks2 are conflicting, please disable one of them.", 0);
       
     4 end
       
     5 
       
     6 local st = require "util.stanza";
       
     7 local jid_split = require "util.jid".split;
       
     8 
       
     9 local mod_pep = module:depends "pep";
       
    10 local private_storage = module:open_store("private", "map");
       
    11 
       
    12 local namespace = "urn:xmpp:bookmarks:1";
       
    13 local namespace_private = "jabber:iq:private";
       
    14 local namespace_legacy = "storage:bookmarks";
       
    15 
       
    16 local default_options = {
       
    17 	["persist_items"] = true;
       
    18 	["max_items"] = "max";
       
    19 	["send_last_published_item"] = "never";
       
    20 	["access_model"] = "whitelist";
       
    21 };
       
    22 
       
    23 if not mod_pep.check_node_config(nil, nil, default_options) then
       
    24 	-- 0.11 or earlier not supporting max_items="max" trows an error here
       
    25 	module:log("debug", "Setting max_items=pep_max_items because 'max' is not supported in this version");
       
    26 	default_options["max_items"] = module:get_option_number("pep_max_items", 256);
       
    27 end
       
    28 
       
    29 module:hook("account-disco-info", function (event)
       
    30 	-- This Time it’s Serious!
       
    31 	event.reply:tag("feature", { var = namespace.."#compat" }):up();
       
    32 	event.reply:tag("feature", { var = namespace.."#compat-pep" }):up();
       
    33 end);
       
    34 
       
    35 -- This must be declared on the domain JID, not the account JID.  Note that
       
    36 -- this isn’t defined in the XEP.
       
    37 module:add_feature(namespace_private);
       
    38 
       
    39 local function generate_legacy_storage(items)
       
    40 	local storage = st.stanza("storage", { xmlns = namespace_legacy });
       
    41 	for _, item_id in ipairs(items) do
       
    42 		local item = items[item_id];
       
    43 		local bookmark = item:get_child("conference", namespace);
       
    44 		local conference = st.stanza("conference", {
       
    45 			jid = item.attr.id,
       
    46 			name = bookmark.attr.name,
       
    47 			autojoin = bookmark.attr.autojoin,
       
    48 		});
       
    49 		local nick = bookmark:get_child_text("nick");
       
    50 		if nick ~= nil then
       
    51 			conference:text_tag("nick", nick):up();
       
    52 		end
       
    53 		local password = bookmark:get_child_text("password");
       
    54 		if password ~= nil then
       
    55 			conference:text_tag("password", password):up();
       
    56 		end
       
    57 		storage:add_child(conference);
       
    58 	end
       
    59 
       
    60 	return storage;
       
    61 end
       
    62 
       
    63 local function on_retrieve_legacy_pep(event)
       
    64 	local stanza, session = event.stanza, event.origin;
       
    65 	local pubsub = stanza:get_child("pubsub", "http://jabber.org/protocol/pubsub");
       
    66 	if pubsub == nil then
       
    67 		return;
       
    68 	end
       
    69 
       
    70 	local items = pubsub:get_child("items");
       
    71 	if items == nil then
       
    72 		return;
       
    73 	end
       
    74 
       
    75 	local node = items.attr.node;
       
    76 	if node ~= namespace_legacy then
       
    77 		return;
       
    78 	end
       
    79 
       
    80 	local username = session.username;
       
    81 	local jid = username.."@"..session.host;
       
    82 	local service = mod_pep.get_pep_service(username);
       
    83 	local ok, ret = service:get_items(namespace, session.full_jid);
       
    84 	if not ok then
       
    85 		module:log("error", "Failed to retrieve PEP bookmarks of %s: %s", jid, ret);
       
    86 		session.send(st.error_reply(stanza, "cancel", ret, "Failed to retrive bookmarks from PEP"));
       
    87 		return true;
       
    88 	end
       
    89 
       
    90 	local storage = generate_legacy_storage(ret);
       
    91 
       
    92 	module:log("debug", "Sending back legacy PEP for %s: %s", jid, storage);
       
    93 	session.send(st.reply(stanza)
       
    94 		:tag("pubsub", {xmlns = "http://jabber.org/protocol/pubsub"})
       
    95 			:tag("items", {node = namespace_legacy})
       
    96 				:tag("item", {id = "current"})
       
    97 					:add_child(storage));
       
    98 	return true;
       
    99 end
       
   100 
       
   101 local function on_retrieve_private_xml(event)
       
   102 	local stanza, session = event.stanza, event.origin;
       
   103 	local query = stanza:get_child("query", namespace_private);
       
   104 	if query == nil then
       
   105 		return;
       
   106 	end
       
   107 
       
   108 	local bookmarks = query:get_child("storage", namespace_legacy);
       
   109 	if bookmarks == nil then
       
   110 		return;
       
   111 	end
       
   112 
       
   113 	module:log("debug", "Getting private bookmarks: %s", bookmarks);
       
   114 
       
   115 	local username = session.username;
       
   116 	local jid = username.."@"..session.host;
       
   117 	local service = mod_pep.get_pep_service(username);
       
   118 	local ok, ret = service:get_items(namespace, session.full_jid);
       
   119 	if not ok then
       
   120 		if ret == "item-not-found" then
       
   121 			module:log("debug", "Got no PEP bookmarks item for %s, returning empty private bookmarks", jid);
       
   122 			session.send(st.reply(stanza):add_child(query));
       
   123 		else
       
   124 			module:log("error", "Failed to retrieve PEP bookmarks of %s: %s", jid, ret);
       
   125 			session.send(st.error_reply(stanza, "cancel", ret, "Failed to retrive bookmarks from PEP"));
       
   126 		end
       
   127 		return true;
       
   128 	end
       
   129 
       
   130 	local storage = generate_legacy_storage(ret);
       
   131 
       
   132 	module:log("debug", "Sending back private for %s: %s", jid, storage);
       
   133 	session.send(st.reply(stanza):query(namespace_private):add_child(storage));
       
   134 	return true;
       
   135 end
       
   136 
       
   137 local function compare_bookmark2(a, b)
       
   138 	if a == nil or b == nil then
       
   139 		return false;
       
   140 	end
       
   141 	local a_conference = a:get_child("conference", namespace);
       
   142 	local b_conference = b:get_child("conference", namespace);
       
   143 	local a_nick = a_conference:get_child_text("nick");
       
   144 	local b_nick = b_conference:get_child_text("nick");
       
   145 	local a_password = a_conference:get_child_text("password");
       
   146 	local b_password = b_conference:get_child_text("password");
       
   147 	return (a.attr.id == b.attr.id and
       
   148 	        a_conference.attr.name == b_conference.attr.name and
       
   149 	        a_conference.attr.autojoin == b_conference.attr.autojoin and
       
   150 	        a_nick == b_nick and
       
   151 	        a_password == b_password);
       
   152 end
       
   153 
       
   154 local function publish_to_pep(jid, bookmarks, synchronise)
       
   155 	local service = mod_pep.get_pep_service(jid_split(jid));
       
   156 
       
   157 	if #bookmarks.tags == 0 then
       
   158 		if synchronise then
       
   159 			-- If we set zero legacy bookmarks, purge the bookmarks 2 node.
       
   160 			module:log("debug", "No bookmark in the set, purging instead.");
       
   161 			return service:purge(namespace, jid, true);
       
   162 		else
       
   163 			return true;
       
   164 		end
       
   165 	end
       
   166 
       
   167 	-- Retrieve the current bookmarks2.
       
   168 	module:log("debug", "Retrieving the current bookmarks 2.");
       
   169 	local has_bookmarks2, ret = service:get_items(namespace, jid);
       
   170 	local bookmarks2;
       
   171 	if not has_bookmarks2 and ret == "item-not-found" then
       
   172 		module:log("debug", "Got item-not-found, assuming it was empty until now, creating.");
       
   173 		local ok, err = service:create(namespace, jid, default_options);
       
   174 		if not ok then
       
   175 			module:log("error", "Creating bookmarks 2 node failed: %s", err);
       
   176 			return ok, err;
       
   177 		end
       
   178 		bookmarks2 = {};
       
   179 	elseif not has_bookmarks2 then
       
   180 		module:log("debug", "Got %s error, aborting.", ret);
       
   181 		return false, ret;
       
   182 	else
       
   183 		module:log("debug", "Got existing bookmarks2.");
       
   184 		bookmarks2 = ret;
       
   185 	end
       
   186 
       
   187 	-- Get a list of all items we may want to remove.
       
   188 	local to_remove = {};
       
   189 	for i in ipairs(bookmarks2) do
       
   190 		to_remove[bookmarks2[i]] = true;
       
   191 	end
       
   192 
       
   193 	for bookmark in bookmarks:childtags("conference", namespace_legacy) do
       
   194 		-- Create the new conference element by copying everything from the legacy one.
       
   195 		local conference = st.stanza("conference", {
       
   196 			xmlns = namespace,
       
   197 			name = bookmark.attr.name,
       
   198 			autojoin = bookmark.attr.autojoin,
       
   199 		});
       
   200 		local nick = bookmark:get_child_text("nick");
       
   201 		if nick ~= nil then
       
   202 			conference:text_tag("nick", nick):up();
       
   203 		end
       
   204 		local password = bookmark:get_child_text("password");
       
   205 		if password ~= nil then
       
   206 			conference:text_tag("password", password):up();
       
   207 		end
       
   208 
       
   209 		-- Create its wrapper.
       
   210 		local item = st.stanza("item", { xmlns = "http://jabber.org/protocol/pubsub", id = bookmark.attr.jid })
       
   211 			:add_child(conference);
       
   212 
       
   213 		-- Then publish it only if it’s a new one or updating a previous one.
       
   214 		if compare_bookmark2(item, bookmarks2[bookmark.attr.jid]) then
       
   215 			module:log("debug", "Item %s identical to the previous one, skipping.", item.attr.id);
       
   216 			to_remove[bookmark.attr.jid] = nil;
       
   217 		else
       
   218 			if bookmarks2[bookmark.attr.jid] == nil then
       
   219 				module:log("debug", "Item %s not existing previously, publishing.", item.attr.id);
       
   220 			else
       
   221 				module:log("debug", "Item %s different from the previous one, publishing.", item.attr.id);
       
   222 				to_remove[bookmark.attr.jid] = nil;
       
   223 			end
       
   224 			local ok, err = service:publish(namespace, jid, bookmark.attr.jid, item, default_options);
       
   225 			if not ok then
       
   226 				module:log("error", "Publishing item %s failed: %s", item.attr.id, err);
       
   227 				return ok, err;
       
   228 			end
       
   229 		end
       
   230 	end
       
   231 
       
   232 	-- Now handle retracting items that have been removed.
       
   233 	if synchronise then
       
   234 		for id in pairs(to_remove) do
       
   235 			module:log("debug", "Item %s removed from bookmarks.", id);
       
   236 			local ok, err = service:retract(namespace, jid, id, st.stanza("retract", { id = id }));
       
   237 			if not ok then
       
   238 				module:log("error", "Retracting item %s failed: %s", id, err);
       
   239 				return ok, err;
       
   240 			end
       
   241 		end
       
   242 	end
       
   243 	return true;
       
   244 end
       
   245 
       
   246 -- Synchronise legacy PEP to PEP.
       
   247 local function on_publish_legacy_pep(event)
       
   248 	local stanza, session = event.stanza, event.origin;
       
   249 	local pubsub = stanza:get_child("pubsub", "http://jabber.org/protocol/pubsub");
       
   250 	if pubsub == nil then
       
   251 		return;
       
   252 	end
       
   253 
       
   254 	local publish = pubsub:get_child("publish");
       
   255 	if publish == nil or publish.attr.node ~= namespace_legacy then
       
   256 		return;
       
   257 	end
       
   258 
       
   259 	local item = publish:get_child("item");
       
   260 	if item == nil then
       
   261 		return;
       
   262 	end
       
   263 
       
   264 	-- Here we ignore the item id, it’ll be generated as 'current' anyway.
       
   265 
       
   266 	local bookmarks = item:get_child("storage", namespace_legacy);
       
   267 	if bookmarks == nil then
       
   268 		return;
       
   269 	end
       
   270 
       
   271 	-- We also ignore the publish-options.
       
   272 
       
   273 	module:log("debug", "Legacy PEP bookmarks set by client, publishing to PEP.");
       
   274 
       
   275 	local ok, err = publish_to_pep(session.full_jid, bookmarks, true);
       
   276 	if not ok then
       
   277 		module:log("error", "Failed to publish to PEP bookmarks for %s@%s: %s", session.username, session.host, err);
       
   278 		session.send(st.error_reply(stanza, "cancel", "internal-server-error", "Failed to store bookmarks to PEP"));
       
   279 		return true;
       
   280 	end
       
   281 
       
   282 	session.send(st.reply(stanza));
       
   283 	return true;
       
   284 end
       
   285 
       
   286 -- Synchronise Private XML to PEP.
       
   287 local function on_publish_private_xml(event)
       
   288 	local stanza, session = event.stanza, event.origin;
       
   289 	local query = stanza:get_child("query", namespace_private);
       
   290 	if query == nil then
       
   291 		return;
       
   292 	end
       
   293 
       
   294 	local bookmarks = query:get_child("storage", namespace_legacy);
       
   295 	if bookmarks == nil then
       
   296 		return;
       
   297 	end
       
   298 
       
   299 	module:log("debug", "Private bookmarks set by client, publishing to PEP.");
       
   300 
       
   301 	local ok, err = publish_to_pep(session.full_jid, bookmarks, true);
       
   302 	if not ok then
       
   303 		module:log("error", "Failed to publish to PEP bookmarks for %s@%s: %s", session.username, session.host, err);
       
   304 		session.send(st.error_reply(stanza, "cancel", "internal-server-error", "Failed to store bookmarks to PEP"));
       
   305 		return true;
       
   306 	end
       
   307 
       
   308 	session.send(st.reply(stanza));
       
   309 	return true;
       
   310 end
       
   311 
       
   312 local function migrate_legacy_bookmarks(event)
       
   313 	local session = event.session;
       
   314 	local username = session.username;
       
   315 	local service = mod_pep.get_pep_service(username);
       
   316 	local jid = username.."@"..session.host;
       
   317 
       
   318 	local ok, ret = service:get_items(namespace_legacy, session.full_jid);
       
   319 	if ok then
       
   320 		module:log("debug", "Legacy PEP bookmarks found for %s, migrating.", jid);
       
   321 		local failed = false;
       
   322 		for _, item_id in ipairs(ret) do
       
   323 			local item = ret[item_id];
       
   324 			if item.attr.id ~= "current" then
       
   325 				module:log("warn", "Legacy PEP bookmarks for %s isn’t using 'current' as its id: %s", jid, item.attr.id);
       
   326 			end
       
   327 			local bookmarks = item:get_child("storage", namespace_legacy);
       
   328 			module:log("debug", "Got legacy PEP bookmarks of %s: %s", jid, bookmarks);
       
   329 
       
   330 			local ok, err = publish_to_pep(session.full_jid, bookmarks, false);
       
   331 			if not ok then
       
   332 				module:log("error", "Failed to store legacy PEP bookmarks to bookmarks 2 for %s, aborting migration: %s", jid, err);
       
   333 				failed = true;
       
   334 				break;
       
   335 			end
       
   336 		end
       
   337 		if not failed then
       
   338 			module:log("debug", "Successfully migrated legacy PEP bookmarks of %s to bookmarks 2, attempting deletion of the node.", jid);
       
   339 			local ok, err = service:delete(namespace_legacy, jid);
       
   340 			if not ok then
       
   341 				module:log("error", "Failed to delete legacy PEP bookmarks for %s: %s", jid, err);
       
   342 			end
       
   343 		end
       
   344 	end
       
   345 
       
   346 	local data, err = private_storage:get(username, "storage:storage:bookmarks");
       
   347 	if not data then
       
   348 		module:log("debug", "No existing legacy bookmarks for %s, migration already done: %s", jid, err);
       
   349 		local ok, ret2 = service:get_items(namespace, session.full_jid);
       
   350 		if not ok or not ret2 then
       
   351 			module:log("debug", "Additionally, no bookmarks 2 were existing for %s, assuming empty.", jid);
       
   352 			module:fire_event("bookmarks/empty", { session = session });
       
   353 		end
       
   354 		return;
       
   355 	end
       
   356 	local bookmarks = st.deserialize(data);
       
   357 	module:log("debug", "Got legacy bookmarks of %s: %s", jid, bookmarks);
       
   358 
       
   359 	module:log("debug", "Going to store legacy bookmarks to bookmarks 2 %s.", jid);
       
   360 	local ok, err = publish_to_pep(session.full_jid, bookmarks, false);
       
   361 	if not ok then
       
   362 		module:log("error", "Failed to store legacy bookmarks to bookmarks 2 for %s, aborting migration: %s", jid, err);
       
   363 		return;
       
   364 	end
       
   365 	module:log("debug", "Stored legacy bookmarks to bookmarks 2 for %s.", jid);
       
   366 
       
   367 	local ok, err = private_storage:set(username, "storage:storage:bookmarks", nil);
       
   368 	if not ok then
       
   369 		module:log("error", "Failed to remove legacy bookmarks of %s: %s", jid, err);
       
   370 		return;
       
   371 	end
       
   372 	module:log("debug", "Removed legacy bookmarks of %s, migration done!", jid);
       
   373 end
       
   374 
       
   375 local function on_node_created(event)
       
   376 	local service, node, actor = event.service, event.node, event.actor;
       
   377 	if node ~= namespace_legacy then
       
   378 		return;
       
   379 	end
       
   380 
       
   381 	module:log("debug", "Something tried to create legacy PEP bookmarks for %s.", actor);
       
   382 	local ok, err = service:delete(namespace_legacy, actor);
       
   383 	if not ok then
       
   384 		module:log("error", "Failed to delete legacy PEP bookmarks for %s: %s", actor, err);
       
   385 	end
       
   386 	module:log("debug", "Legacy PEP bookmarks node of %s deleted.", actor);
       
   387 end
       
   388 
       
   389 module:hook("iq/bare/jabber:iq:private:query", function (event)
       
   390 	if event.stanza.attr.type == "get" then
       
   391 		return on_retrieve_private_xml(event);
       
   392 	else
       
   393 		return on_publish_private_xml(event);
       
   394 	end
       
   395 end, 1);
       
   396 module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function (event)
       
   397 	if event.stanza.attr.type == "get" then
       
   398 		return on_retrieve_legacy_pep(event);
       
   399 	else
       
   400 		return on_publish_legacy_pep(event);
       
   401 	end
       
   402 end, 1);
       
   403 module:hook("resource-bind", migrate_legacy_bookmarks);
       
   404 module:handle_items("pep-service", function (event)
       
   405 	local service = event.item.service;
       
   406 	module:hook_object_event(service.events, "node-created", on_node_created);
       
   407 end, function () end, true);