Mercurial > libervia-pubsub
comparison sat_pubsub/bookmark_compat.py @ 491:4e8e8788bc86
Bookmark compatibility layer:
The new `bookmark_compat` module add a compatibility layer between XEP-0048 (with
XEP-0049 private XML storage) and XEP-0402, i.e. it implements the
`urn:xmpp:bookmarks:1#compat` feature.
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 21 Nov 2024 11:03:51 +0100 |
parents | |
children | 468b7cd6c344 |
comparison
equal
deleted
inserted
replaced
490:cab491b1b563 | 491:4e8e8788bc86 |
---|---|
1 #!/usr/bin/env python3 | |
2 # | |
3 # Copyright (c) 2015-2024 Jérôme Poisson | |
4 | |
5 | |
6 # This program is free software: you can redistribute it and/or modify | |
7 # it under the terms of the GNU Affero General Public License as published by | |
8 # the Free Software Foundation, either version 3 of the License, or | |
9 # (at your option) any later version. | |
10 | |
11 # This program is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU Affero General Public License for more details. | |
15 | |
16 # You should have received a copy of the GNU Affero General Public License | |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | |
19 "This module implements a compatibility layer for XEP-0048." | |
20 | |
21 | |
22 from twisted.internet import defer | |
23 from twisted.python import log | |
24 from twisted.words.protocols.jabber import error as jabber_error, jid, xmlstream | |
25 from twisted.words.xish import domish | |
26 from wokkel import disco, iwokkel, pubsub | |
27 from wokkel.iwokkel import IPubSubService | |
28 from zope.interface import implementer | |
29 | |
30 from sat_pubsub import const | |
31 | |
32 from . import error | |
33 | |
34 NS_IQ_PRIVATE = "jabber:iq:private" | |
35 NS_STORAGE_BOOKMARKS = "storage:bookmarks" | |
36 NS_BOOKMARKS2 = "urn:xmpp:bookmarks:1" | |
37 NS_BOOKMARKS_COMPAT = f"{NS_BOOKMARKS2}#compat" | |
38 IQ_PRIVATE_GET = f'/iq[@type="get"]/query[@xmlns="{NS_IQ_PRIVATE}"]' | |
39 IQ_PRIVATE_SET = f'/iq[@type="set"]/query[@xmlns="{NS_IQ_PRIVATE}"]' | |
40 | |
41 | |
42 @implementer(iwokkel.IDisco) | |
43 class BookmarkCompatHandler(disco.DiscoClientProtocol): | |
44 | |
45 def __init__(self, service_jid): | |
46 super().__init__() | |
47 self.backend = None | |
48 | |
49 def connectionInitialized(self): | |
50 for handler in self.parent.handlers: | |
51 if IPubSubService.providedBy(handler): | |
52 self._pubsub_service = handler | |
53 break | |
54 self.backend = self.parent.parent.getServiceNamed("backend") | |
55 self.xmlstream.addObserver(IQ_PRIVATE_GET, self._on_get) | |
56 self.xmlstream.addObserver(IQ_PRIVATE_SET, self._on_set) | |
57 | |
58 def _on_get(self, iq_elt: domish.Element) -> None: | |
59 """Handle incoming legacy IQ GET requests for bookmarks. | |
60 | |
61 This method processes legacy XEP-0048 IQ GET requests. It does some checks and | |
62 then proceeds to return the bookmarks. | |
63 | |
64 @param iq_elt: The incoming IQ element. | |
65 """ | |
66 if not iq_elt.delegated: | |
67 return | |
68 assert self.xmlstream is not None | |
69 iq_elt.handled = True | |
70 from_jid = jid.JID(iq_elt["from"]) | |
71 to_jid = jid.JID(iq_elt["to"]) | |
72 if from_jid.userhostJID() != to_jid: | |
73 msg = ( | |
74 f"{from_jid.userhost()} is not allowed to access private storage of " | |
75 f"{to_jid.userhost()}!" | |
76 ) | |
77 log.msg(f"Hack attempt? {msg}") | |
78 error_elt = jabber_error.StanzaError("forbidden", text=msg).toResponse(iq_elt) | |
79 self.xmlstream.send(error_elt) | |
80 return | |
81 | |
82 query_elt = iq_elt.query | |
83 assert query_elt is not None | |
84 | |
85 storage_elt = query_elt.firstChildElement() | |
86 if ( | |
87 storage_elt is None | |
88 or storage_elt.name != "storage" | |
89 or storage_elt.uri != NS_STORAGE_BOOKMARKS | |
90 ): | |
91 error_elt = jabber_error.StanzaError( | |
92 "not-allowed", | |
93 text=( | |
94 f'"{NS_STORAGE_BOOKMARKS}" is the only private XML storage allowed ' | |
95 "on this server" | |
96 ), | |
97 ).toResponse(iq_elt) | |
98 self.xmlstream.send(error_elt) | |
99 return | |
100 | |
101 defer.ensureDeferred(self.return_bookmarks(iq_elt, to_jid)) | |
102 | |
103 async def return_bookmarks(self, iq_elt: domish.Element, requestor: jid.JID) -> None: | |
104 """Send IQ result for bookmark request on private XML. | |
105 | |
106 Retrieve bookmark from Bookmarks2 PEP node, convert them to private XML | |
107 XEP-0048 format, and send the IQ result. | |
108 @param iq_elt: The incoming IQ element. | |
109 @param requestor: The JID of the entity requesting the bookmarks. | |
110 """ | |
111 assert self.backend is not None | |
112 assert self.xmlstream is not None | |
113 items, __ = await self.backend.getItems( | |
114 NS_BOOKMARKS2, requestor, requestor, ext_data={"pep": True} | |
115 ) | |
116 iq_result_elt = xmlstream.toResponse(iq_elt, "result") | |
117 | |
118 query_elt = iq_result_elt.addElement((NS_IQ_PRIVATE, "query")) | |
119 storage_elt = query_elt.addElement((NS_STORAGE_BOOKMARKS, "storage")) | |
120 | |
121 # For simply add all bookmarks to get XEP-0048 private XML elements. | |
122 for item_elt in items: | |
123 conference_elt = next(item_elt.elements(NS_BOOKMARKS2, "conference"), None) | |
124 if conference_elt is not None: | |
125 # The namespace is not the same for XEP-0048 | |
126 conference_elt.uri = NS_STORAGE_BOOKMARKS | |
127 for elt in conference_elt.children: | |
128 elt.uri = NS_STORAGE_BOOKMARKS | |
129 conference_elt["jid"] = item_elt["id"] | |
130 storage_elt.addChild(conference_elt) | |
131 else: | |
132 log.msg( | |
133 "Warning: Unexpectedly missing conference element: " | |
134 f"{item_elt.toXml()}" | |
135 ) | |
136 | |
137 self.xmlstream.send(iq_result_elt) | |
138 | |
139 def _on_set(self, iq_elt: domish.Element) -> None: | |
140 if not iq_elt.delegated: | |
141 return | |
142 assert self.xmlstream is not None | |
143 iq_elt.handled = True | |
144 from_jid = jid.JID(iq_elt["from"]) | |
145 to_jid = jid.JID(iq_elt["to"]) | |
146 if from_jid.userhostJID() != to_jid: | |
147 msg = ( | |
148 f"{from_jid.userhost()} is not allowed to access private storage of " | |
149 f"{to_jid.userhost()}!" | |
150 ) | |
151 log.msg(f"Hack attempt? {msg}") | |
152 error_elt = jabber_error.StanzaError("forbidden", text=msg).toResponse(iq_elt) | |
153 self.xmlstream.send(error_elt) | |
154 return | |
155 query_elt = iq_elt.query | |
156 assert query_elt is not None | |
157 storage_elt = query_elt.firstChildElement() | |
158 if ( | |
159 storage_elt is None | |
160 or storage_elt.name != "storage" | |
161 or storage_elt.uri != NS_STORAGE_BOOKMARKS | |
162 ): | |
163 error_elt = jabber_error.StanzaError( | |
164 "not-allowed", | |
165 text=( | |
166 f'"{NS_STORAGE_BOOKMARKS}" is the only private XML storage allowed ' | |
167 "on this server" | |
168 ), | |
169 ).toResponse(iq_elt) | |
170 self.xmlstream.send(error_elt) | |
171 return | |
172 defer.ensureDeferred(self.on_set(iq_elt, from_jid)) | |
173 | |
174 async def publish_bookmarks( | |
175 self, iq_elt: domish.Element, items: list[domish.Element], requestor: jid.JID | |
176 ) -> None: | |
177 """Publish bookmarks on Bookmarks2 PEP node""" | |
178 assert self.backend is not None | |
179 assert self.xmlstream is not None | |
180 try: | |
181 await self.backend.publish( | |
182 NS_BOOKMARKS2, | |
183 items, | |
184 requestor, | |
185 options={const.OPT_ACCESS_MODEL: const.VAL_AMODEL_WHITELIST}, | |
186 pep=True, | |
187 recipient=requestor, | |
188 ) | |
189 except error.NodeNotFound: | |
190 # The node doesn't exist, we create it | |
191 await self.backend.createNode( | |
192 NS_BOOKMARKS2, | |
193 requestor, | |
194 options={const.OPT_ACCESS_MODEL: const.VAL_AMODEL_WHITELIST}, | |
195 pep=True, | |
196 recipient=requestor, | |
197 ) | |
198 await self.publish_bookmarks(iq_elt, items, requestor) | |
199 except Exception as e: | |
200 log.err(f"Error while publishing converted bookmarks: {e}") | |
201 error_elt = jabber_error.StanzaError( | |
202 "internal-server-error", | |
203 text=(f"Something went wrong while publishing the bookmark items: {e}"), | |
204 ).toResponse(iq_elt) | |
205 self.xmlstream.send(error_elt) | |
206 raise e | |
207 | |
208 async def on_set(self, iq_elt: domish.Element, requestor: jid.JID) -> None: | |
209 """Handle IQ set request for bookmarks on private XML. | |
210 | |
211 This method processes an IQ set request to update bookmarks stored in private XML. | |
212 It extracts conference elements from the request, transforms them into XEP-0402 | |
213 format, and publishes them to XEP-0402 PEP node. | |
214 | |
215 @param iq_elt: The incoming IQ element containing the set request. | |
216 @param requestor: The JID of the entity making the request. | |
217 """ | |
218 # TODO: We should check if items already exist in Bookmarks 2 and avoid updating | |
219 # them if there is no change. However, considering that XEP-0048 is deprecated and | |
220 # active implementations without XEP-0402 are rare, it might not be worth the | |
221 # trouble. | |
222 assert self.backend is not None | |
223 assert self.xmlstream is not None | |
224 query_elt = iq_elt.query | |
225 assert query_elt is not None | |
226 # <storage> presence has been checked in ``_on_set``, we know that we have it | |
227 # here. | |
228 storage_elt = next(query_elt.elements(NS_STORAGE_BOOKMARKS, "storage")) | |
229 | |
230 items = [] | |
231 for conference_elt in storage_elt.elements(NS_STORAGE_BOOKMARKS, "conference"): | |
232 item_elt = domish.Element((pubsub.NS_PUBSUB, "item")) | |
233 try: | |
234 item_elt["id"] = conference_elt["jid"] | |
235 except AttributeError: | |
236 log.msg( | |
237 "Warning: ignoring <conference> with missing jid: " | |
238 f"conference_elt.toXml()" | |
239 ) | |
240 continue | |
241 new_conference_elt = item_elt.addElement((NS_BOOKMARKS2, "conference")) | |
242 for attr, value in conference_elt.attributes.items(): | |
243 if attr == "jid": | |
244 continue | |
245 new_conference_elt[attr] = value | |
246 for child in conference_elt.children: | |
247 new_child = domish.Element((NS_BOOKMARKS2, child.name)) | |
248 new_child.addContent(str(child)) | |
249 new_conference_elt.addChild(new_child) | |
250 items.append(item_elt) | |
251 | |
252 await self.publish_bookmarks(iq_elt, items, requestor) | |
253 | |
254 iq_result_elt = xmlstream.toResponse(iq_elt, "result") | |
255 self.xmlstream.send(iq_result_elt) | |
256 | |
257 def getDiscoInfo(self, requestor, service, nodeIdentifier=""): | |
258 return [ | |
259 disco.DiscoFeature(NS_IQ_PRIVATE), | |
260 disco.DiscoFeature(NS_BOOKMARKS_COMPAT), | |
261 ] | |
262 | |
263 def getDiscoItems(self, requestor, service, nodeIdentifier=""): | |
264 return [] |