url_image_preview.py 25.2 KB
Newer Older
Dicson's avatar
Dicson committed
1
# -*- coding: utf-8 -*-
Thilo Molitor's avatar
Thilo Molitor committed
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
##
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
##
## Gajim is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Gajim.  If not, see <http://www.gnu.org/licenses/>.
##
Dicson's avatar
Dicson committed
17 18

import os
Thilo Molitor's avatar
Thilo Molitor committed
19 20
import hashlib
import binascii
21
import logging
Thilo Molitor's avatar
Thilo Molitor committed
22 23 24
from urllib.parse import urlparse
from io import BytesIO
import shutil
Philipp Hörist's avatar
Philipp Hörist committed
25
from functools import partial
Dicson's avatar
Dicson committed
26

27 28
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf

29 30
from gajim.common import app
from gajim.common import helpers
Thilo Molitor's avatar
Thilo Molitor committed
31 32
from gajim.common import configpaths
from gajim import dialogs
33 34
from gajim.plugins import GajimPlugin
from gajim.plugins.helpers import log_calls
35 36
from url_image_preview.http_functions import get_http_head, get_http_file
from url_image_preview.config_dialog import UrlImagePreviewConfigDialog
37
from url_image_preview.resize_gif import resize_gif
Dicson's avatar
Dicson committed
38

39
log = logging.getLogger('gajim.plugin_system.preview')
Thilo Molitor's avatar
Thilo Molitor committed
40

41
PILLOW_AVAILABLE = True
Thilo Molitor's avatar
Thilo Molitor committed
42 43 44 45
try:
    from PIL import Image
except:
    log.debug('Pillow not available')
46
    PILLOW_AVAILABLE = False
Thilo Molitor's avatar
Thilo Molitor committed
47 48 49 50 51 52 53 54 55 56

try:
    if os.name == 'nt':
        from cryptography.hazmat.backends.openssl import backend
    else:
        from cryptography.hazmat.backends import default_backend
    from cryptography.hazmat.primitives.ciphers import Cipher
    from cryptography.hazmat.primitives.ciphers import algorithms
    from cryptography.hazmat.primitives.ciphers.modes import GCM
    decryption_available = True
57
except Exception:
Thilo Molitor's avatar
Thilo Molitor committed
58 59
    DEP_MSG = 'For preview of encrypted images, ' \
              'please install python-cryptography!'
60
    log.exception('Error')
Thilo Molitor's avatar
Thilo Molitor committed
61 62 63 64 65
    log.info('Decryption/Encryption disabled due to errors')
    decryption_available = False

ACCEPTED_MIME_TYPES = ('image/png', 'image/jpeg', 'image/gif', 'image/raw',
                       'image/svg+xml', 'image/x-ms-bmp')
Dicson's avatar
Dicson committed
66 67 68 69 70


class UrlImagePreviewPlugin(GajimPlugin):
    @log_calls('UrlImagePreviewPlugin')
    def init(self):
Thilo Molitor's avatar
Thilo Molitor committed
71 72
        if not decryption_available:
            self.available_text = DEP_MSG
Philipp Hörist's avatar
Philipp Hörist committed
73
        self.config_dialog = partial(UrlImagePreviewConfigDialog, self)
Dicson's avatar
Dicson committed
74
        self.gui_extension_points = {
Thilo Molitor's avatar
Thilo Molitor committed
75 76
            'chat_control_base': (self.connect_with_chat_control,
                                  self.disconnect_from_chat_control),
77
            'history_window':
78
                (self.connect_with_history, self.disconnect_from_history),
79
            'print_real_text': (self.print_real_text, None), }
Dicson's avatar
Dicson committed
80
        self.config_default_values = {
Thilo Molitor's avatar
Thilo Molitor committed
81 82
            'PREVIEW_SIZE': (150, 'Preview size(10-512)'),
            'MAX_FILE_SIZE': (524288, 'Max file size for image preview'),
83
            'LEFTCLICK_ACTION': ('open_menuitem', 'Open'),
84 85
            'ANONYMOUS_MUC': (False, ''),
            'VERIFY': (True, ''),}
