Newer
Older

Thilo Molitor
committed
import gobject

Thilo Molitor
committed
from urlparse import urlparse
from plugins.helpers import log_calls, log
from plugins.gui import GajimPluginConfigDialog
from conversation_textview import TextViewImage
ACCEPTED_MIME_TYPES = ('image/png','image/jpeg','image/gif','image/raw',
'image/svg+xml')
class UrlImagePreviewPlugin(GajimPlugin):
@log_calls('UrlImagePreviewPlugin')
def init(self):
self.config_dialog = UrlImagePreviewPluginConfigDialog(self)
self.gui_extension_points = {
'chat_control_base': (self.connect_with_chat_control,
self.disconnect_from_chat_control),
'print_special_text': (self.print_special_text,
self.print_special_text1),}
self.config_default_values = {
'PREVIEW_SIZE': (150, 'Preview size(10-512)'),
'MAX_FILE_SIZE': (524288, 'Max file size for image preview')}
self.chat_control = None
self.controls = []
@log_calls('UrlImagePreviewPlugin')
def connect_with_chat_control(self, chat_control):
self.chat_control = chat_control
control = Base(self, self.chat_control)
self.controls.append(control)
@log_calls('UrlImagePreviewPlugin')
def disconnect_from_chat_control(self, chat_control):
for control in self.controls:
control.disconnect_from_chat_control()
self.controls = []
def print_special_text(self, tv, special_text, other_tags, graphics=True,
iter_=None):
for control in self.controls:
if control.chat_control.conv_textview != tv:
continue
control.print_special_text(special_text, other_tags, graphics=True,
def print_special_text1(self, chat_control, special_text, other_tags=None,
graphics=True, iter_=None):
for control in self.controls:
if control.chat_control == chat_control:
control.disconnect_from_chat_control()
self.controls.remove(control)
class Base(object):
def __init__(self, plugin, chat_control):
self.plugin = plugin
self.chat_control = chat_control
self.textview = self.chat_control.conv_textview
def print_special_text(self, special_text, other_tags, graphics=True,
iter_=None):
# remove qip bbcode
special_text = special_text.rsplit('[/img]')[0]

Thilo Molitor
committed
parts = urlparse(special_text)
if not parts.scheme in ["https", "http", "ftp", "ftps"] or \
not parts.netloc:
log.info("Not accepting URL for image preview: %s" % special_text)
return
if not iter_:
iter_ = buffer_.get_end_iter()
# Detect XHTML-IM link
ttt = buffer_.get_tag_table()
tags_ = [(ttt.lookup(t) if isinstance(t, str) else t) for t in other_tags]
for t in tags_:
is_xhtml_link = getattr(t, 'href', None)
if is_xhtml_link:
break
# Show URL, until image is loaded (if ever)
repl_start = buffer_.create_mark(None, iter_, True)
buffer_.insert_with_tags(iter_, special_text, \
*[(ttt.lookup(t) if isinstance(t, str) else t) for t in ["url"]])
repl_end = buffer_.create_mark(None, iter_, True)
# First get the http head request with does not fetch data, just headers
gajim.thread_interface(self._get_http_head, [self.textview.account,
special_text], self._check_mime_size, [special_text, repl_start, repl_end])
# Don't print the URL in the message window (in the calling function)
self.textview.plugin_modified = True
def _check_mime_size(self, (file_mime, file_size), url, repl_start, repl_end):
# Check if mime type is acceptable
if file_mime.lower() not in ACCEPTED_MIME_TYPES:
# URL is already displayed

Thilo Molitor
committed
log.info("Not accepted mime type '%s' for URL: '%s'" % (file_mime.lower(), url))
return
# Check if file size is acceptable
if file_size > self.plugin.config['MAX_FILE_SIZE'] or file_size == 0:

Thilo Molitor
committed
log.info("File size too big or unknown for URL: '%s'" % url)
# URL is already displayed
return
# Start downloading image
gajim.thread_interface(helpers.download_image, [ self.textview.account, {

Thilo Molitor
committed
'src': url, 'max_size': self.plugin.config['MAX_FILE_SIZE'] } ],
self._update_img, [url, file_mime, repl_start, repl_end])
def _update_img(self, (mem, alt), url, file_mime, repl_start, repl_end):
if mem:
try:
loader = gtk.gdk.PixbufLoader()
loader.write(mem)
loader.close()
pixbuf = loader.get_pixbuf()
pixbuf, w, h = self.get_pixbuf_of_size(pixbuf,
self.plugin.config['PREVIEW_SIZE'])
eb = gtk.EventBox()
eb.connect('button-press-event', self.on_button_press_event,
url)
eb.connect('enter-notify-event', self.on_enter_event)
eb.connect('leave-notify-event', self.on_leave_event)

Thilo Molitor
committed
# this is threadsafe (gtk textview is NOT threadsafe by itself!!)
def add_to_textview():
buffer_ = repl_start.get_buffer()
iter_ = buffer_.get_iter_at_mark(repl_start)
buffer_.insert(iter_, "\n")
anchor = buffer_.create_child_anchor(iter_)
# Use url as tooltip for image
img = TextViewImage(anchor, url)
img.set_from_pixbuf(pixbuf)
eb.add(img)
eb.show_all()
self.textview.tv.add_child_at_anchor(eb, anchor)
buffer_.delete(iter_, buffer_.get_iter_at_mark(repl_end))
return False
gobject.idle_add(add_to_textview)
# URL is already displayed
log.error('Could not display image for URL: %s' % url)
raise
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
else:
# If image could not be downloaded, URL is already displayed
log.error('Could not download image for URL: %s' % url)
def _get_http_head (self, account, url):
# Check if proxy is used
proxy = helpers.get_proxy_info(account)
if proxy and proxy['type'] in ('http', 'socks5'):
return self._get_http_head_proxy(url, proxy)
return self._get_http_head_direct(url)
def _get_http_head_direct (self, url):
log.debug('Get head request direct for URL: %s' % url)
try:
req = urllib2.Request(url)
req.get_method = lambda : 'HEAD'
req.add_header('User-Agent', 'Gajim %s' % gajim.version)
f = urllib2.urlopen(req)
except Exception, ex:
log.debug('Could not get head response for URL: %s' % url)
return ('', 0)
url_headers = f.info()
ctype = ''
ctype_list = url_headers.getheaders('Content-Type')
if ctype_list:
ctype = ctype_list[0]
clen_list = url_headers.getheaders('Content-Length')
clen = 0
if clen_list:
try:
clen = int(clen_list[0])
except ValueError:
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
return (ctype, clen)
def _get_http_head_proxy(self, url, proxy):
log.debug('Get head request with proxy for URL: %s' % url)
if not gajim.HAVE_PYCURL:
log.error('PYCURL not installed')
return ('', 0)
import pycurl
from cStringIO import StringIO
headers = ''
try:
b = StringIO()
c = pycurl.Curl()
c.setopt(pycurl.URL, url.encode('utf-8'))
c.setopt(pycurl.FOLLOWLOCATION, 1)
c.setopt(pycurl.CONNECTTIMEOUT, 5)
# Make a HEAD request:
c.setopt(pycurl.CUSTOMREQUEST, 'HEAD')
c.setopt(pycurl.NOBODY, 1)
c.setopt(pycurl.HEADER, 1)
c.setopt(pycurl.TIMEOUT, 10)
c.setopt(pycurl.MAXFILESIZE, 2000000)
c.setopt(pycurl.WRITEFUNCTION, b.write)
c.setopt(pycurl.USERAGENT, 'Gajim ' + gajim.version)
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# set proxy
c.setopt(pycurl.PROXY, proxy['host'].encode('utf-8'))
c.setopt(pycurl.PROXYPORT, proxy['port'])
if proxy['useauth']:
c.setopt(pycurl.PROXYUSERPWD, proxy['user'].encode('utf-8')\
+ ':' + proxy['pass'].encode('utf-8'))
c.setopt(pycurl.PROXYAUTH, pycurl.HTTPAUTH_ANY)
if proxy['type'] == 'http':
c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_HTTP)
elif proxy['type'] == 'socks5':
c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_SOCKS5)
x = c.perform()
c.close()
headers = b.getvalue()
except pycurl.error, ex:
log.debug('Could not get head response for URL: %s' % url)
return ('', 0)
ctype = ''
searchObj = re.search( r'^Content-Type: (.*)$', headers, re.M|re.I)
if searchObj:
ctype = searchObj.group(1).strip()
clen = 0
searchObj = re.search( r'^Content-Length: (.*)$', headers, re.M|re.I)
if searchObj:
try:
clen = int(searchObj.group(1).strip())
except ValueError:
pass
return (ctype, clen)
# Change mouse pointer to HAND2 when mouse enter the eventbox with the image
def on_enter_event (self, eb, event):
self.textview.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
gtk.gdk.Cursor(gtk.gdk.HAND2))
# Change mouse pointer to default when mouse leaves the eventbox
def on_leave_event (self, eb, event):
self.textview.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
gtk.gdk.Cursor(gtk.gdk.XTERM))
def on_button_press_event(self, eb, event, url):
if event.button == 1: # left click
# Open URL in browser
helpers.launch_browser_mailer('url', url)
def get_pixbuf_of_size(self, pixbuf, size):
# Creates a pixbuf that fits in the specified square of sizexsize
# while preserving the aspect ratio
# Returns tuple: (scaled_pixbuf, actual_width, actual_height)
image_width = pixbuf.get_width()
image_height = pixbuf.get_height()
if image_width > image_height:
if image_width > size:
image_height = int(size / float(image_width) * image_height)
image_width = int(size)
else:
if image_height > size:
image_width = int(size / float(image_height) * image_width)
image_height = int(size)
crop_pixbuf = pixbuf.scale_simple(image_width, image_height,
gtk.gdk.INTERP_BILINEAR)
return (crop_pixbuf, image_width, image_height)
def disconnect_from_chat_control(self):
pass
class UrlImagePreviewPluginConfigDialog(GajimPluginConfigDialog):
max_file_size = [262144, 524288, 1048576, 5242880, 10485760]
def init(self):
self.GTK_BUILDER_FILE_PATH = self.plugin.local_file_path(
'config_dialog.ui')
self.xml = gtk.Builder()
self.xml.set_translation_domain('gajim_plugins')
self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH, [
'vbox1', 'liststore1'])
self.preview_size_spinbutton = self.xml.get_object('preview_size')
self.preview_size_spinbutton.get_adjustment().set_all(20, 10, 512, 1,
10, 0)
self.max_size_combobox = self.xml.get_object('max_size_combobox')
vbox = self.xml.get_object('vbox1')
self.child.pack_start(vbox)
self.xml.connect_signals(self)
def on_run(self):
self.preview_size_spinbutton.set_value(self.plugin.config[
'PREVIEW_SIZE'])
value = self.plugin.config['MAX_FILE_SIZE']
if value:
self.max_size_combobox.set_active(self.max_file_size.index(value))
else:
self.max_size_combobox.set_active(-1)
def preview_size_value_changed(self, spinbutton):
self.plugin.config['PREVIEW_SIZE'] = spinbutton.get_value()
def max_size_value_changed(self, widget):
self.plugin.config['MAX_FILE_SIZE'] = self.max_file_size[
self.max_size_combobox.get_active()]