url_image_preview.py 28 KB
Newer Older
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/>.
##
17 18

import os
Thilo Molitor's avatar
Thilo Molitor committed
19 20
import hashlib
import binascii
21
import logging
22
import math
Thilo Molitor's avatar
Thilo Molitor committed
23
from urllib.parse import urlparse
24
from urllib.parse import unquote
Thilo Molitor's avatar
Thilo Molitor committed
25 26
from io import BytesIO
import shutil
27
from functools import partial
28

29 30
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf

31 32
from gajim.common import app
from gajim.common import helpers
Thilo Molitor's avatar
Thilo Molitor committed
33
from gajim.common import configpaths
34

Thilo Molitor's avatar
Thilo Molitor committed
35
from gajim import dialogs
36
from gajim import gtkgui_helpers
37

38 39
from gajim.plugins import GajimPlugin
from gajim.plugins.helpers import log_calls
40 41
from gajim.plugins.plugins_i18n import _

42 43
from url_image_preview.http_functions import get_http_head, get_http_file
from url_image_preview.config_dialog import UrlImagePreviewConfigDialog
44

45
from gajim.gtk.filechoosers import FileSaveDialog
46
from gajim.gtk.util import get_cursor
47

48

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

51
ERROR_MSG = None
Thilo Molitor's avatar
Thilo Molitor committed
52 53
try:
    from PIL import Image
54
    from url_image_preview.resize_gif import resize_gif
Thilo Molitor's avatar
Thilo Molitor committed
55 56
except:
    log.debug('Pillow not available')
57
    ERROR_MSG = 'Please install python-pillow'
Thilo Molitor's avatar
Thilo Molitor committed
58 59 60 61 62 63 64 65 66 67

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
68
except Exception:
Thilo Molitor's avatar
Thilo Molitor committed
69 70
    DEP_MSG = 'For preview of encrypted images, ' \
              'please install python-cryptography!'
71
    log.exception('Error')
Thilo Molitor's avatar
Thilo Molitor committed
72 73 74 75 76
    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')
77 78 79 80 81


class UrlImagePreviewPlugin(GajimPlugin):
    @log_calls('UrlImagePreviewPlugin')
    def init(self):
82 83 84 85 86 87
        if ERROR_MSG:
            self.activatable = False
            self.available_text = ERROR_MSG
            self.config_dialog = None
            return

Thilo Molitor's avatar
Thilo Molitor committed
88 89
        if not decryption_available:
            self.available_text = DEP_MSG
90
        self.config_dialog = partial(UrlImagePreviewConfigDialog, self)
91
        self.gui_extension_points = {
Thilo Molitor's avatar
Thilo Molitor committed
92 93
            'chat_control_base': (self.connect_with_chat_control,
                                  self.disconnect_from_chat_control),
94
            'history_window':
95
                (self.connect_with_history, self.disconnect_from_history),
96
            'print_real_text': (self.print_real_text, None), }
97
        self.config_default_values = {
Thilo Molitor's avatar
Thilo Molitor committed
98
            'PREVIEW_SIZE': (150, 'Preview size(10-512)'),
99
            'MAX_FILE_SIZE': (5242880, 'Max file size for image preview'),
100
            'ALLOW_ALL_IMAGES': (False, ''),
101
            'LEFTCLICK_ACTION': ('open_menuitem', 'Open'),
102
            'ANONYMOUS_MUC': (False, ''),
103
            'GEO_PREVIEW_PROVIDER': ('Google', 'Google Maps'),
104
            'VERIFY': (True, ''),}
Thilo Molitor's avatar
Thilo Molitor committed
105
        self.controls = {}
106
        self.history_window_control = None
Thilo Molitor's avatar
Thilo Molitor committed
107

108 109
    @log_calls('UrlImagePreviewPlugin')
    def connect_with_chat_control(self, chat_control):
Thilo Molitor's avatar
Thilo Molitor committed
110 111 112 113
        account = chat_control.contact.account.name
        jid = chat_control.contact.jid
        if account not in self.controls:
            self.controls[account] = {}
114
        self.controls[account][jid] = Base(self, chat_control.conv_textview)
115 116 117

    @log_calls('UrlImagePreviewPlugin')
    def disconnect_from_chat_control(self, chat_control):
Thilo Molitor's avatar
Thilo Molitor committed
118 119
        account = chat_control.contact.account.name
        jid = chat_control.contact.jid
120
        self.controls[account][jid].deinit_handlers()
Thilo Molitor's avatar
Thilo Molitor committed
121
        del self.controls[account][jid]
122