Thilo Molitor's avatar
Thilo Molitor committed
86
        self.controls = {}
87
        self.history_window_control = None
Thilo Molitor's avatar
Thilo Molitor committed
88

Dicson's avatar
Dicson committed
89 90
    @log_calls('UrlImagePreviewPlugin')
    def connect_with_chat_control(self, chat_control):
Thilo Molitor's avatar
Thilo Molitor committed
91 92 93 94
        account = chat_control.contact.account.name
        jid = chat_control.contact.jid
        if account not in self.controls:
            self.controls[account] = {}
95
        self.controls[account][jid] = Base(self, chat_control.conv_textview)
Dicson's avatar
Dicson committed
96 97 98

    @log_calls('UrlImagePreviewPlugin')
    def disconnect_from_chat_control(self, chat_control):
Thilo Molitor's avatar
Thilo Molitor committed
99 100
        account = chat_control.contact.account.name
        jid = chat_control.contact.jid
101
        self.controls[account][jid].deinit_handlers()
Thilo Molitor's avatar
Thilo Molitor committed
102
        del self.controls[account][jid]
103

104 105 106 107 108 109 110
    @log_calls('UrlImagePreviewPlugin')
    def connect_with_history(self, history_window):
        if self.history_window_control:
            log.error("connect_with_history: deinit handlers")
            self.history_window_control.deinit_handlers()
        log.error("connect_with_history: create base")
        self.history_window_control = Base(
111
            self, history_window.history_textview)
112 113 114 115 116 117

    @log_calls('UrlImagePreviewPlugin')
    def disconnect_from_history(self, history_window):
        if self.history_window_control:
            self.history_window_control.deinit_handlers()
        self.history_window_control = None
Dicson's avatar
Dicson committed
118

119 120
    def print_real_text(self, tv, real_text, text_tags, graphics,
                        iter_, additional_data):
121
        if tv.used_in_history_window and self.history_window_control:
122 123 124
            self.history_window_control.print_real_text(
                real_text, text_tags, graphics, iter_, additional_data)

Thilo Molitor's avatar
Thilo Molitor committed
125 126
        account = tv.account
        for jid in self.controls[account]:
127
            if self.controls[account][jid].textview != tv:
Dicson's avatar
Dicson committed
128
                continue
129 130
            self.controls[account][jid].print_real_text(
                real_text, text_tags, graphics, iter_, additional_data)
Thilo Molitor's avatar
Thilo Molitor committed
131
            return
Dicson's avatar
Dicson committed
132 133 134


class Base(object):
135
    def __init__(self, plugin, textview):
Dicson's avatar
Dicson committed
136
        self.plugin = plugin
137
        self.textview = textview
Thilo Molitor's avatar
Thilo Molitor committed
138
        self.handlers = {}
Dicson's avatar
Dicson committed
139

Thilo Molitor's avatar
Thilo Molitor committed
140 141 142 143 144 145 146 147
        self.directory = os.path.join(configpaths.gajimpaths['MY_DATA'],
                                      'downloads')
        self.thumbpath = os.path.join(configpaths.gajimpaths['MY_CACHE'],
                                      'downloads.thumb')

        try:
            self._create_path(self.directory)
            self._create_path(self.thumbpath)
148
        except Exception:
Thilo Molitor's avatar
Thilo Molitor committed
149 150 151
            log.error("Error creating download and/or thumbnail folder!")
            raise

152
    def deinit_handlers(self):
Thilo Molitor's avatar
Thilo Molitor committed
153 154 155 156 157 158 159
        # remove all register handlers on wigets, created by self.xml
        # to prevent circular references among objects
        for i in list(self.handlers.keys()):
            if self.handlers[i].handler_is_connected(i):
                self.handlers[i].disconnect(i)
            del self.handlers[i]

