Mercurial > libervia-backend
comparison src/plugins/plugin_xep_0329.py @ 2503:c0bec8bac2b5
XEP-0329: implementation of File Information Sharing:
the plugin do an implementation for client and component.
For client, any directory from local file can be shared.
For component file are stored using new file handling mechanism from SàT.
Permission is handled, using "public" and "whitelist" for now.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 28 Feb 2018 18:28:39 +0100 |
parents | |
children | 025afb04c10b |
comparison
equal
deleted
inserted
replaced
2502:7ad5f2c4e34a | 2503:c0bec8bac2b5 |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SAT plugin for File Information Sharing (XEP-0329) | |
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 from sat.core.i18n import _ | |
21 from sat.core import exceptions | |
22 from sat.core.constants import Const as C | |
23 from sat.core.log import getLogger | |
24 log = getLogger(__name__) | |
25 from sat.tools import stream | |
26 from sat.tools.common import regex | |
27 from wokkel import disco, iwokkel | |
28 from zope.interface import implements | |
29 from twisted.words.protocols.jabber import xmlstream | |
30 from twisted.words.protocols.jabber import jid | |
31 from twisted.words.protocols.jabber import error as jabber_error | |
32 from twisted.internet import defer | |
33 import mimetypes | |
34 import json | |
35 import os | |
36 | |
37 | |
38 PLUGIN_INFO = { | |
39 C.PI_NAME: "File Information Sharing", | |
40 C.PI_IMPORT_NAME: "XEP-0329", | |
41 C.PI_TYPE: "XEP", | |
42 C.PI_MODES: C.PLUG_MODE_BOTH, | |
43 C.PI_PROTOCOLS: ["XEP-0329"], | |
44 C.PI_DEPENDENCIES: ["XEP-0234", "XEP-0300"], | |
45 C.PI_MAIN: "XEP_0329", | |
46 C.PI_HANDLER: "yes", | |
47 C.PI_DESCRIPTION: _(u"""Implementation of File Information Sharing""") | |
48 } | |
49 | |
50 NS_FIS = 'urn:xmpp:fis:0' | |
51 | |
52 IQ_FIS_REQUEST = C.IQ_GET + '/query[@xmlns="' + NS_FIS + '"]' | |
53 SINGLE_FILES_DIR = u"files" | |
54 TYPE_VIRTUAL= u'virtual' | |
55 TYPE_PATH = u'path' | |
56 SHARE_TYPES = (TYPE_PATH, TYPE_VIRTUAL) | |
57 KEY_TYPE = u'type' | |
58 | |
59 | |
60 class ShareNode(object): | |
61 """node containing directory or files to share, virtual or real""" | |
62 host = None | |
63 | |
64 def __init__(self, name, parent, type_, access, path=None): | |
65 assert type_ in SHARE_TYPES | |
66 if name is not None: | |
67 if name == u'..' or u'/' in name or u'\\' in name: | |
68 log.warning(_(u'path change chars found in name [{name}], hack attempt?').format(name=name)) | |
69 if name == u'..': | |
70 name = u'--' | |
71 else: | |
72 name = regex.pathEscape(name) | |
73 self.name = name | |
74 self.children = {} | |
75 self.type = type_ | |
76 self.access = {} if access is None else access | |
77 assert isinstance(self.access, dict) | |
78 self.persistent = False | |
79 self.parent = None | |
80 if parent is not None: | |
81 assert name | |
82 parent.addChild(self) | |
83 else: | |
84 assert name is None | |
85 if path is not None: | |
86 if type_ != TYPE_PATH: | |
87 raise exceptions.InternalError(_(u"path can only be set on path nodes")) | |
88 self._path = path | |
89 | |
90 @property | |
91 def path(self): | |
92 return self._path | |
93 | |
94 def __getitem__(self, key): | |
95 return self.children[key] | |
96 | |
97 def __contains__(self, item): | |
98 return self.children.__contains__(item) | |
99 | |
100 def __iter__(self): | |
101 return self.children.__iter__ | |
102 | |
103 def iteritems(self): | |
104 return self.children.iteritems() | |
105 | |
106 def getOrCreate(self, name, type_=TYPE_VIRTUAL, access=None): | |
107 """get a node or create a virtual one and return it""" | |
108 if access is None: | |
109 access = {C.ACCESS_PERM_READ: {KEY_TYPE: C.ACCESS_TYPE_PUBLIC}} | |
110 try: | |
111 return self.children[name] | |
112 except KeyError: | |
113 node = ShareNode(name, self, type_=type_, access=access) | |
114 return node | |
115 | |
116 def addChild(self, node): | |
117 if node.parent is not None: | |
118 raise exceptions.ConflictError(_(u"a node can't have several parents")) | |
119 node.parent = self | |
120 self.children[node.name] = node | |
121 | |
122 def _checkNodePermission(self, client, node, perms, peer_jid): | |
123 """Check access to this node for peer_jid | |
124 | |
125 @param node(SharedNode): node to check access | |
126 @param perms(unicode): permissions to check, iterable of C.ACCESS_PERM_* | |
127 @param peer_jid(jid.JID): entity which try to access the node | |
128 @return (bool): True if entity can access | |
129 """ | |
130 file_data = {u'access':self.access, u'owner': client.jid.userhostJID()} | |
131 try: | |
132 self.host.memory.checkFilePermission(file_data, peer_jid, perms) | |
133 except exceptions.PermissionError: | |
134 return False | |
135 else: | |
136 return True | |
137 | |
138 def checkPermissions(self, client, peer_jid, perms=(C.ACCESS_PERM_READ,), check_parents=True): | |
139 """check that peer_jid can access this node and all its parents | |
140 | |
141 @param peer_jid(jid.JID): entrity trying to access the node | |
142 @param perms(unicode): permissions to check, iterable of C.ACCESS_PERM_* | |
143 @param check_parents(bool): if True, access of all parents of this node will be checked too | |
144 @return (bool): True if entity can access this node | |
145 """ | |
146 peer_jid = peer_jid.userhostJID() | |
147 if peer_jid == client.jid.userhostJID(): | |
148 return True | |
149 | |
150 parent = self | |
151 while parent != None: | |
152 if not self._checkNodePermission(client, parent, perms, peer_jid): | |
153 return False | |
154 parent = parent.parent | |
155 | |
156 return True | |
157 | |
158 @staticmethod | |
159 def find(client, path, peer_jid, perms=(C.ACCESS_PERM_READ,)): | |
160 """find node corresponding to a path | |
161 | |
162 @param path(unicode): path to the requested file or directory | |
163 @param peer_jid(jid.JID): entity trying to find the node | |
164 used to check permission | |
165 @return (dict, unicode): shared data, remaining path | |
166 @raise exceptions.PermissionError: user can't access this file | |
167 @raise exceptions.DataError: path is invalid | |
168 @raise NotFound: path lead to a non existing file/directory | |
169 """ | |
170 path_elts = filter(None, path.split(u'/')) | |
171 | |
172 if u'..' in path_elts: | |
173 log.warning(_(u'parent dir ("..") found in path, hack attempt? path is {path} [{profile}]').format( | |
174 path=path, profile=client.profile)) | |
175 raise exceptions.PermissionError(u"illegal path elements") | |
176 | |
177 if not path_elts: | |
178 raise exceptions.DataError(_(u'path is invalid: {path}').format(path=path)) | |
179 | |
180 node = client._XEP_0329_root_node | |
181 | |
182 while path_elts: | |
183 if node.type == TYPE_VIRTUAL: | |
184 try: | |
185 node = node[path_elts.pop(0)] | |
186 except KeyError: | |
187 raise exceptions.NotFound | |
188 elif node.type == TYPE_PATH: | |
189 break | |
190 | |
191 if not node.checkPermissions(client, peer_jid, perms = perms): | |
192 raise exceptions.PermissionError(u"permission denied") | |
193 | |
194 return node, u'/'.join(path_elts) | |
195 | |
196 | |
197 class XEP_0329(object): | |
198 | |
199 def __init__(self, host): | |
200 log.info(_("File Information Sharing initialization")) | |
201 self.host = host | |
202 ShareNode.host = host | |
203 self._h = host.plugins['XEP-0300'] | |
204 self._jf = host.plugins['XEP-0234'] | |
205 host.bridge.addMethod("FISList", ".plugin", in_sign='ssa{ss}s', out_sign='aa{ss}', method=self._listFiles, async=True) | |
206 host.bridge.addMethod("FISSharePath", ".plugin", in_sign='ssss', out_sign='s', method=self._sharePath) | |
207 host.trigger.add("XEP-0234_fileSendingRequest", self._fileSendingRequestTrigger) | |
208 | |
209 def getHandler(self, client): | |
210 return XEP_0329_handler(self) | |
211 | |
212 def profileConnected(self, client): | |
213 if not client.is_component: | |
214 client._XEP_0329_root_node = ShareNode(None, None, TYPE_VIRTUAL, {C.ACCESS_PERM_READ: {KEY_TYPE: C.ACCESS_TYPE_PUBLIC}}) | |
215 client._XEP_0329_names_data = {} # name to share map | |
216 | |
217 def _fileSendingRequestTrigger(self, client, session, content_data, content_name, file_data, file_elt): | |
218 """this trigger check that a requested file is available, and fill suitable data if so | |
219 | |
220 path and name are used to retrieve the file. If path is missing, we try our luck with known names | |
221 """ | |
222 if client.is_component: | |
223 return True, None | |
224 | |
225 try: | |
226 name = file_data[u'name'] | |
227 except KeyError: | |
228 return True, None | |
229 assert u'/' not in name | |
230 | |
231 path = file_data.get(u'path') | |
232 if path is not None: | |
233 # we have a path, we can follow it to find node | |
234 try: | |
235 node, rem_path = ShareNode.find(client, path, session[u'peer_jid']) | |
236 except (exceptions.PermissionError, exceptions.NotFound): | |
237 # no file, or file not allowed, we continue normal workflow | |
238 return True, None | |
239 except exceptions.DataError: | |
240 log.warning(_(u'invalid path: {path}').format(path=path)) | |
241 return True, None | |
242 | |
243 if node.type == TYPE_VIRTUAL: | |
244 # we have a virtual node, so name must link to a path node | |
245 try: | |
246 path = node[name].path | |
247 except KeyError: | |
248 return True, None | |
249 elif node.type == TYPE_PATH: | |
250 # we have a path node, so we can retrieve the full path now | |
251 path = os.path.join(node.path, rem_path, name) | |
252 else: | |
253 raise exceptions.InternalError(u'unknown type: {type}'.format(type=node.type)) | |
254 if not os.path.exists(path): | |
255 return True, None | |
256 size = os.path.getsize(path) | |
257 else: | |
258 # we don't have the path, we try to find the file by its name | |
259 try: | |
260 name_data = client._XEP_0329_names_data[name] | |
261 except KeyError: | |
262 return True, None | |
263 | |
264 for path, shared_file in name_data.iteritems(): | |
265 if True: # FIXME: filters are here | |
266 break | |
267 else: | |
268 return True, None | |
269 parent_node = shared_file[u'parent'] | |
270 if not parent_node.checkPermissions(client, session[u'peer_jid']): | |
271 log.warning(_(u"{peer_jid} requested a file (s)he can't access [{profile}]").format( | |
272 peer_jid = session[u'peer_jid'], profile = client.profile)) | |
273 return True, None | |
274 size = shared_file[u'size'] | |
275 | |
276 file_data[u'size'] = size | |
277 file_elt.addElement(u'size', content=unicode(size)) | |
278 hash_algo = file_data[u'hash_algo'] = self._h.getDefaultAlgo() | |
279 hasher = file_data[u'hash_hasher'] = self._h.getHasher(hash_algo) | |
280 file_elt.addChild(self._h.buildHashUsedElt(hash_algo)) | |
281 content_data['stream_object'] = stream.FileStreamObject( | |
282 self.host, | |
283 client, | |
284 path, | |
285 uid=self._jf.getProgressId(session, content_name), | |
286 size=size, | |
287 data_cb=lambda data: hasher.update(data), | |
288 ) | |
289 return False, True | |
290 | |
291 # common methods | |
292 | |
293 def _requestHandler(self, client, iq_elt, root_nodes_cb, files_from_node_cb): | |
294 iq_elt.handled = True | |
295 owner = jid.JID(iq_elt['from']).userhostJID() | |
296 node = iq_elt.query.getAttribute('node') | |
297 if not node: | |
298 d = defer.maybeDeferred(root_nodes_cb, client, iq_elt, owner) | |
299 else: | |
300 d = defer.maybeDeferred(files_from_node_cb, client, iq_elt, owner, node) | |
301 d.addErrback(lambda failure_: log.error(_(u"error while retrieving files: {msg}").format(msg=failure_))) | |
302 | |
303 def _iqError(self, client, iq_elt, condition="item-not-found"): | |
304 error_elt = jabber_error.StanzaError(condition).toResponse(iq_elt) | |
305 client.send(error_elt) | |
306 | |
307 # client | |
308 | |
309 def _addPathData(self, client, query_elt, path, parent_node): | |
310 """Fill query_elt with files/directories found in path""" | |
311 name = os.path.basename(path) | |
312 if os.path.isfile(path): | |
313 size = os.path.getsize(path) | |
314 media_type = mimetypes.guess_type(path, strict=False)[0] | |
315 file_elt = self._jf.buildFileElement(name = name, | |
316 size = size, | |
317 media_type = media_type, | |
318 date = os.path.getmtime(path)) | |
319 | |
320 query_elt.addChild(file_elt) | |
321 # we don't specify hash as it would be too resource intensive to calculate it for all files | |
322 # we add file to name_data, so users can request it later | |
323 name_data = client._XEP_0329_names_data.setdefault(name, {}) | |
324 if path not in name_data: | |
325 name_data[path] = {'size': size, | |
326 'media_type': media_type, | |
327 'parent': parent_node} | |
328 else: | |
329 # we have a directory | |
330 directory_elt = query_elt.addElement('directory') | |
331 directory_elt['name'] = name | |
332 | |
333 def _pathNodeHandler(self, client, iq_elt, query_elt, node, path): | |
334 """Fill query_elt for path nodes, i.e. physical directories""" | |
335 path = os.path.join(node.path, path) | |
336 | |
337 if not os.path.exists(path): | |
338 # path may have been moved since it has been shared | |
339 return self._iqError(client, iq_elt) | |
340 elif os.path.isfile(path): | |
341 self._addPathData(client, query_elt, path, node) | |
342 else: | |
343 for name in sorted(os.listdir(path.encode('utf-8')), key=lambda n: n.lower()): | |
344 try: | |
345 name = name.decode('utf-8', 'strict') | |
346 except UnicodeDecodeError as e: | |
347 log.warning(_(u"ignoring invalid unicode name ({name}): {msg}").format( | |
348 name = name.decode('utf-8', 'replace'), | |
349 msg = e)) | |
350 continue | |
351 full_path = os.path.join(path, name) | |
352 self._addPathData(client, query_elt, full_path, node) | |
353 | |
354 def _virtualNodeHandler(self, client, peer_jid, iq_elt, query_elt, node): | |
355 """Fill query_elt for virtual nodes""" | |
356 for name, child_node in node.iteritems(): | |
357 if not child_node.checkPermissions(client, peer_jid, check_parents=False): | |
358 continue | |
359 node_type = child_node.type | |
360 if node_type == TYPE_VIRTUAL: | |
361 directory_elt = query_elt.addElement('directory') | |
362 directory_elt['name'] = name | |
363 elif node_type == TYPE_PATH: | |
364 self._addPathData(client, query_elt, child_node.path, child_node) | |
365 else: | |
366 raise exceptions.InternalError(_(u'unexpected type: {type}').format(type=node_type)) | |
367 | |
368 def _getRootNodesCb(self, client, iq_elt, owner): | |
369 peer_jid = jid.JID(iq_elt['from']) | |
370 iq_result_elt = xmlstream.toResponse(iq_elt, 'result') | |
371 query_elt = iq_result_elt.addElement((NS_FIS, 'query')) | |
372 for name, node in client._XEP_0329_root_node.iteritems(): | |
373 if not node.checkPermissions(client, peer_jid, check_parents=False): | |
374 continue | |
375 directory_elt = query_elt.addElement('directory') | |
376 directory_elt['name'] = name | |
377 client.send(iq_result_elt) | |
378 | |
379 def _getFilesFromNodeCb(self, client, iq_elt, owner, node_path): | |
380 """Main method to retrieve files/directories from a node_path""" | |
381 peer_jid = jid.JID(iq_elt[u'from']) | |
382 try: | |
383 node, path = ShareNode.find(client, node_path, peer_jid) | |
384 except (exceptions.PermissionError, exceptions.NotFound): | |
385 return self._iqError(client, iq_elt) | |
386 except exceptions.DataError: | |
387 return self._iqError(client, iq_elt, condition='not-acceptable') | |
388 | |
389 node_type = node.type | |
390 peer_jid = jid.JID(iq_elt['from']) | |
391 iq_result_elt = xmlstream.toResponse(iq_elt, 'result') | |
392 query_elt = iq_result_elt.addElement((NS_FIS, 'query')) | |
393 query_elt[u'node'] = node_path | |
394 | |
395 # we now fill query_elt according to node_type | |
396 if node_type == TYPE_PATH: | |
397 # it's a physical path | |
398 self._pathNodeHandler(client, iq_elt, query_elt, node, path) | |
399 elif node_type == TYPE_VIRTUAL: | |
400 assert not path | |
401 self._virtualNodeHandler(client, peer_jid, iq_elt, query_elt, node) | |
402 else: | |
403 raise exceptions.InternalError(_(u'unknown node type: {type}').format(type=node_type)) | |
404 | |
405 client.send(iq_result_elt) | |
406 | |
407 def onRequest(self, iq_elt, client): | |
408 return self._requestHandler(client, iq_elt, self._getRootNodesCb, self._getFilesFromNodeCb) | |
409 | |
410 # Component | |
411 | |
412 @defer.inlineCallbacks | |
413 def _compGetRootNodesCb(self, client, iq_elt, owner): | |
414 peer_jid = jid.JID(iq_elt['from']) | |
415 files_data = yield self.host.memory.getFiles(client, peer_jid=peer_jid, parent=u'', | |
416 type_=C.FILE_TYPE_DIRECTORY, owner=owner) | |
417 iq_result_elt = xmlstream.toResponse(iq_elt, 'result') | |
418 query_elt = iq_result_elt.addElement((NS_FIS, 'query')) | |
419 for file_data in files_data: | |
420 name = file_data[u'name'] | |
421 directory_elt = query_elt.addElement(u'directory') | |
422 directory_elt[u'name'] = name | |
423 client.send(iq_result_elt) | |
424 | |
425 @defer.inlineCallbacks | |
426 def _compGetFilesFromNodeCb(self, client, iq_elt, owner, node_path): | |
427 peer_jid = jid.JID(iq_elt['from']) | |
428 try: | |
429 files_data = yield self.host.memory.getFiles(client, peer_jid=peer_jid, path=node_path, owner=owner) | |
430 except exceptions.NotFound: | |
431 self._iqError(client, iq_elt) | |
432 return | |
433 iq_result_elt = xmlstream.toResponse(iq_elt, 'result') | |
434 query_elt = iq_result_elt.addElement((NS_FIS, 'query')) | |
435 query_elt[u'node'] = node_path | |
436 for file_data in files_data: | |
437 file_elt = self._jf.buildFileElementFromDict(file_data, | |
438 date=file_data.get(u'modified', file_data[u'created'])) | |
439 query_elt.addChild(file_elt) | |
440 client.send(iq_result_elt) | |
441 | |
442 def onComponentRequest(self, iq_elt, client): | |
443 return self._requestHandler(client, iq_elt, self._compGetRootNodesCb, self._compGetFilesFromNodeCb) | |
444 | |
445 def _parseResult(self, iq_elt): | |
446 query_elt = next(iq_elt.elements(NS_FIS, 'query')) | |
447 files = [] | |
448 | |
449 for elt in query_elt.elements(): | |
450 if elt.name == 'file': | |
451 # we have a file | |
452 try: | |
453 file_data = self._jf.parseFileElement(elt) | |
454 except exceptions.DataError: | |
455 continue | |
456 file_data[u'type'] = C.FILE_TYPE_FILE | |
457 elif elt.name == 'directory' and elt.uri == NS_FIS: | |
458 # we have a directory | |
459 | |
460 file_data = {'name': elt['name'], 'type': C.FILE_TYPE_DIRECTORY} | |
461 else: | |
462 log.warning(_(u"unexpected element, ignoring: {elt}").format(elt=elt.toXml())) | |
463 continue | |
464 files.append(file_data) | |
465 return files | |
466 | |
467 # file methods # | |
468 | |
469 def _serializeData(self, files_data): | |
470 for file_data in files_data: | |
471 for key, value in file_data.iteritems(): | |
472 file_data[key] = unicode(value) | |
473 return files_data | |
474 | |
475 def _listFiles(self, target_jid, path, extra, profile): | |
476 client = self.host.getClient(profile) | |
477 target_jid = client.jid.userhostJID() if not target_jid else jid.JID(target_jid) | |
478 d = self.listFiles(client, target_jid, path or None) | |
479 d.addCallback(self._serializeData) | |
480 return d | |
481 | |
482 def listFiles(self, client, target_jid, path=None, extra=None): | |
483 """List file shared by an entity | |
484 | |
485 @param target_jid(jid.JID): jid of the sharing entity | |
486 @param path(unicode, None): path to the directory containing shared files | |
487 None to get root directories | |
488 @param extra(dict, None): extra data | |
489 @return list(dict): shared files | |
490 """ | |
491 iq_elt = client.IQ('get') | |
492 iq_elt['to'] = target_jid.full() | |
493 query_elt = iq_elt.addElement((NS_FIS, 'query')) | |
494 if path: | |
495 query_elt['node'] = path | |
496 d = iq_elt.send() | |
497 d.addCallback(self._parseResult) | |
498 return d | |
499 | |
500 def _sharePath(self, name, path, access, profile): | |
501 client = self.host.getClient(profile) | |
502 access= json.loads(access) | |
503 return self.sharePath(client, name or None, path, access) | |
504 | |
505 def sharePath(self, client, name, path, access): | |
506 if client.is_component: | |
507 raise exceptions.ClientTypeError | |
508 if not os.path.exists(path): | |
509 raise ValueError(_(u"This path doesn't exist!")) | |
510 if not path or not path.strip(u' /'): | |
511 raise ValueError(_(u"A path need to be specified")) | |
512 | |
513 node = client._XEP_0329_root_node | |
514 node_type = TYPE_PATH | |
515 if os.path.isfile(path): | |
516 # we have a single file, the workflow is diferrent as we store all single files in the same dir | |
517 node = node.getOrCreate(SINGLE_FILES_DIR) | |
518 | |
519 if not name: | |
520 name = os.path.basename(path.rstrip(u' /')) | |
521 if not name: | |
522 raise exceptions.InternalError(_(u"Can't find a proper name")) | |
523 | |
524 if not isinstance(access, dict): | |
525 raise ValueError(_(u'access must be a dict')) | |
526 | |
527 if name in node or name == SINGLE_FILES_DIR: | |
528 idx = 1 | |
529 new_name = name + '_' + unicode(idx) | |
530 while new_name in node: | |
531 idx += 1 | |
532 new_name = name + '_' + unicode(idx) | |
533 name = new_name | |
534 log.info(_(u"A directory with this name is already shared, renamed to {new_name} [{profile}]".format( | |
535 new_name=new_name, profile=client.profile))) | |
536 | |
537 ShareNode(name=name, parent=node, type_=node_type, access=access, path=path) | |
538 return name | |
539 | |
540 | |
541 class XEP_0329_handler(xmlstream.XMPPHandler): | |
542 implements(iwokkel.IDisco) | |
543 | |
544 def __init__(self, plugin_parent): | |
545 self.plugin_parent = plugin_parent | |
546 self.host = plugin_parent.host | |
547 | |
548 def connectionInitialized(self): | |
549 if self.parent.is_component: | |
550 self.xmlstream.addObserver(IQ_FIS_REQUEST, self.plugin_parent.onComponentRequest, client=self.parent) | |
551 else: | |
552 self.xmlstream.addObserver(IQ_FIS_REQUEST, self.plugin_parent.onRequest, client=self.parent) | |
553 | |
554 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): | |
555 return [disco.DiscoFeature(NS_FIS)] | |
556 | |
557 def getDiscoItems(self, requestor, target, nodeIdentifier=''): | |
558 return [] |