changeset 3321:8bbd2ed924e8

plugin XEP-0329: added way to change `access_model` using PubSub-like configuration: Those methods are not standard, but have been done in a PubSub-like way to prepare the move to a PubSub based file sharing system.
author Goffi <goffi@goffi.org>
date Sat, 01 Aug 2020 16:14:37 +0200
parents bb92085720c8
children 56ee491c0df6
files sat/plugins/plugin_xep_0329.py
diffstat 1 files changed, 149 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/sat/plugins/plugin_xep_0329.py	Sat Aug 01 16:12:44 2020 +0200
+++ b/sat/plugins/plugin_xep_0329.py	Sat Aug 01 16:14:37 2020 +0200
@@ -58,6 +58,8 @@
 # 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}"]'
+IQ_FIS_CONFIGURATION_GET = f'{C.IQ_GET}/configuration[@xmlns="{NS_FIS_CONFIGURATION}"]'
+IQ_FIS_CONFIGURATION_SET = f'{C.IQ_SET}/configuration[@xmlns="{NS_FIS_CONFIGURATION}"]'
 SINGLE_FILES_DIR = "files"
 TYPE_VIRTUAL = "virtual"
 TYPE_PATH = "path"
@@ -317,6 +319,22 @@
             method=self._affiliationsSet,
             async_=True,
         )
+        host.bridge.addMethod(
+            "FISConfigurationGet",
+            ".plugin",
+            in_sign="ssss",
+            out_sign="a{ss}",
+            method=self._configurationGet,
+            async_=True,
+        )
+        host.bridge.addMethod(
+            "FISConfigurationSet",
+            ".plugin",
+            in_sign="sssa{ss}s",
+            out_sign="",
+            method=self._configurationSet,
+            async_=True,
+        )
         host.bridge.addSignal("FISSharedPathNew", ".plugin", signature="sss")
         host.bridge.addSignal("FISSharedPathRemoved", ".plugin", signature="ss")
         host.trigger.add("XEP-0234_fileSendingRequest", self._fileSendingRequestTrigger)
@@ -856,6 +874,137 @@
         iq_result_elt = xmlstream.toResponse(iq_elt, "result")
         client.send(iq_result_elt)
 
+    # configuration
+
+    def _configurationGet(self, service_jid_s, namespace, path, profile):
+        client = self.host.getClient(profile)
+        service = jid.JID(service_jid_s)
+        d = defer.ensureDeferred(self.configurationGet(
+            client, service, namespace or None, path))
+        d.addCallback(
+            lambda configuration: {
+                str(entity): affiliation for entity, affiliation in configuration.items()
+            }
+        )
+        return d
+
+    async def configurationGet(
+        self,
+        client: SatXMPPEntity,
+        service: jid.JID,
+        namespace: Optional[str],
+        path: str
+    ) -> Dict[str, str]:
+        if not path:
+            raise ValueError(f"invalid path: {path!r}")
+        iq_elt = client.IQ("get")
+        iq_elt['to'] = service.full()
+        configuration_elt = iq_elt.addElement((NS_FIS_CONFIGURATION, "configuration"))
+        if namespace:
+            configuration_elt["namespace"] = namespace
+        configuration_elt["path"] = path
+        iq_result_elt = await iq_elt.send()
+        try:
+            configuration_elt = next(iq_result_elt.elements(NS_FIS_CONFIGURATION, "configuration"))
+        except StopIteration:
+            raise exceptions.DataError(f"Invalid result to configuration request: {iq_result_elt.toXml()}")
+
+        form = data_form.findForm(configuration_elt, NS_FIS_CONFIGURATION)
+        configuration = {f.var: f.value for f in form.fields.values()}
+
+        return configuration
+
+    def _configurationSet(self, service_jid_s, namespace, path, configuration, profile):
+        client = self.host.getClient(profile)
+        service = jid.JID(service_jid_s)
+        return defer.ensureDeferred(self.configurationSet(
+            client, service, namespace or None, path, configuration))
+
+    async def configurationSet(
+        self,
+        client: SatXMPPEntity,
+        service: jid.JID,
+        namespace: Optional[str],
+        path: str,
+        configuration: Dict[jid.JID, str],
+    ):
+        if not path:
+            raise ValueError(f"invalid path: {path!r}")
+        iq_elt = client.IQ("set")
+        iq_elt['to'] = service.full()
+        configuration_elt = iq_elt.addElement((NS_FIS_CONFIGURATION, "configuration"))
+        if namespace:
+            configuration_elt["namespace"] = namespace
+        configuration_elt["path"] = path
+        form = data_form.Form(formType="submit", formNamespace=NS_FIS_CONFIGURATION)
+        form.makeFields(configuration)
+        configuration_elt.addChild(form.toElement())
+        await iq_elt.send()
+
+    def _onComponentConfigurationGet(self, iq_elt, client):
+        iq_elt.handled = True
+        defer.ensureDeferred(self.onComponentConfigurationGet(client, iq_elt))
+
+    async def onComponentConfigurationGet(self, client, iq_elt):
+        try:
+            (
+                from_jid, configuration_elt, path, namespace, file_data
+            ) = await self._parseElement(client, iq_elt, "configuration", NS_FIS_CONFIGURATION)
+        except exceptions.CancelError:
+            return
+        except RootPathException:
+            client.sendError(iq_elt, 'bad-request', "Root path can't be used")
+            return
+        try:
+            access_type = file_data['access'][C.ACCESS_PERM_READ]['type']
+        except KeyError:
+            access_model = 'whitelist'
+        else:
+            access_model = 'open' if access_type == C.ACCESS_TYPE_PUBLIC else 'whitelist'
+
+        iq_result_elt = xmlstream.toResponse(iq_elt, "result")
+        configuration_elt = iq_result_elt.addElement((NS_FIS_CONFIGURATION, 'configuration'))
+        form = data_form.Form(formType="form", formNamespace=NS_FIS_CONFIGURATION)
+        form.makeFields({'access_model': access_model})
+        configuration_elt.addChild(form.toElement())
+        client.send(iq_result_elt)
+
+    def _onComponentConfigurationSet(self, iq_elt, client):
+        iq_elt.handled = True
+        defer.ensureDeferred(self.onComponentConfigurationSet(client, iq_elt))
+
+    async def onComponentConfigurationSet(self, client, iq_elt):
+        try:
+            (
+                from_jid, configuration_elt, path, namespace, file_data
+            ) = await self._parseElement(client, iq_elt, "configuration", NS_FIS_CONFIGURATION)
+        except exceptions.CancelError:
+            return
+        except RootPathException:
+            client.sendError(iq_elt, 'bad-request', "Root path can't be used")
+            return
+
+        from_jid_bare = from_jid.userhostJID()
+        is_owner = from_jid_bare == file_data.get('owner')
+        if not is_owner:
+            log.warning(
+                f"{from_jid} tried to modify {path} configuration while the owner is "
+                f"{file_data['owner']}"
+            )
+            client.sendError(iq_elt, 'forbidden')
+            return
+
+        form = data_form.findForm(configuration_elt, NS_FIS_CONFIGURATION)
+        for name, value in form.items():
+            if name == 'access_model':
+                await self.host.memory.setFileAccessModel(client, file_data, value)
+            else:
+                # TODO: send a IQ error?
+                log.warning(
+                    f"Trying to set a not implemented configuration option: {name}")
+        iq_result_elt = xmlstream.toResponse(iq_elt, "result")
+        client.send(iq_result_elt)
+
     # file methods #
 
     def _serializeData(self, files_data):