160 161 162
    def print_real_text(self, real_text, text_tags, graphics, iter_,
                        additional_data):
        urlparts = urlparse(real_text)
163 164
        if (urlparts.scheme not in ["https", "aesgcm"] or
                not urlparts.netloc):
165 166
            log.info("Not accepting URL scheme '%s' for image preview: %s",
                     urlparts.scheme, real_text)
167 168
            return

169 170 171 172 173
        try:
            oob_url = additional_data["gajim"]["oob_url"]
        except (KeyError, AttributeError):
            oob_url = None

174 175
        # allow aesgcm uris without oob marker (aesgcm uris are always
        # httpupload filetransfers)
176
        if urlparts.scheme != "aesgcm" and real_text != oob_url:
177 178 179
            log.info("Not accepting URL for image preview "
                     "(wrong or no oob data): %s", real_text)
            log.debug("additional_data: %s", additional_data)
Thilo Molitor's avatar
Thilo Molitor committed
180 181 182 183 184
            return

        # Don't print the URL in the message window (in the calling function)
        self.textview.plugin_modified = True

Dicson's avatar
Dicson committed
185
        buffer_ = self.textview.tv.get_buffer()
Thilo Molitor's avatar
Thilo Molitor committed
186 187 188 189 190 191
        if not iter_:
            iter_ = buffer_.get_end_iter()

        # Show URL, until image is loaded (if ever)
        ttt = buffer_.get_tag_table()
        repl_start = buffer_.create_mark(None, iter_, True)
192
        buffer_.insert_with_tags(iter_, real_text,
Thilo Molitor's avatar
Thilo Molitor committed
193 194 195 196 197 198
            *[(ttt.lookup(t) if isinstance(t, str) else t) for t in ["url"]])
        repl_end = buffer_.create_mark(None, iter_, True)

        filename = os.path.basename(urlparts.path)
        ext = os.path.splitext(filename)[1]
        name = os.path.splitext(filename)[0]
199
        namehash = hashlib.sha1(real_text.encode('utf-8')).hexdigest()
Thilo Molitor's avatar
Thilo Molitor committed
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
        newfilename = name + '_' + namehash + ext
        thumbfilename = name + '_' + namehash + '_thumb_' \
            + str(self.plugin.config['PREVIEW_SIZE']) + ext

        filepath = os.path.join(self.directory, newfilename)
        thumbpath = os.path.join(self.thumbpath, thumbfilename)
        filepaths = [filepath, thumbpath]

        key = ''
        iv = ''
        encrypted = False
        if urlparts.fragment:
            fragment = binascii.unhexlify(urlparts.fragment)
            key = fragment[16:]
            iv = fragment[:16]
            if len(key) == 32 and len(iv) == 16:
                encrypted = True
            if not encrypted:
                key = fragment[12:]
                iv = fragment[:12]
                if len(key) == 32 and len(iv) == 12:
                    encrypted = True
222

Thilo Molitor's avatar
Thilo Molitor committed
223 224 225 226 227
        # file exists but thumbnail got deleted
        if os.path.exists(filepath) and not os.path.exists(thumbpath):
            with open(filepath, 'rb') as f:
                mem = f.read()
            app.thread_interface(
228
                self._save_thumbnail, [thumbpath, mem],
229
                self._update_img, [real_text, repl_start,
Thilo Molitor's avatar
Thilo Molitor committed
230 231 232 233 234 235 236
                                   repl_end, filepath, encrypted])

        # display thumbnail if already downloadeded
        # (but only if file also exists)
        elif os.path.exists(filepath) and os.path.exists(thumbpath):
            app.thread_interface(
                self._load_thumbnail, [thumbpath],
237
                self._update_img, [real_text, repl_start,
Thilo Molitor's avatar
Thilo Molitor committed
238 239 240 241 242 243 244 245 246 247 248
                                   repl_end, filepath, encrypted])

        # or download file, calculate thumbnail and finally display it
        else:
            if encrypted and not decryption_available:
                log.debug('Please install Crytography to decrypt pictures')
            else:
                # First get the http head request
                # which does not fetch data, just headers
                # then check the mime type and filesize
                if urlparts.scheme == 'aesgcm':
