comparison sat/plugins/plugin_xep_0231.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/plugins/plugin_xep_0231.py@a201194fc461
children 56f94936df1e
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT plugin for Bit of Binary handling (XEP-0231)
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.constants import Const as C
22 from sat.core import exceptions
23 from sat.core.log import getLogger
24 log = getLogger(__name__)
25 from sat.tools import xml_tools
26 from wokkel import disco, iwokkel
27 from zope.interface import implements
28 from twisted.python import failure
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 from functools import partial
34 import base64
35 import time
36
37
38 PLUGIN_INFO = {
39 C.PI_NAME: "Bits of Binary",
40 C.PI_IMPORT_NAME: "XEP-0231",
41 C.PI_TYPE: "XEP",
42 C.PI_MODES: C.PLUG_MODE_BOTH,
43 C.PI_PROTOCOLS: ["XEP-0231"],
44 C.PI_MAIN: "XEP_0231",
45 C.PI_HANDLER: "yes",
46 C.PI_DESCRIPTION: _("""Implementation of bits of binary (used for small images/files)""")
47 }
48
49 NS_BOB = u'urn:xmpp:bob'
50 IQ_BOB_REQUEST = C.IQ_GET + '/data[@xmlns="' + NS_BOB + '"]'
51
52
53 class XEP_0231(object):
54
55 def __init__(self, host):
56 log.info(_(u"plugin Bits of Binary initialization"))
57 self.host = host
58 host.registerNamespace('bob', NS_BOB)
59 host.trigger.add("xhtml_post_treat", self.XHTMLTrigger)
60 host.bridge.addMethod("bobGetFile", ".plugin",
61 in_sign='sss', out_sign='s',
62 method=self._getFile,
63 async=True)
64
65 def dumpData(self, cache, data_elt, cid):
66 """save file encoded in data_elt to cache
67
68 @param cache(memory.cache.Cache): cache to use to store the data
69 @param data_elt(domish.Element): <data> as in XEP-0231
70 @param cid(unicode): content-id
71 @return(unicode): full path to dumped file
72 """
73 # FIXME: is it needed to use a separate thread?
74 # probably not with the little data expected with BoB
75 try:
76 max_age = int(data_elt['max-age'])
77 if max_age < 0:
78 raise ValueError
79 except (KeyError, ValueError):
80 log.warning(u'invalid max-age found')
81 max_age = None
82
83 with cache.cacheData(
84 PLUGIN_INFO[C.PI_IMPORT_NAME],
85 cid,
86 data_elt.getAttribute('type'),
87 max_age) as f:
88
89 file_path = f.name
90 f.write(base64.b64decode(str(data_elt)))
91
92 return file_path
93
94 def getHandler(self, client):
95 return XEP_0231_handler(self)
96
97 def _requestCb(self, iq_elt, cache, cid):
98 for data_elt in iq_elt.elements(NS_BOB, u'data'):
99 if data_elt.getAttribute('cid') == cid:
100 file_path = self.dumpData(cache, data_elt, cid)
101 return file_path
102
103 log.warning(u"invalid data stanza received, requested cid was not found:\n{iq_elt}\nrequested cid: {cid}".format(
104 iq_elt = iq_elt,
105 cid = cid
106 ))
107 raise failure.Failure(exceptions.DataError("missing data"))
108
109 def _requestEb(self, failure_):
110 """Log the error and continue errback chain"""
111 log.warning(u"Can't get requested data:\n{reason}".format(reason=failure_))
112 return failure_
113
114 def requestData(self, client, to_jid, cid, cache=None):
115 """Request data if we don't have it in cache
116
117 @param to_jid(jid.JID): jid to request the data to
118 @param cid(unicode): content id
119 @param cache(memory.cache.Cache, None): cache to use
120 client.cache will be used if None
121 @return D(unicode): path to file with data
122 """
123 if cache is None:
124 cache = client.cache
125 iq_elt = client.IQ('get')
126 iq_elt['to'] = to_jid.full()
127 data_elt = iq_elt.addElement((NS_BOB, 'data'))
128 data_elt['cid'] = cid
129 d = iq_elt.send()
130 d.addCallback(self._requestCb, cache, cid)
131 d.addErrback(self._requestEb)
132 return d
133
134 def _setImgEltSrc(self, path, img_elt):
135 img_elt[u'src'] = u'file://{}'.format(path)
136
137 def XHTMLTrigger(self, client, message_elt, body_elt, lang, treat_d):
138 for img_elt in xml_tools.findAll(body_elt, C.NS_XHTML, u'img'):
139 source = img_elt.getAttribute(u'src','')
140 if source.startswith(u'cid:'):
141 cid = source[4:]
142 file_path = client.cache.getFilePath(cid)
143 if file_path is not None:
144 # image is in cache, we change the url
145 img_elt[u'src'] = u'file://{}'.format(file_path)
146 continue
147 else:
148 # image is not in cache, is it given locally?
149 for data_elt in message_elt.elements(NS_BOB, u'data'):
150 if data_elt.getAttribute('cid') == cid:
151 file_path = self.dumpData(client.cache, data_elt, cid)
152 img_elt[u'src'] = u'file://{}'.format(file_path)
153 break
154 else:
155 # cid not found locally, we need to request it
156 # so we use the deferred
157 d = self.requestData(client, jid.JID(message_elt['from']), cid)
158 d.addCallback(partial(self._setImgEltSrc, img_elt=img_elt))
159 treat_d.addCallback(lambda dummy: d)
160
161 def onComponentRequest(self, iq_elt, client):
162 """cache data is retrieve from common cache for components"""
163 # FIXME: this is a security/privacy issue as no access check is done
164 # but this is mitigated by the fact that the cid must be known.
165 # An access check should be implemented though.
166
167 iq_elt.handled = True
168 data_elt = next(iq_elt.elements(NS_BOB, 'data'))
169 try:
170 cid = data_elt[u'cid']
171 except KeyError:
172 error_elt = jabber_error.StanzaError('not-acceptable').toResponse(iq_elt)
173 client.send(error_elt)
174 return
175
176 metadata = self.host.common_cache.getMetadata(cid)
177 if metadata is None:
178 error_elt = jabber_error.StanzaError('item-not-found').toResponse(iq_elt)
179 client.send(error_elt)
180 return
181
182 with open(metadata['path']) as f:
183 data = f.read()
184
185 result_elt = xmlstream.toResponse(iq_elt, 'result')
186 data_elt = result_elt.addElement((NS_BOB, 'data'), content = data.encode('base64'))
187 data_elt[u'cid'] = cid
188 data_elt[u'type'] = metadata[u'mime_type']
189 data_elt[u'max-age'] = unicode(int(max(0, metadata['eol'] - time.time())))
190 client.send(result_elt)
191
192 def _getFile(self, peer_jid_s, cid, profile):
193 peer_jid = jid.JID(peer_jid_s)
194 assert cid
195 client = self.host.getClient(profile)
196 return self.getFile(client, peer_jid, cid)
197
198 def getFile(self, client, peer_jid, cid, parent_elt=None):
199 """Retrieve a file from it's content-id
200
201 @param peer_jid(jid.JID): jid of the entity offering the data
202 @param cid(unicode): content-id of file data
203 @param parent_elt(domish.Element, None): if file is not in cache,
204 data will be looked after in children of this elements.
205 None to ignore
206 @return D(unicode): path to cached data
207 """
208 file_path = client.cache.getFilePath(cid)
209 if file_path is not None:
210 # file is in cache
211 return defer.succeed(file_path)
212 else:
213 # file not in cache, is it given locally?
214 if parent_elt is not None:
215 for data_elt in parent_elt.elements(NS_BOB, u'data'):
216 if data_elt.getAttribute('cid') == cid:
217 return defer.succeed(self.dumpData(client.cache, data_elt, cid))
218
219 # cid not found locally, we need to request it
220 # so we use the deferred
221 return self.requestData(client, peer_jid, cid)
222
223
224 class XEP_0231_handler(xmlstream.XMPPHandler):
225 implements(iwokkel.IDisco)
226
227 def __init__(self, plugin_parent):
228 self.plugin_parent = plugin_parent
229 self.host = plugin_parent.host
230
231 def connectionInitialized(self):
232 if self.parent.is_component:
233 self.xmlstream.addObserver(IQ_BOB_REQUEST, self.plugin_parent.onComponentRequest, client=self.parent)
234
235 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
236 return [disco.DiscoFeature(NS_BOB)]
237
238 def getDiscoItems(self, requestor, target, nodeIdentifier=''):
239 return []