# HG changeset patch # User Goffi # Date 1596291164 -7200 # Node ID bb92085720c81eb1e4cdffaec7004be5bedab388 # Parent 3a15e76a694ea598075125c401b47f3f969f252f plugin XEP-0329: implemented ways to get/set affiliations: PubSub-like affiliations can now be get/set using non standard methods. This has been done to prepare the move to a pubsub based file sharing system. diff -r 3a15e76a694e -r bb92085720c8 sat/plugins/plugin_xep_0329.py --- a/sat/plugins/plugin_xep_0329.py Sat Aug 01 16:07:39 2020 +0200 +++ b/sat/plugins/plugin_xep_0329.py Sat Aug 01 16:12:44 2020 +0200 @@ -16,25 +16,28 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from sat.core.i18n import _ -from sat.core import exceptions -from sat.core.constants import Const as C -from sat.core.log import getLogger - -log = getLogger(__name__) -from sat.tools import stream -from sat.tools.common import regex -from wokkel import disco, iwokkel +import mimetypes +import json +import os +from pathlib import Path +from typing import Optional, Dict from zope.interface import implementer from twisted.words.protocols.jabber import xmlstream from twisted.words.protocols.jabber import jid from twisted.words.protocols.jabber import error as jabber_error from twisted.internet import defer -import mimetypes -import json -import os +from wokkel import disco, iwokkel, data_form +from sat.core.i18n import _ +from sat.core.xmpp import SatXMPPEntity +from sat.core import exceptions +from sat.core.constants import Const as C +from sat.core.log import getLogger +from sat.tools import stream +from sat.tools.common import regex +log = getLogger(__name__) + PLUGIN_INFO = { C.PI_NAME: "File Information Sharing", C.PI_IMPORT_NAME: "XEP-0329", @@ -48,8 +51,13 @@ } NS_FIS = "urn:xmpp:fis:0" +NS_FIS_AFFILIATION = "org.salut-a-toi.fis-affiliation" +NS_FIS_CONFIGURATION = "org.salut-a-toi.fis-configuration" -IQ_FIS_REQUEST = C.IQ_GET + '/query[@xmlns="' + NS_FIS + '"]' +IQ_FIS_REQUEST = f'{C.IQ_GET}/query[@xmlns="{NS_FIS}"]' +# not in the standard, but needed, and it's handy to keep it here +IQ_FIS_AFFILIATION_GET = f'{C.IQ_GET}/affiliations[@xmlns="{NS_FIS_AFFILIATION}"]' +IQ_FIS_AFFILIATION_SET = f'{C.IQ_SET}/affiliations[@xmlns="{NS_FIS_AFFILIATION}"]' SINGLE_FILES_DIR = "files" TYPE_VIRTUAL = "virtual" TYPE_PATH = "path" @@ -57,6 +65,10 @@ KEY_TYPE = "type" +class RootPathException(Exception): + """Root path is requested""" + + class ShareNode(object): """Node containing directory or files to share, virtual or real""" @@ -289,6 +301,22 @@ out_sign="", method=self._unsharePath, ) + host.bridge.addMethod( + "FISAffiliationsGet", + ".plugin", + in_sign="ssss", + out_sign="a{ss}", + method=self._affiliationsGet, + async_=True, + ) + host.bridge.addMethod( + "FISAffiliationsSet", + ".plugin", + in_sign="sssa{ss}s", + out_sign="", + method=self._affiliationsSet, + async_=True, + ) host.bridge.addSignal("FISSharedPathNew", ".plugin", signature="sss") host.bridge.addSignal("FISSharedPathRemoved", ".plugin", signature="ss") host.trigger.add("XEP-0234_fileSendingRequest", self._fileSendingRequestTrigger) @@ -656,6 +684,178 @@ files.append(file_data) return files + # affiliations # + + async def _parseElement(self, client, iq_elt, element, namespace): + from_jid = jid.JID(iq_elt['from']) + elt = next(iq_elt.elements(namespace, element)) + path = Path("/", elt['path']) + if len(path.parts) < 2: + raise RootPathException + namespace = elt.getAttribute('namespace') + files_data = await self.host.memory.getFiles( + client, + peer_jid=from_jid, + path=str(path.parent), + name=path.name, + namespace=namespace, + ) + if len(files_data) != 1: + client.sendError(iq_elt, 'item-not-found') + raise exceptions.CancelError + file_data = files_data[0] + return from_jid, elt, path, namespace, file_data + + def _affiliationsGet(self, service_jid_s, namespace, path, profile): + client = self.host.getClient(profile) + service = jid.JID(service_jid_s) + d = defer.ensureDeferred(self.affiliationsGet( + client, service, namespace or None, path)) + d.addCallback( + lambda affiliations: { + str(entity): affiliation for entity, affiliation in affiliations.items() + } + ) + return d + + async def affiliationsGet( + self, + client: SatXMPPEntity, + service: jid.JID, + namespace: Optional[str], + path: str + ) -> Dict[jid.JID, str]: + if not path: + raise ValueError(f"invalid path: {path!r}") + iq_elt = client.IQ("get") + iq_elt['to'] = service.full() + affiliations_elt = iq_elt.addElement((NS_FIS_AFFILIATION, "affiliations")) + if namespace: + affiliations_elt["namespace"] = namespace + affiliations_elt["path"] = path + iq_result_elt = await iq_elt.send() + try: + affiliations_elt = next(iq_result_elt.elements(NS_FIS_AFFILIATION, "affiliations")) + except StopIteration: + raise exceptions.DataError(f"Invalid result to affiliations request: {iq_result_elt.toXml()}") + + affiliations = {} + for affiliation_elt in affiliations_elt.elements(NS_FIS_AFFILIATION, 'affiliation'): + try: + affiliations[jid.JID(affiliation_elt['jid'])] = affiliation_elt['affiliation'] + except (KeyError, RuntimeError): + raise exceptions.DataError( + f"invalid affiliation element: {affiliation_elt.toXml()}") + + return affiliations + + def _affiliationsSet(self, service_jid_s, namespace, path, affiliations, profile): + client = self.host.getClient(profile) + service = jid.JID(service_jid_s) + affiliations = {jid.JID(e): a for e, a in affiliations.items()} + return defer.ensureDeferred(self.affiliationsSet( + client, service, namespace or None, path, affiliations)) + + async def affiliationsSet( + self, + client: SatXMPPEntity, + service: jid.JID, + namespace: Optional[str], + path: str, + affiliations: Dict[jid.JID, str], + ): + if not path: + raise ValueError(f"invalid path: {path!r}") + iq_elt = client.IQ("set") + iq_elt['to'] = service.full() + affiliations_elt = iq_elt.addElement((NS_FIS_AFFILIATION, "affiliations")) + if namespace: + affiliations_elt["namespace"] = namespace + affiliations_elt["path"] = path + for entity_jid, affiliation in affiliations.items(): + affiliation_elt = affiliations_elt.addElement('affiliation') + affiliation_elt['jid'] = entity_jid.full() + affiliation_elt['affiliation'] = affiliation + await iq_elt.send() + + def _onComponentAffiliationsGet(self, iq_elt, client): + iq_elt.handled = True + defer.ensureDeferred(self.onComponentAffiliationsGet(client, iq_elt)) + + async def onComponentAffiliationsGet(self, client, iq_elt): + try: + ( + from_jid, affiliations_elt, path, namespace, file_data + ) = await self._parseElement(client, iq_elt, "affiliations", NS_FIS_AFFILIATION) + except exceptions.CancelError: + return + except RootPathException: + # if root path is requested, we only get owner affiliation + peer_jid, owner = self._compParseJids(client, iq_elt) + is_owner = peer_jid.userhostJID() == owner + affiliations = {owner: 'owner'} + else: + from_jid_bare = from_jid.userhostJID() + is_owner = from_jid_bare == file_data.get('owner') + affiliations = self.host.memory.getFileAffiliations(file_data) + iq_result_elt = xmlstream.toResponse(iq_elt, "result") + affiliations_elt = iq_result_elt.addElement((NS_FIS_AFFILIATION, 'affiliations')) + for entity_jid, affiliation in affiliations.items(): + if not is_owner and entity_jid.userhostJID() != from_jid_bare: + # only onwer can get all affiliations + continue + affiliation_elt = affiliations_elt.addElement('affiliation') + affiliation_elt['jid'] = entity_jid.userhost() + affiliation_elt['affiliation'] = affiliation + client.send(iq_result_elt) + + def _onComponentAffiliationsSet(self, iq_elt, client): + iq_elt.handled = True + defer.ensureDeferred(self.onComponentAffiliationsSet(client, iq_elt)) + + async def onComponentAffiliationsSet(self, client, iq_elt): + try: + ( + from_jid, affiliations_elt, path, namespace, file_data + ) = await self._parseElement(client, iq_elt, "affiliations", NS_FIS_AFFILIATION) + except exceptions.CancelError: + return + except RootPathException: + client.sendError(iq_elt, 'bad-request', "Root path can't be used") + return + + if from_jid.userhostJID() != file_data['owner']: + log.warning( + f"{from_jid} tried to modify {path} affiliations while the owner is " + f"{file_data['owner']}" + ) + client.sendError(iq_elt, 'forbidden') + return + + try: + affiliations = { + jid.JID(e['jid']): e['affiliation'] + for e in affiliations_elt.elements(NS_FIS_AFFILIATION, 'affiliation') + } + except (KeyError, RuntimeError): + log.warning( + f"invalid affiliation element: {affiliations_elt.toXml()}" + ) + client.sendError(iq_elt, 'bad-request', "invalid affiliation element") + return + except Exception as e: + log.error( + f"unexepected exception while setting affiliation element: {e}\n" + f"{affiliations_elt.toXml()}" + ) + client.sendError(iq_elt, 'internal-server-error', f"{e}") + return + + await self.host.memory.setFileAffiliations(client, file_data, affiliations) + + iq_result_elt = xmlstream.toResponse(iq_elt, "result") + client.send(iq_result_elt) + # file methods # def _serializeData(self, files_data): @@ -763,6 +963,26 @@ self.xmlstream.addObserver( IQ_FIS_REQUEST, self.plugin_parent.onComponentRequest, client=self.parent ) + self.xmlstream.addObserver( + IQ_FIS_AFFILIATION_GET, + self.plugin_parent._onComponentAffiliationsGet, + client=self.parent + ) + self.xmlstream.addObserver( + IQ_FIS_AFFILIATION_SET, + self.plugin_parent._onComponentAffiliationsSet, + client=self.parent + ) + self.xmlstream.addObserver( + IQ_FIS_CONFIGURATION_GET, + self.plugin_parent._onComponentConfigurationGet, + client=self.parent + ) + self.xmlstream.addObserver( + IQ_FIS_CONFIGURATION_SET, + self.plugin_parent._onComponentConfigurationSet, + client=self.parent + ) else: self.xmlstream.addObserver( IQ_FIS_REQUEST, self.plugin_parent.onRequest, client=self.parent