249
                    real_text = 'https://' + real_text[9:]
250
                verify = self.plugin.config['VERIFY']
Thilo Molitor's avatar
Thilo Molitor committed
251
                app.thread_interface(
252
                    get_http_head, [self.textview.account, real_text, verify],
253
                    self._check_mime_size, [real_text, repl_start, repl_end,
Thilo Molitor's avatar
Thilo Molitor committed
254 255
                                            filepaths, key, iv, encrypted])

256
    def _save_thumbnail(self, thumbpath, mem):
Thilo Molitor's avatar
Thilo Molitor committed
257 258 259
        size = self.plugin.config['PREVIEW_SIZE']

        try:
260 261 262
            loader = GdkPixbuf.PixbufLoader()
            loader.write(mem)
            loader.close()
263 264 265 266
            if loader.get_format().get_name() == 'gif':
                pixbuf = loader.get_animation()
            else:
                pixbuf = loader.get_pixbuf()
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
        except GLib.GError as error:
            log.info('Failed to load image using Gdk.Pixbuf')
            log.debug(error)

            if not PILLOW_AVAILABLE:
                log.info('Pillow not available')
                return
            # Try Pillow
            image = Image.open(BytesIO(mem)).convert("RGBA")
            array = GLib.Bytes.new(image.tobytes())
            width, height = image.size
            pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(
                array, GdkPixbuf.Colorspace.RGB, True,
                8, width, height, width * 4)

Thilo Molitor's avatar
Thilo Molitor committed
282 283
        try:
            self._create_path(os.path.dirname(thumbpath))
284 285
            thumbnail = pixbuf
            if isinstance(pixbuf, GdkPixbuf.PixbufAnimation):
286
                if size < pixbuf.get_width() or size < pixbuf.get_height():
287 288 289 290 291
                    resize_gif(mem, thumbpath, (size, size))
                    thumbnail = self._load_thumbnail(thumbpath)
                else:
                    self._write_file(thumbpath, mem)
            else:
292 293 294
                width, height = self._get_thumbnail_size(pixbuf, size)
                thumbnail = pixbuf.scale_simple(
                    width, height, GdkPixbuf.InterpType.BILINEAR)
295
                thumbnail.savev(thumbpath, 'png', [], [])
296
        except Exception as error:
Thilo Molitor's avatar
Thilo Molitor committed
297 298 299 300 301
            dialogs.ErrorDialog(
                _('Could not save file'),
                _('Exception raised while saving thumbnail '
                  'for image file (see error log for more '
                  'information)'),
302
                transient_for=app.app.get_active_window())
303
            log.exception(error)
304 305
            return
        return thumbnail
Thilo Molitor's avatar
Thilo Molitor committed
306

307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
    @staticmethod
    def _get_thumbnail_size(pixbuf, size):
        # Calculates the new thumbnail size while preserving the aspect ratio
        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)

        return image_width, image_height

324 325 326 327 328
    @staticmethod
    def _load_thumbnail(thumbpath):
        ext = os.path.splitext(thumbpath)[1]
        if ext == '.gif':
            return GdkPixbuf.PixbufAnimation.new_from_file(thumbpath)
329
        return GdkPixbuf.Pixbuf.new_from_file(thumbpath)
Thilo Molitor's avatar
Thilo Molitor committed
330

331 332
    @staticmethod
    def _write_file(path, data):
333
        log.info("Writing '%s' of size %d...", path, len(data))
Thilo Molitor's avatar
Thilo Molitor committed
334 335 336 337 338
        try:
            with open(path, "wb") as output_file:
                output_file.write(data)
                output_file.closed
        except Exception as e:
339
            log.error("Failed to write file '%s'!", path)
Thilo Molitor's avatar
Thilo Molitor committed
340 341
            raise