123 124 125 126 127
    @log_calls('UrlImagePreviewPlugin')
    def connect_with_history(self, history_window):
        if self.history_window_control:
            self.history_window_control.deinit_handlers()
        self.history_window_control = Base(
128
            self, history_window.history_textview)
129 130 131 132 133 134

    @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
135

136 137
    def print_real_text(self, tv, real_text, text_tags, graphics,
                        iter_, additional_data):
138
        if tv.used_in_history_window and self.history_window_control:
139 140 141
            self.history_window_control.print_real_text(
                real_text, text_tags, graphics, iter_, additional_data)

Thilo Molitor's avatar
Thilo Molitor committed
142 143
        account = tv.account
        for jid in self.controls[account]:
144
            if self.controls[account][jid].textview != tv:
145
                continue
146 147
            self.controls[account][jid].print_real_text(
                real_text, text_tags, graphics, iter_, additional_data)
Thilo Molitor's avatar
Thilo Molitor committed
148
            return
149 150 151


class Base(object):
152
    def __init__(self, plugin, textview):
153
        self.plugin = plugin
154
        self.textview = textview
Thilo Molitor's avatar
Thilo Molitor committed
155
        self.handlers = {}
156

157
        self.directory = os.path.join(configpaths.get('MY_DATA'),
Thilo Molitor's avatar
Thilo Molitor committed
158
                                      'downloads')
159
        self.thumbpath = os.path.join(configpaths.get('MY_CACHE'),
Thilo Molitor's avatar
Thilo Molitor committed
160 161 162 163 164
                                      'downloads.thumb')

        try:
            self._create_path(self.directory)
            self._create_path(self.thumbpath)
165
        except Exception:
Thilo Molitor's avatar
Thilo Molitor committed
166 167 168
            log.error("Error creating download and/or thumbnail folder!")
            raise

169
    def deinit_handlers(self):
Thilo Molitor's avatar
Thilo Molitor committed
170 171 172 173 174 175 176
        # 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]

177 178 179
    def print_real_text(self, real_text, text_tags, graphics, iter_,
                        additional_data):

180 181
        if len(real_text.split(' ')) > 1:
            # urlparse dont recognises spaces as URL delimiter
182
            log.debug('Url with text will not be displayed: %s', real_text)
183 184
            return

185
        urlparts = urlparse(unquote(real_text))
186
        if not self._accept_uri(urlparts, real_text, additional_data):
187
            return
Thilo Molitor's avatar
Thilo Molitor committed
188 189 190 191

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

192
        buffer_ = self.textview.tv.get_buffer()
Thilo Molitor's avatar
Thilo Molitor committed
193 194 195 196 197 198
        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)
199
        buffer_.insert_with_tags(iter_, real_text,
Thilo Molitor's avatar
Thilo Molitor committed
200 201 202
            *[(ttt.lookup(t) if isinstance(t, str) else t) for t in ["url"]])
        repl_end = buffer_.create_mark(None, iter_, True)

203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 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
        # Handle geo:-URIs
        if real_text.startswith('geo:'):
            if self.plugin.config['GEO_PREVIEW_PROVIDER'] == 'no_preview':
                return
            size = self.plugin.config['PREVIEW_SIZE']
            geo_provider = self.plugin.config['GEO_PREVIEW_PROVIDER']
            key = ''
            iv = ''
            encrypted = False
            ext = '.png'
            color = 'blue'
            zoom = 16
            location = real_text[4:]
            lat, _, lon = location.partition(',')
            if lon == '':
                return

            filename = 'location_' + geo_provider + '_' \
                + location.replace(',', '_').replace('.', '-')
            newfilename = filename + ext
            thumbfilename = filename + '_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]

            # Google
            if geo_provider == 'Google':
                url = 'https://maps.googleapis.com/maps/api/staticmap?' \
                      'center={}&zoom={}&size={}x{}&markers=color:{}' \
                      '|label:.|{}'.format(location, zoom, size, size,
                                           color, location)
                weburl = 'https://www.google.com/maps/place/{}' \
                         .format(location)
                real_text = url
            else:
                # OpenStreetMap / MapQuest
                apikey = 'F7x36jLVv2hiANVAXmhwvUB044XvGASh'

                url = 'https://open.mapquestapi.com/staticmap/v4/' \
                      'getmap?key={}&center={}&zoom={}&size={},{}&type=map' \
                      '&imagetype=png&pois={},{}&scalebar=false' \
                      .format(apikey, location, zoom, size, size, color,
246
                              location)
