# HG changeset patch # User Goffi # Date 1596291277 -7200 # Node ID 8bbd2ed924e82e112b2143c14422e15f42d859f1 # Parent bb92085720c81eb1e4cdffaec7004be5bedab388 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. diff -r bb92085720c8 -r 8bbd2ed924e8 sat/plugins/plugin_xep_0329.py --- 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):