342
    def _update_img(self, pixbuf, url, repl_start, repl_end,
Thilo Molitor's avatar
Thilo Molitor committed
343
                    filepath, encrypted):
344
        if pixbuf is None:
Thilo Molitor's avatar
Thilo Molitor committed
345
            # If image could not be downloaded, URL is already displayed
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
            log.error('Could not download image for URL: %s', url)
            return

        urlparts = urlparse(url)
        filename = os.path.basename(urlparts.path)
        event_box = Gtk.EventBox()
        event_box.connect('button-press-event', self.on_button_press_event,
                          filepath, filename, url, encrypted)
        event_box.connect('enter-notify-event', self.on_enter_event)
        event_box.connect('leave-notify-event', self.on_leave_event)

        def add_to_textview():
            try:
                at_end = self.textview.at_the_end()

                buffer_ = repl_start.get_buffer()
                iter_ = buffer_.get_iter_at_mark(repl_start)
363
                buffer_.insert(iter_, "\n")
364
                anchor = buffer_.create_child_anchor(iter_)
365
                anchor.plaintext = url
366

367 368 369 370
                if isinstance(pixbuf, GdkPixbuf.PixbufAnimation):
                    image = Gtk.Image.new_from_animation(pixbuf)
                else:
                    image = Gtk.Image.new_from_pixbuf(pixbuf)
371
                event_box.set_tooltip_text(url)
372 373 374 375 376 377 378 379 380 381 382 383 384
                event_box.add(image)
                event_box.show_all()
                self.textview.tv.add_child_at_anchor(event_box, anchor)
                buffer_.delete(iter_,
                               buffer_.get_iter_at_mark(repl_end))

                if at_end:
                    GLib.idle_add(self.textview.scroll_to_end_iter)
            except Exception as ex:
                log.exception("Exception while loading %s: %s", url, ex)
            return False
        # add to mainloop --> make call threadsafe
        GLib.idle_add(add_to_textview)
Dicson's avatar
Dicson committed
385

Thilo Molitor's avatar
Thilo Molitor committed
386 387 388 389 390 391 392
    def _check_mime_size(self, tuple_arg,
                         url, repl_start, repl_end, filepaths,
                         key, iv, encrypted):
        file_mime, file_size = tuple_arg
        # Check if mime type is acceptable
        if file_mime == '' and file_size == 0:
            log.info("Failed to load HEAD Request for URL: '%s'"
393
                     "(see debug log for more info)", url)
Thilo Molitor's avatar
Thilo Molitor committed
394 395 396
            # URL is already displayed
            return
        if file_mime.lower() not in ACCEPTED_MIME_TYPES:
397 398
            log.info("Not accepted mime type '%s' for URL: '%s'",
                     file_mime.lower(), url)
Thilo Molitor's avatar
Thilo Molitor committed
399 400 401
            # URL is already displayed
            return
        # Check if file size is acceptable
402 403
        max_size = int(self.plugin.config['MAX_FILE_SIZE'])
        if file_size > max_size or file_size == 0:
404 405
            log.info("File size (%s) too big or unknown (zero) for URL: '%s'",
                     file_size, url)
Thilo Molitor's avatar
Thilo Molitor committed
406 407 408 409
            # URL is already displayed
            return

        attributes = {'src': url,
410
                      'verify': self.plugin.config['VERIFY'],
411
                      'max_size': max_size,
Thilo Molitor's avatar
Thilo Molitor committed
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
                      'filepaths': filepaths,
                      'key': key,
                      'iv': iv}

        app.thread_interface(
            self._download_image, [self.textview.account,
                                   attributes, encrypted],
            self._update_img, [url, repl_start, repl_end,
                               filepaths[0], encrypted])

    def _download_image(self, account, attributes, encrypted):
        filepath = attributes['filepaths'][0]
        thumbpath = attributes['filepaths'][1]
        key = attributes['key']
        iv = attributes['iv']
        mem, alt = get_http_file(account, attributes)

        # Decrypt file if necessary
        if encrypted:
            mem = self._aes_decrypt_fast(key, iv, mem)

        try:
            # Write file to harddisk
            self._write_file(filepath, mem)
        except Exception as e:
            dialogs.ErrorDialog(
                _('Could not save file'),
                _('Exception raised while saving image file'
                  ' (see error log for more information)'),
441
                transient_for=app.app.get_active_window())