247 248 249 250
                weburl = 'http://www.openstreetmap.org/' \
                         '?mlat={}&mlon={}#map={}/{}/{}&layers=N' \
                         .format(lat, lon, zoom, lat, lon)
                real_text = url
251 252 253 254 255
        else:
            weburl = real_text
            filename = os.path.basename(urlparts.path)
            ext = os.path.splitext(filename)[1]
            name = os.path.splitext(filename)[0]
256 257 258 259 260 261
            if len(name) > 90:
                # Many Filesystems have a limit on filename length
                # Most have 255, some encrypted ones only 143
                # We add around 50 chars for the hash,
                # so the filename should not exceed 90
                name = name[:90]
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284
            namehash = hashlib.sha1(real_text.encode('utf-8')).hexdigest()
            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
285

Thilo Molitor's avatar
Thilo Molitor committed
286 287
        # file exists but thumbnail got deleted
        if os.path.exists(filepath) and not os.path.exists(thumbpath):
288 289
            if urlparts.scheme == 'geo':
                    real_text = weburl
Thilo Molitor's avatar
Thilo Molitor committed
290 291 292
            with open(filepath, 'rb') as f:
                mem = f.read()
            app.thread_interface(
293
                self._save_thumbnail, [thumbpath, mem],
294
                self._update_img, [real_text, repl_start,
Thilo Molitor's avatar
Thilo Molitor committed
295 296 297 298 299
                                   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):
300 301
            if urlparts.scheme == 'geo':
                    real_text = weburl
Thilo Molitor's avatar
Thilo Molitor committed
302 303
            app.thread_interface(
                self._load_thumbnail, [thumbpath],
304
                self._update_img, [real_text, repl_start,
Thilo Molitor's avatar
Thilo Molitor committed
305 306 307 308 309 310 311 312 313 314 315
                                   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':
316
                    real_text = 'https://' + real_text[9:]
317
                verify = self.plugin.config['VERIFY']
Thilo Molitor's avatar
Thilo Molitor committed
318
                app.thread_interface(
319
                    get_http_head, [self.textview.account, real_text, verify],
320 321 322
                    self._check_mime_size, [real_text, weburl, repl_start,
                                            repl_end, filepaths, key, iv,
                                            encrypted])
Thilo Molitor's avatar
Thilo Molitor committed
323

324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
    def _accept_uri(self, urlparts, real_text, additional_data):
        try:
            oob_url = additional_data["gajim"]["oob_url"]
        except (KeyError, AttributeError):
            oob_url = None

        if not urlparts.netloc:
            log.info('No netloc found in URL %s', real_text)
            return False

        # geo
        if urlparts.scheme == "geo":
            if self.plugin.config['GEO_PREVIEW_PROVIDER'] == 'no_preview':
                log.info('geo: link preview is disabled')
                return False
339 340
            return True

341 342 343 344 345 346 347
        # aesgcm
        if urlparts.scheme == 'aesgcm':
            return True

        # https
        if urlparts.scheme == 'https':
            if real_text == oob_url or self.plugin.config['ALLOW_ALL_IMAGES']:
348
                return True
349
            log.info('Incorrect oob data found')
350 351
            return False

352 353 354
        log.info('Not supported URI scheme found: %s', real_text)
        return False

355
    def _save_thumbnail(self, thumbpath, mem):
Thilo Molitor's avatar
Thilo Molitor committed
356 357 358
        size = self.plugin.config['PREVIEW_SIZE']

        try:
359 360 361
            loader = GdkPixbuf.PixbufLoader()
            loader.write(mem)
            loader.close()
362 363 364 365
            if loader.get_format().get_name() == 'gif':
                pixbuf = loader.get_animation()
            else:
                pixbuf = loader.get_pixbuf()
366 367 368 369 370 371 372 373 374 375 376 377
        except GLib.GError as error:
            log.info('Failed to load image using Gdk.Pixbuf')
            log.debug(error)

            # 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
378 379
        try:
            self._create_path(os.path.dirname(thumbpath))
380 381
            thumbnail = pixbuf
            if isinstance(pixbuf, GdkPixbuf.PixbufAnimation):
382
                if size < pixbuf.get_width() or size < pixbuf.get_height():
383 384 385 386 387
                    resize_gif(mem, thumbpath, (size, size))
                    thumbnail = self._load_thumbnail(thumbpath)
                else:
                    self._write_file(thumbpath, mem)
            else:
388 389 390
                width, height = self._get_thumbnail_size(pixbuf, size)
                thumbnail = pixbuf.scale_simple(
                    width, height, GdkPixbuf.InterpType.BILINEAR)
391
                thumbnail.savev(thumbpath, 'png', [], [])
392
        except Exception as error:
393 394
            GLib.idle_add(
                self._raise_error_dialog,
Thilo Molitor's avatar
Thilo Molitor committed
395 396 397
                _('Could not save file'),
                _('Exception raised while saving thumbnail '
                  'for image file (see error log for more '
398
                  'information)'))
399
            log.exception(error)
400 401
            return
        return thumbnail
Thilo Molitor's avatar
Thilo Molitor committed
402

403 404 405 406 407 408 409 410
    @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:
411
                image_height = math.ceil((size / float(image_width) * image_height))
412 413 414
                image_width = int(size)
        else:
            if image_height > size:
415
                image_width = math.ceil((size / float(image_height) * image_width))
416 417 418 419
                image_height = int(size)

        return image_width, image_height

420 421 422 423 424
    @staticmethod
    def _load_thumbnail(thumbpath):
        ext = os.path.splitext(thumbpath)[1]
        if ext == '.gif':
            return GdkPixbuf.PixbufAnimation.new_from_file(thumbpath)
425
        return GdkPixbuf.Pixbuf.new_from_file(thumbpath)
Thilo Molitor's avatar
Thilo Molitor committed
426

427 428
    @staticmethod
    def _write_file(path, data):
429
        log.info("Writing '%s' of size %d...", path, len(data))
Thilo Molitor's avatar
Thilo Molitor committed
430 431 432 433 434
        try:
            with open(path, "wb") as output_file:
                output_file.write(data)
                output_file.closed
        except Exception as e:
435
            log.error("Failed to write file '%s'!", path)
Thilo Molitor's avatar
Thilo Molitor committed
436 437
            raise

438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
    def _get_at_end(self):
        try:
            # Gajim 1.0.0
            return self.textview.at_the_end()
        except AttributeError:
            # Gajim 1.0.1
            return self.textview.autoscroll

    def _scroll_to_end(self):
        try:
            # Gajim 1.0.0
            self.textview.scroll_to_end_iter()
        except AttributeError:
            # Gajim 1.0.1
            self.textview.scroll_to_end()

454
    def _update_img(self, pixbuf, url, repl_start, repl_end,
Thilo Molitor's avatar
Thilo Molitor committed
455
                    filepath, encrypted):
456
        if pixbuf is None:
Thilo Molitor's avatar
Thilo Molitor committed
457
            # If image could not be downloaded, URL is already displayed
458 459 460
            log.error('Could not download image for URL: %s', url)
            return

461
        urlparts = urlparse(unquote(url))
462
        filename = os.path.basename(urlparts.path)
463 464 465
        if os.path.basename(filepath).startswith('location_'):
            filename = os.path.basename(filepath)

466 467
        def add_to_textview():
            try:
468
                at_end = self._get_at_end()
469 470 471

                buffer_ = repl_start.get_buffer()
                iter_ = buffer_.get_iter_at_mark(repl_start)
472
                buffer_.insert(iter_, "\n")
473
                anchor = buffer_.create_child_anchor(iter_)
474
                anchor.plaintext = url
475

476 477 478
                image = self._create_clickable_image(pixbuf, url)

                self.textview.tv.add_child_at_anchor(image, anchor)
479 480 481
                buffer_.delete(iter_,
                               buffer_.get_iter_at_mark(repl_end))

482 483 484 485 486
                image.connect(
                    'button-press-event', self.on_button_press_event,
                    filepath, filename, url, encrypted)
                image.get_window().set_cursor(get_cursor('HAND2'))

487
                if at_end:
488
                    self._scroll_to_end()
489 490 491 492 493
            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)
494

495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512
    def _create_clickable_image(self, pixbuf, url):
        if isinstance(pixbuf, GdkPixbuf.PixbufAnimation):
            image = Gtk.Image.new_from_animation(pixbuf)
        else:
            image = Gtk.Image.new_from_pixbuf(pixbuf)

        css = '''#Preview {
        box-shadow: 0px 0px 3px 0px alpha(@theme_text_color, 0.2);
        margin: 5px 10px 5px 10px; }'''
        gtkgui_helpers.add_css_to_widget(image, css)
        image.set_name('Preview')

        event_box = Gtk.EventBox()
        event_box.set_tooltip_text(url)
        event_box.add(image)
        event_box.show_all()
        return event_box

Thilo Molitor's avatar
Thilo Molitor committed
513
    def _check_mime_size(self, tuple_arg,
514
                         url, weburl, repl_start, repl_end, filepaths,
Thilo Molitor's avatar
Thilo Molitor committed
515 516 517
                         key, iv, encrypted):
        file_mime, file_size = tuple_arg
        # Check if mime type is acceptable
518 519 520
        if not file_mime or not file_size:
            log.info("Failed to load HEAD Request for URL: '%s' "
                     "mime: %s, size: %s", url, file_mime, file_size)
Thilo Molitor's avatar
Thilo Molitor committed
521 522 523
            # URL is already displayed
            return
        if file_mime.lower() not in ACCEPTED_MIME_TYPES:
524 525
            log.info("Not accepted mime type '%s' for URL: '%s'",
                     file_mime.lower(), url)
Thilo Molitor's avatar
Thilo Molitor committed
526 527 528
            # URL is already displayed
            return
        # Check if file size is acceptable
529 530
        max_size = int(self.plugin.config['MAX_FILE_SIZE'])
        if file_size > max_size or file_size == 0:
531 532
            log.info("File size (%s) too big or unknown (zero) for URL: '%s'",
                     file_size, url)
Thilo Molitor's avatar
Thilo Molitor committed
533 534 535 536
            # URL is already displayed
            return

        attributes = {'src': url,
537
                      'verify': self.plugin.config['VERIFY'],
538
                      'max_size': max_size,
Thilo Molitor's avatar
Thilo Molitor committed
539 540 541 542 543 544 545
                      'filepaths': filepaths,
                      'key': key,
                      'iv': iv}

        app.thread_interface(
            self._download_image, [self.textview.account,
                                   attributes, encrypted],
546
            self._update_img, [weburl, repl_start, repl_end,
Thilo Molitor's avatar
Thilo Molitor committed
547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563
                               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:
564 565
            GLib.idle_add(
                self._raise_error_dialog,
Thilo Molitor's avatar
Thilo Molitor committed
566 567
                _('Could not save file'),
                _('Exception raised while saving image file'
568
                  ' (see error log for more information)'))
Thilo Molitor's avatar
Thilo Molitor committed
569 570 571
            log.error(str(e))

        # Create thumbnail, write it to harddisk and return it
572
        return self._save_thumbnail(thumbpath, mem)
Thilo Molitor's avatar
Thilo Molitor committed
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609

    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')
610

Thilo Molitor's avatar
Thilo Molitor committed
611 612 613 614 615 616 617 618 619 620
        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
621 622
        id_ = save_as_menuitem.connect(
            'activate', self.on_save_as_menuitem_activate_new, data)
Thilo Molitor's avatar
Thilo Molitor committed
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637
        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"]
638 639 640 641 642
        original_filename = data["original_filename"]
        url = data["url"]
        if original_filename.startswith('location_'):
            helpers.launch_browser_mailer('url', url)
            return
Thilo Molitor's avatar
Thilo Molitor committed
643 644
        helpers.launch_file_manager(filepath)

645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663
    def on_save_as_menuitem_activate_new(self, menu, data):
        filepath = data["filepath"]
        original_filename = data["original_filename"]

        def on_ok(target_path):
            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
            shutil.copy(filepath, target_path)

        FileSaveDialog(on_ok,
                       path=app.config.get('last_save_dir'),
                       file_name=original_filename,
                       transient_for=app.app.get_active_window())

Thilo Molitor's avatar
Thilo Molitor committed
664 665 666 667 668 669 670 671 672 673 674 675 676 677
    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.'),
678
                transient_for=app.app.get_active_window())
Thilo Molitor's avatar
Thilo Molitor committed
679 680
        else:
            helpers.launch_browser_mailer('url', url)
681

Thilo Molitor's avatar
Thilo Molitor committed
682 683 684 685 686 687 688 689 690 691 692
    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.'),
693
                transient_for=app.app.get_active_window())
Thilo Molitor's avatar
Thilo Molitor committed
694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716
            return
        command = app.config.get('custombrowser')
        command = helpers.build_command(command, filepath)
        try:
            helpers.exec_command(command)
        except Exception:
            pass

    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)
717 718
            # menu.attach_to_widget(self.tv, None)
            # menu.popup(None, None, None, event.button, event.time)
Thilo Molitor's avatar
Thilo Molitor committed
719 720
            menu.popup_at_pointer(event)

721 722 723 724 725 726 727
    @staticmethod
    def _raise_error_dialog(pritext, sectext):
        # Used by methods that run in a different thread
        dialogs.ErrorDialog(pritext,
                            sectext,
                            transient_for=app.app.get_active_window())

728 729
    def disconnect_from_chat_control(self):
        pass