Thilo Molitor's avatar
Thilo Molitor committed
442 443 444
            log.error(str(e))

        # Create thumbnail, write it to harddisk and return it
445
        return self._save_thumbnail(thumbpath, mem)
Thilo Molitor's avatar
Thilo Molitor committed
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482

    def _create_path(self, folder):
        if os.path.exists(folder):
            return
        log.debug("creating folder '%s'" % folder)
        os.mkdir(folder, 0o700)

    def _aes_decrypt_fast(self, key, iv, payload):
        # Use AES128 GCM with the given key and iv to decrypt the payload.
        if os.name == 'nt':
            be = backend
        else:
            be = default_backend()
        data = payload[:-16]
        tag = payload[-16:]
        decryptor = Cipher(
            algorithms.AES(key),
            GCM(iv, tag=tag),
            backend=be).decryptor()
        return decryptor.update(data) + decryptor.finalize()

    def make_rightclick_menu(self, event, data):
        xml = Gtk.Builder()
        xml.set_translation_domain('gajim_plugins')
        xml.add_from_file(self.plugin.local_file_path('context_menu.ui'))
        menu = xml.get_object('context_menu')

        open_menuitem = xml.get_object('open_menuitem')
        save_as_menuitem = xml.get_object('save_as_menuitem')
        copy_link_location_menuitem = \
            xml.get_object('copy_link_location_menuitem')
        open_link_in_browser_menuitem = \
            xml.get_object('open_link_in_browser_menuitem')
        open_file_in_browser_menuitem = \
            xml.get_object('open_file_in_browser_menuitem')
        extras_separator = \
            xml.get_object('extras_separator')
483

Thilo Molitor's avatar
Thilo Molitor committed
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515
        if data["encrypted"]:
            open_link_in_browser_menuitem.hide()
        if app.config.get('autodetect_browser_mailer') \
                or app.config.get('custombrowser') == '':
            extras_separator.hide()
            open_file_in_browser_menuitem.hide()

        id_ = open_menuitem.connect(
            'activate', self.on_open_menuitem_activate, data)
        self.handlers[id_] = open_menuitem
        id_ = save_as_menuitem.connect(
            'activate', self.on_save_as_menuitem_activate, data)
        self.handlers[id_] = save_as_menuitem
        id_ = copy_link_location_menuitem.connect(
            'activate', self.on_copy_link_location_menuitem_activate, data)
        self.handlers[id_] = copy_link_location_menuitem
        id_ = open_link_in_browser_menuitem.connect(
            'activate', self.on_open_link_in_browser_menuitem_activate, data)
        self.handlers[id_] = open_link_in_browser_menuitem
        id_ = open_file_in_browser_menuitem.connect(
            'activate', self.on_open_file_in_browser_menuitem_activate, data)
        self.handlers[id_] = open_file_in_browser_menuitem

        return menu

    def on_open_menuitem_activate(self, menu, data):
        filepath = data["filepath"]
        helpers.launch_file_manager(filepath)

    def on_save_as_menuitem_activate(self, menu, data):
        filepath = data["filepath"]
        original_filename = data["original_filename"]
516

Thilo Molitor's avatar
Thilo Molitor committed
517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581
        def on_continue(response, target_path):
            if response < 0:
                return
            shutil.copy(filepath, target_path)
            dialog.destroy()

        def on_ok(widget):
            target_path = dialog.get_filename()
            if os.path.exists(target_path):
                # check if we have write permissions
                if not os.access(target_path, os.W_OK):
                    file_name = os.path.basename(target_path)
                    dialogs.ErrorDialog(
                        _('Cannot overwrite existing file "%s"') % file_name,
                        _('A file with this name already exists and you do '
                          'not have permission to overwrite it.'))
                    return
                dialog2 = dialogs.FTOverwriteConfirmationDialog(
                    _('This file already exists'),
                    _('What do you want to do?'),
                    propose_resume=False,
                    on_response=(on_continue, target_path),
                    transient_for=dialog)
                dialog2.set_destroy_with_parent(True)
            else:
                dirname = os.path.dirname(target_path)
                if not os.access(dirname, os.W_OK):
                    dialogs.ErrorDialog(
                        _('Directory "%s" is not writable') % dirname,
                        _('You do not have permission to '
                          'create files in this directory.'))
                    return
                on_continue(0, target_path)

        def on_cancel(widget):
            dialog.destroy()

        dialog = dialogs.FileChooserDialog(
            title_text=_('Save Image as...'),
            action=Gtk.FileChooserAction.SAVE,
            buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                     Gtk.STOCK_SAVE, Gtk.ResponseType.OK),
            default_response=Gtk.ResponseType.OK,
            current_folder=app.config.get('last_save_dir'),
            on_response_ok=on_ok,
            on_response_cancel=on_cancel)

        dialog.set_current_name(original_filename)
        dialog.connect('delete-event', lambda widget, event:
                       on_cancel(widget))

    def on_copy_link_location_menuitem_activate(self, menu, data):
        url = data["url"]
        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        clipboard.set_text(url, -1)
        clipboard.store()

    def on_open_link_in_browser_menuitem_activate(self, menu, data):
        url = data["url"]
        if data["encrypted"]:
            dialogs.ErrorDialog(
                _('Encrypted file'),
                _('You cannot open encrypted files in your '
                  'browser directly. Try "Open Downloaded File '
                  'in Browser" instead.'),
582
                transient_for=app.app.get_active_window())
Thilo Molitor's avatar
Thilo Molitor committed
583 584
        else:
            helpers.launch_browser_mailer('url', url)
585

Thilo Molitor's avatar
Thilo Molitor committed
586 587 588 589 590 591 592 593 594 595 596
    def on_open_file_in_browser_menuitem_activate(self, menu, data):
        if os.name == "nt":
            filepath = "file://" + os.path.abspath(data["filepath"])
        else:
            filepath = "file://" + data["filepath"]
        if app.config.get('autodetect_browser_mailer') \
                or app.config.get('custombrowser') == '':
            dialogs.ErrorDialog(
                _('Cannot open downloaded file in browser'),
                _('You have to set a custom browser executable '
                  'in your gajim settings for this to work.'),
597
                transient_for=app.app.get_active_window())
Thilo Molitor's avatar
Thilo Molitor committed
598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631
            return
        command = app.config.get('custombrowser')
        command = helpers.build_command(command, filepath)
        try:
            helpers.exec_command(command)
        except Exception:
            pass

    # 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.TextWindowType.TEXT).set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2))

    # Change mouse pointer to default when mouse leaves the eventbox
    def on_leave_event(self, eb, event):
        self.textview.tv.get_window(
            Gtk.TextWindowType.TEXT).set_cursor(Gdk.Cursor(Gdk.CursorType.XTERM))

    def on_button_press_event(self, eb, event, filepath,
                              original_filename, url, encrypted):
        data = {"filepath": filepath,
                "original_filename": original_filename,
                "url": url,
                "encrypted": encrypted}
        # left click
        if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 1:
            method = getattr(self, "on_"
                             + self.plugin.config['LEFTCLICK_ACTION']
                             + "_activate")
            method(event, data)
        # right klick
        elif event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:
            menu = self.make_rightclick_menu(event, data)
632 633
            # menu.attach_to_widget(self.tv, None)
            # menu.popup(None, None, None, event.button, event.time)
Thilo Molitor's avatar
Thilo Molitor committed
634 635
            menu.popup_at_pointer(event)

Dicson's avatar
Dicson committed
636 637
    def disconnect_from_chat_control(self):
        pass