0 | |
# -*- coding: utf-8 -*-
|
1 | |
##
|
2 | |
## This file is part of Gajim.
|
3 | |
##
|
4 | |
## Gajim is free software; you can redistribute it and/or modify
|
5 | |
## it under the terms of the GNU General Public License as published
|
6 | |
## by the Free Software Foundation; version 3 only.
|
7 | |
##
|
8 | |
## Gajim is distributed in the hope that it will be useful,
|
9 | |
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10 | |
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11 | |
## GNU General Public License for more details.
|
12 | |
##
|
13 | |
## You should have received a copy of the GNU General Public License
|
14 | |
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
15 | |
##
|
|
0 |
# This file is part of Image Preview Gajim Plugin.
|
|
1 |
#
|
|
2 |
# Image Preview Gajim Plugin is free software;
|
|
3 |
# you can redistribute it and/or modify
|
|
4 |
# it under the terms of the GNU General Public License as published
|
|
5 |
# by the Free Software Foundation; version 3 only.
|
|
6 |
#
|
|
7 |
# Image Preview Gajim Plugin is distributed in the hope that it will be useful,
|
|
8 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
9 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
10 |
# GNU General Public License for more details.
|
|
11 |
#
|
|
12 |
# You should have received a copy of the GNU General Public License
|
|
13 |
# along with Image Preview Gajim Plugin.
|
|
14 |
# If not, see <http://www.gnu.org/licenses/>.
|
16 | 15 |
|
17 | 16 |
import os
|
18 | |
import hashlib
|
19 | |
import binascii
|
20 | 17 |
import logging
|
21 | |
import math
|
|
18 |
import shutil
|
|
19 |
from pathlib import Path
|
|
20 |
from functools import partial
|
22 | 21 |
from urllib.parse import urlparse
|
23 | 22 |
from urllib.parse import unquote
|
24 | |
from io import BytesIO
|
25 | |
import shutil
|
26 | |
from functools import partial
|
27 | |
|
28 | |
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf
|
|
23 |
|
|
24 |
from gi.repository import Gtk
|
|
25 |
from gi.repository import Gdk
|
|
26 |
from gi.repository import GLib
|
|
27 |
from gi.repository import Soup
|
29 | 28 |
|
30 | 29 |
from gajim.common import app
|
31 | |
from gajim.common import helpers
|
32 | 30 |
from gajim.common import configpaths
|
33 | |
|
34 | |
from gajim import dialogs
|
35 | |
from gajim import gtkgui_helpers
|
|
31 |
from gajim.common.helpers import open_file
|
|
32 |
from gajim.common.helpers import open_uri
|
|
33 |
from gajim.common.helpers import write_file_async
|
|
34 |
from gajim.common.helpers import load_file_async
|
|
35 |
from gajim.common.helpers import get_tls_error_phrase
|
|
36 |
from gajim.gtk.dialogs import ErrorDialog
|
|
37 |
from gajim.gtk.filechoosers import FileSaveDialog
|
|
38 |
from gajim.gtk.util import load_icon
|
|
39 |
from gajim.gtk.util import get_monitor_scale_factor
|
36 | 40 |
|
37 | 41 |
from gajim.plugins import GajimPlugin
|
38 | |
from gajim.plugins.helpers import log_calls
|
|
42 |
from gajim.plugins.helpers import get_builder
|
39 | 43 |
from gajim.plugins.plugins_i18n import _
|
40 | 44 |
|
41 | |
from url_image_preview.http_functions import get_http_head, get_http_file
|
42 | 45 |
from url_image_preview.config_dialog import UrlImagePreviewConfigDialog
|
43 | 46 |
|
44 | |
from gajim.gtk.filechoosers import FileSaveDialog
|
45 | |
from gajim.gtk.util import get_cursor
|
46 | |
|
47 | |
|
48 | |
log = logging.getLogger('gajim.plugin_system.preview')
|
|
47 |
|
|
48 |
log = logging.getLogger('gajim.p.preview')
|
49 | 49 |
|
50 | 50 |
ERROR_MSG = None
|
51 | 51 |
try:
|
52 | |
from PIL import Image
|
53 | |
from url_image_preview.resize_gif import resize_gif
|
54 | |
except:
|
55 | |
log.debug('Pillow not available')
|
56 | |
ERROR_MSG = 'Please install python-pillow'
|
|
52 |
from PIL import Image # pylint: disable=unused-import
|
|
53 |
except ImportError:
|
|
54 |
log.error('Pillow not available')
|
|
55 |
ERROR_MSG = _('Please install python-pillow')
|
57 | 56 |
|
58 | 57 |
try:
|
59 | |
if os.name == 'nt':
|
60 | |
from cryptography.hazmat.backends.openssl import backend
|
61 | |
else:
|
62 | |
from cryptography.hazmat.backends import default_backend
|
63 | |
from cryptography.hazmat.primitives.ciphers import Cipher
|
64 | |
from cryptography.hazmat.primitives.ciphers import algorithms
|
65 | |
from cryptography.hazmat.primitives.ciphers.modes import GCM
|
66 | |
decryption_available = True
|
|
58 |
import cryptography # pylint: disable=unused-import
|
67 | 59 |
except Exception:
|
68 | |
DEP_MSG = 'For preview of encrypted images, ' \
|
69 | |
'please install python-cryptography!'
|
70 | |
log.exception('Error')
|
71 | |
log.info('Decryption/Encryption disabled due to errors')
|
72 | |
decryption_available = False
|
73 | |
|
74 | |
ACCEPTED_MIME_TYPES = ('image/png', 'image/jpeg', 'image/gif', 'image/raw',
|
75 | |
'image/svg+xml', 'image/x-ms-bmp')
|
|
60 |
ERROR_MSG = _('Please install python-cryptography')
|
|
61 |
log.error('python-cryptography not available')
|
|
62 |
|
|
63 |
# pylint: disable=ungrouped-imports
|
|
64 |
if ERROR_MSG is None:
|
|
65 |
from url_image_preview.utils import aes_decrypt
|
|
66 |
from url_image_preview.utils import get_image_paths
|
|
67 |
from url_image_preview.utils import split_geo_uri
|
|
68 |
from url_image_preview.utils import parse_fragment
|
|
69 |
from url_image_preview.utils import create_thumbnail
|
|
70 |
from url_image_preview.utils import pixbuf_from_data
|
|
71 |
from url_image_preview.utils import create_clickable_image
|
|
72 |
from url_image_preview.utils import filename_from_uri
|
|
73 |
# pylint: enable=ungrouped-imports
|
|
74 |
|
|
75 |
ACCEPTED_MIME_TYPES = [
|
|
76 |
'image/png',
|
|
77 |
'image/jpeg',
|
|
78 |
'image/gif',
|
|
79 |
'image/raw',
|
|
80 |
'image/svg+xml',
|
|
81 |
'image/x-ms-bmp',
|
|
82 |
]
|
76 | 83 |
|
77 | 84 |
|
78 | 85 |
class UrlImagePreviewPlugin(GajimPlugin):
|
79 | |
@log_calls('UrlImagePreviewPlugin')
|
80 | 86 |
def init(self):
|
|
87 |
# pylint: disable=attribute-defined-outside-init
|
81 | 88 |
if ERROR_MSG:
|
82 | 89 |
self.activatable = False
|
83 | 90 |
self.available_text = ERROR_MSG
|
84 | 91 |
self.config_dialog = None
|
85 | 92 |
return
|
86 | 93 |
|
87 | |
if not decryption_available:
|
88 | |
self.available_text = DEP_MSG
|
89 | 94 |
self.config_dialog = partial(UrlImagePreviewConfigDialog, self)
|
|
95 |
|
90 | 96 |
self.gui_extension_points = {
|
91 | |
'chat_control_base': (self.connect_with_chat_control,
|
92 | |
self.disconnect_from_chat_control),
|
93 | |
'history_window':
|
94 | |
(self.connect_with_history, self.disconnect_from_history),
|
95 | |
'print_real_text': (self.print_real_text, None), }
|
|
97 |
'chat_control_base': (self._on_connect_chat_control_base,
|
|
98 |
self._on_disconnect_chat_control_base),
|
|
99 |
'history_window': (self._on_connect_history_window,
|
|
100 |
self._on_disconnect_history_window),
|
|
101 |
'print_real_text': (self._print_real_text, None), }
|
|
102 |
|
96 | 103 |
self.config_default_values = {
|
97 | |
'PREVIEW_SIZE': (150, 'Preview size(10-512)'),
|
|
104 |
'PREVIEW_SIZE': (150, 'Preview size (100-1000)'),
|
98 | 105 |
'MAX_FILE_SIZE': (5242880, 'Max file size for image preview'),
|
99 | 106 |
'ALLOW_ALL_IMAGES': (False, ''),
|
100 | 107 |
'LEFTCLICK_ACTION': ('open_menuitem', 'Open'),
|
101 | 108 |
'ANONYMOUS_MUC': (False, ''),
|
102 | |
'GEO_PREVIEW_PROVIDER': ('Google', 'Google Maps'),
|
103 | 109 |
'VERIFY': (True, ''),}
|
104 | |
self.controls = {}
|
105 | |
self.history_window_control = None
|
106 | |
|
107 | |
@log_calls('UrlImagePreviewPlugin')
|
108 | |
def connect_with_chat_control(self, chat_control):
|
109 | |
account = chat_control.contact.account.name
|
110 | |
jid = chat_control.contact.jid
|
111 | |
if account not in self.controls:
|
112 | |
self.controls[account] = {}
|
113 | |
self.controls[account][jid] = Base(self, chat_control.conv_textview)
|
114 | |
|
115 | |
@log_calls('UrlImagePreviewPlugin')
|
116 | |
def disconnect_from_chat_control(self, chat_control):
|
117 | |
account = chat_control.contact.account.name
|
118 | |
jid = chat_control.contact.jid
|
119 | |
self.controls[account][jid].deinit_handlers()
|
120 | |
del self.controls[account][jid]
|
121 | |
|
122 | |
@log_calls('UrlImagePreviewPlugin')
|
123 | |
def connect_with_history(self, history_window):
|
124 | |
if self.history_window_control:
|
125 | |
self.history_window_control.deinit_handlers()
|
126 | |
self.history_window_control = Base(
|
127 | |
self, history_window.history_textview)
|
128 | |
|
129 | |
@log_calls('UrlImagePreviewPlugin')
|
130 | |
def disconnect_from_history(self, history_window):
|
131 | |
if self.history_window_control:
|
132 | |
self.history_window_control.deinit_handlers()
|
133 | |
self.history_window_control = None
|
134 | |
|
135 | |
def print_real_text(self, tv, real_text, text_tags, graphics,
|
136 | |
iter_, additional_data):
|
137 | |
if tv.used_in_history_window and self.history_window_control:
|
138 | |
self.history_window_control.print_real_text(
|
139 | |
real_text, text_tags, graphics, iter_, additional_data)
|
140 | |
|
141 | |
account = tv.account
|
142 | |
for jid in self.controls[account]:
|
143 | |
if self.controls[account][jid].textview != tv:
|
144 | |
continue
|
145 | |
self.controls[account][jid].print_real_text(
|
146 | |
real_text, text_tags, graphics, iter_, additional_data)
|
147 | |
return
|
148 | |
|
149 | |
|
150 | |
class Base(object):
|
151 | |
def __init__(self, plugin, textview):
|
152 | |
self.plugin = plugin
|
153 | |
self.textview = textview
|
154 | |
self.handlers = {}
|
155 | |
|
156 | |
self.directory = os.path.join(configpaths.get('MY_DATA'),
|
157 | |
'downloads')
|
158 | |
self.thumbpath = os.path.join(configpaths.get('MY_CACHE'),
|
159 | |
'downloads.thumb')
|
160 | |
|
161 | |
try:
|
162 | |
self._create_path(self.directory)
|
163 | |
self._create_path(self.thumbpath)
|
164 | |
except Exception:
|
165 | |
log.error("Error creating download and/or thumbnail folder!")
|
166 | |
raise
|
167 | |
|
168 | |
def deinit_handlers(self):
|
169 | |
# remove all register handlers on wigets, created by self.xml
|
170 | |
# to prevent circular references among objects
|
171 | |
for i in list(self.handlers.keys()):
|
172 | |
if self.handlers[i].handler_is_connected(i):
|
173 | |
self.handlers[i].disconnect(i)
|
174 | |
del self.handlers[i]
|
175 | |
|
176 | |
def print_real_text(self, real_text, text_tags, graphics, iter_,
|
177 | |
additional_data):
|
178 | |
|
179 | |
if len(real_text.split(' ')) > 1:
|
180 | |
# urlparse dont recognises spaces as URL delimiter
|
181 | |
log.debug('Url with text will not be displayed: %s', real_text)
|
182 | |
return
|
183 | |
|
184 | |
urlparts = urlparse(unquote(real_text))
|
185 | |
if not self._accept_uri(urlparts, real_text, additional_data):
|
186 | |
return
|
187 | |
|
188 | |
# Don't print the URL in the message window (in the calling function)
|
189 | |
self.textview.plugin_modified = True
|
190 | |
|
191 | |
buffer_ = self.textview.tv.get_buffer()
|
|
110 |
|
|
111 |
self._textviews = {}
|
|
112 |
|
|
113 |
self._session = Soup.Session()
|
|
114 |
self._session.add_feature_by_type(Soup.ContentSniffer)
|
|
115 |
self._session.props.https_aliases = ['aesgcm']
|
|
116 |
self._session.props.ssl_strict = False
|
|
117 |
|
|
118 |
self._orig_dir = Path(configpaths.get('MY_DATA')) / 'downloads'
|
|
119 |
self._thumb_dir = Path(configpaths.get('MY_CACHE')) / 'downloads.thumb'
|
|
120 |
|
|
121 |
if GLib.mkdir_with_parents(str(self._orig_dir), 0o700) != 0:
|
|
122 |
log.error('Failed to create: %s', self._orig_dir)
|
|
123 |
|
|
124 |
if GLib.mkdir_with_parents(str(self._thumb_dir), 0o700) != 0:
|
|
125 |
log.error('Failed to create: %s', self._thumb_dir)
|
|
126 |
|
|
127 |
self._migrate_config()
|
|
128 |
|
|
129 |
def _migrate_config(self):
|
|
130 |
action = self.config['LEFTCLICK_ACTION']
|
|
131 |
if action.endswith('_menuitem'):
|
|
132 |
self.config['LEFTCLICK_ACTION'] = action[:-9]
|
|
133 |
|
|
134 |
def _on_connect_chat_control_base(self, chat_control):
|
|
135 |
self._textviews[chat_control.control_id] = chat_control.conv_textview
|
|
136 |
|
|
137 |
def _on_disconnect_chat_control_base(self, chat_control):
|
|
138 |
self._textviews.pop(chat_control.control_id, None)
|
|
139 |
|
|
140 |
def _on_connect_history_window(self, history_window):
|
|
141 |
self._textviews[id(history_window)] = history_window.history_textview
|
|
142 |
|
|
143 |
def _on_disconnect_history_window(self, history_window):
|
|
144 |
self._textviews.pop(id(history_window), None)
|
|
145 |
|
|
146 |
def _get_control_id(self, textview):
|
|
147 |
for control_id, textview_ in self._textviews.items():
|
|
148 |
if textview == textview_:
|
|
149 |
return control_id
|
|
150 |
|
|
151 |
def _print_real_text(self, textview, text, _text_tags, _graphics,
|
|
152 |
iter_, additional_data):
|
|
153 |
|
|
154 |
if len(text.split(' ')) > 1:
|
|
155 |
# urlparse doesn't recognise spaces as URL delimiter
|
|
156 |
log.debug('Text is not an uri: %s...', text[:15])
|
|
157 |
return
|
|
158 |
|
|
159 |
uri = text
|
|
160 |
urlparts = urlparse(unquote(uri))
|
|
161 |
if not self._accept_uri(urlparts, uri, additional_data):
|
|
162 |
return
|
|
163 |
|
|
164 |
textview.plugin_modified = True
|
|
165 |
control_id = self._get_control_id(textview)
|
|
166 |
|
|
167 |
start_mark, end_mark = self._print_text(textview.tv.get_buffer(),
|
|
168 |
iter_,
|
|
169 |
uri)
|
|
170 |
|
|
171 |
if uri.startswith('geo:'):
|
|
172 |
preview = self._process_geo_uri(uri,
|
|
173 |
start_mark,
|
|
174 |
end_mark,
|
|
175 |
control_id)
|
|
176 |
if preview is None:
|
|
177 |
return
|
|
178 |
pixbuf = load_icon('map',
|
|
179 |
size=preview.size,
|
|
180 |
scale=get_monitor_scale_factor(),
|
|
181 |
pixbuf=True)
|
|
182 |
self._update_textview(pixbuf, preview)
|
|
183 |
return
|
|
184 |
|
|
185 |
preview = self._process_web_uri(uri,
|
|
186 |
urlparts,
|
|
187 |
start_mark,
|
|
188 |
end_mark,
|
|
189 |
control_id)
|
|
190 |
|
|
191 |
if not preview.orig_exists():
|
|
192 |
self._download_content(preview)
|
|
193 |
|
|
194 |
elif not preview.thumb_exists():
|
|
195 |
load_file_async(preview.orig_path,
|
|
196 |
self._on_orig_load_finished,
|
|
197 |
preview)
|
|
198 |
|
|
199 |
else:
|
|
200 |
load_file_async(preview.thumb_path,
|
|
201 |
self._on_thumb_load_finished,
|
|
202 |
preview)
|
|
203 |
|
|
204 |
@staticmethod
|
|
205 |
def _print_text(buffer_, iter_, text):
|
192 | 206 |
if not iter_:
|
193 | 207 |
iter_ = buffer_.get_end_iter()
|
194 | 208 |
|
195 | |
# Show URL, until image is loaded (if ever)
|
196 | |
ttt = buffer_.get_tag_table()
|
197 | |
repl_start = buffer_.create_mark(None, iter_, True)
|
198 | |
buffer_.insert_with_tags(iter_, real_text,
|
199 | |
*[(ttt.lookup(t) if isinstance(t, str) else t) for t in ["url"]])
|
200 | |
repl_end = buffer_.create_mark(None, iter_, True)
|
201 | |
|
202 | |
# Handle geo:-URIs
|
203 | |
if real_text.startswith('geo:'):
|
204 | |
if self.plugin.config['GEO_PREVIEW_PROVIDER'] == 'no_preview':
|
205 | |
return
|
206 | |
size = self.plugin.config['PREVIEW_SIZE']
|
207 | |
geo_provider = self.plugin.config['GEO_PREVIEW_PROVIDER']
|
208 | |
key = ''
|
209 | |
iv = ''
|
210 | |
encrypted = False
|
211 | |
ext = '.png'
|
212 | |
color = 'blue'
|
213 | |
zoom = 16
|
214 | |
location = real_text[4:]
|
215 | |
lat, _, lon = location.partition(',')
|
216 | |
if lon == '':
|
217 | |
return
|
218 | |
|
219 | |
filename = 'location_' + geo_provider + '_' \
|
220 | |
+ location.replace(',', '_').replace('.', '-')
|
221 | |
newfilename = filename + ext
|
222 | |
thumbfilename = filename + '_thumb_' \
|
223 | |
+ str(self.plugin.config['PREVIEW_SIZE']) + ext
|
224 | |
filepath = os.path.join(self.directory, newfilename)
|
225 | |
thumbpath = os.path.join(self.thumbpath, thumbfilename)
|
226 | |
filepaths = [filepath, thumbpath]
|
227 | |
|
228 | |
# Google
|
229 | |
if geo_provider == 'Google':
|
230 | |
url = 'https://maps.googleapis.com/maps/api/staticmap?' \
|
231 | |
'center={}&zoom={}&size={}x{}&markers=color:{}' \
|
232 | |
'|label:.|{}'.format(location, zoom, size, size,
|
233 | |
color, location)
|
234 | |
weburl = 'https://www.google.com/maps/place/{}' \
|
235 | |
.format(location)
|
236 | |
real_text = url
|
237 | |
else:
|
238 | |
# OpenStreetMap / MapQuest
|
239 | |
apikey = 'F7x36jLVv2hiANVAXmhwvUB044XvGASh'
|
240 | |
|
241 | |
url = 'https://open.mapquestapi.com/staticmap/v4/' \
|
242 | |
'getmap?key={}¢er={}&zoom={}&size={},{}&type=map' \
|
243 | |
'&imagetype=png&pois={},{}&scalebar=false' \
|
244 | |
.format(apikey, location, zoom, size, size, color,
|
245 | |
location)
|
246 | |
weburl = 'http://www.openstreetmap.org/' \
|
247 | |
'?mlat={}&mlon={}#map={}/{}/{}&layers=N' \
|
248 | |
.format(lat, lon, zoom, lat, lon)
|
249 | |
real_text = url
|
250 | |
else:
|
251 | |
weburl = real_text
|
252 | |
filename = os.path.basename(urlparts.path)
|
253 | |
ext = os.path.splitext(filename)[1]
|
254 | |
name = os.path.splitext(filename)[0]
|
255 | |
if len(name) > 90:
|
256 | |
# Many Filesystems have a limit on filename length
|
257 | |
# Most have 255, some encrypted ones only 143
|
258 | |
# We add around 50 chars for the hash,
|
259 | |
# so the filename should not exceed 90
|
260 | |
name = name[:90]
|
261 | |
namehash = hashlib.sha1(real_text.encode('utf-8')).hexdigest()
|
262 | |
newfilename = name + '_' + namehash + ext
|
263 | |
thumbfilename = name + '_' + namehash + '_thumb_' \
|
264 | |
+ str(self.plugin.config['PREVIEW_SIZE']) + ext
|
265 | |
|
266 | |
filepath = os.path.join(self.directory, newfilename)
|
267 | |
thumbpath = os.path.join(self.thumbpath, thumbfilename)
|
268 | |
filepaths = [filepath, thumbpath]
|
269 | |
|
270 | |
key = ''
|
271 | |
iv = ''
|
272 | |
encrypted = False
|
273 | |
if urlparts.fragment:
|
274 | |
fragment = binascii.unhexlify(urlparts.fragment)
|
275 | |
key = fragment[16:]
|
276 | |
iv = fragment[:16]
|
277 | |
if len(key) == 32 and len(iv) == 16:
|
278 | |
encrypted = True
|
279 | |
if not encrypted:
|
280 | |
key = fragment[12:]
|
281 | |
iv = fragment[:12]
|
282 | |
if len(key) == 32 and len(iv) == 12:
|
283 | |
encrypted = True
|
284 | |
|
285 | |
# file exists but thumbnail got deleted
|
286 | |
if os.path.exists(filepath) and not os.path.exists(thumbpath):
|
287 | |
if urlparts.scheme == 'geo':
|
288 | |
real_text = weburl
|
289 | |
with open(filepath, 'rb') as f:
|
290 | |
mem = f.read()
|
291 | |
app.thread_interface(
|
292 | |
self._save_thumbnail, [thumbpath, mem],
|
293 | |
self._update_img, [real_text, repl_start,
|
294 | |
repl_end, filepath, encrypted])
|
295 | |
|
296 | |
# display thumbnail if already downloadeded
|
297 | |
# (but only if file also exists)
|
298 | |
elif os.path.exists(filepath) and os.path.exists(thumbpath):
|
299 | |
if urlparts.scheme == 'geo':
|
300 | |
real_text = weburl
|
301 | |
app.thread_interface(
|
302 | |
self._load_thumbnail, [thumbpath],
|
303 | |
self._update_img, [real_text, repl_start,
|
304 | |
repl_end, filepath, encrypted])
|
305 | |
|
306 | |
# or download file, calculate thumbnail and finally display it
|
307 | |
else:
|
308 | |
if encrypted and not decryption_available:
|
309 | |
log.debug('Please install Crytography to decrypt pictures')
|
310 | |
else:
|
311 | |
# First get the http head request
|
312 | |
# which does not fetch data, just headers
|
313 | |
# then check the mime type and filesize
|
314 | |
if urlparts.scheme == 'aesgcm':
|
315 | |
real_text = 'https://' + real_text[9:]
|
316 | |
verify = self.plugin.config['VERIFY']
|
317 | |
app.thread_interface(
|
318 | |
get_http_head, [self.textview.account, real_text, verify],
|
319 | |
self._check_mime_size, [real_text, weburl, repl_start,
|
320 | |
repl_end, filepaths, key, iv,
|
321 | |
encrypted])
|
322 | |
|
323 | |
def _accept_uri(self, urlparts, real_text, additional_data):
|
|
209 |
start_mark = buffer_.create_mark(None, iter_, True)
|
|
210 |
buffer_.insert_with_tags_by_name(iter_, text, 'url')
|
|
211 |
end_mark = buffer_.create_mark(None, iter_, True)
|
|
212 |
return start_mark, end_mark
|
|
213 |
|
|
214 |
def _accept_uri(self, urlparts, uri, additional_data):
|
324 | 215 |
try:
|
325 | |
oob_url = additional_data["gajim"]["oob_url"]
|
|
216 |
oob_url = additional_data['gajim']['oob_url']
|
326 | 217 |
except (KeyError, AttributeError):
|
327 | 218 |
oob_url = None
|
328 | 219 |
|
|
220 |
# geo
|
|
221 |
if urlparts.scheme == 'geo':
|
|
222 |
return True
|
|
223 |
|
329 | 224 |
if not urlparts.netloc:
|
330 | |
log.info('No netloc found in URL %s', real_text)
|
|
225 |
log.info('No netloc found in URL: %s', uri)
|
331 | 226 |
return False
|
332 | |
|
333 | |
# geo
|
334 | |
if urlparts.scheme == "geo":
|
335 | |
if self.plugin.config['GEO_PREVIEW_PROVIDER'] == 'no_preview':
|
336 | |
log.info('geo: link preview is disabled')
|
337 | |
return False
|
338 | |
return True
|
339 | 227 |
|
340 | 228 |
# aesgcm
|
341 | 229 |
if urlparts.scheme == 'aesgcm':
|
342 | 230 |
return True
|
343 | 231 |
|
344 | |
# https
|
345 | |
if urlparts.scheme == 'https':
|
346 | |
if real_text == oob_url or self.plugin.config['ALLOW_ALL_IMAGES']:
|
|
232 |
# http/https
|
|
233 |
if urlparts.scheme in ('https', 'http'):
|
|
234 |
if self.config['ALLOW_ALL_IMAGES']:
|
347 | 235 |
return True
|
348 | |
log.info('Incorrect oob data found')
|
349 | |
return False
|
350 | |
|
351 | |
log.info('Not supported URI scheme found: %s', real_text)
|
|
236 |
|
|
237 |
if oob_url is None:
|
|
238 |
log.info('No oob url for: %s', uri)
|
|
239 |
return False
|
|
240 |
|
|
241 |
if uri != oob_url:
|
|
242 |
log.info('uri != oob url: %s != %s', uri, oob_url)
|
|
243 |
return False
|
|
244 |
return True
|
|
245 |
|
|
246 |
log.info('Unsupported URI scheme: %s', uri)
|
352 | 247 |
return False
|
353 | 248 |
|
354 | |
def _save_thumbnail(self, thumbpath, mem):
|
355 | |
size = self.plugin.config['PREVIEW_SIZE']
|
356 | |
|
|
249 |
@staticmethod
|
|
250 |
def _process_geo_uri(uri, start_mark, end_mark, control_id):
|
357 | 251 |
try:
|
358 | |
loader = GdkPixbuf.PixbufLoader()
|
359 | |
loader.write(mem)
|
360 | |
loader.close()
|
361 | |
if loader.get_format().get_name() == 'gif':
|
362 | |
pixbuf = loader.get_animation()
|
363 | |
else:
|
364 | |
pixbuf = loader.get_pixbuf()
|
365 | |
except GLib.GError as error:
|
366 | |
log.info('Failed to load image using Gdk.Pixbuf')
|
367 | |
log.debug(error)
|
368 | |
|
369 | |
# Try Pillow
|
370 | |
image = Image.open(BytesIO(mem)).convert("RGBA")
|
371 | |
array = GLib.Bytes.new(image.tobytes())
|
372 | |
width, height = image.size
|
373 | |
pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(
|
374 | |
array, GdkPixbuf.Colorspace.RGB, True,
|
375 | |
8, width, height, width * 4)
|
376 | |
|
377 | |
try:
|
378 | |
self._create_path(os.path.dirname(thumbpath))
|
379 | |
thumbnail = pixbuf
|
380 | |
if isinstance(pixbuf, GdkPixbuf.PixbufAnimation):
|
381 | |
if size < pixbuf.get_width() or size < pixbuf.get_height():
|
382 | |
resize_gif(mem, thumbpath, (size, size))
|
383 | |
thumbnail = self._load_thumbnail(thumbpath)
|
384 | |
else:
|
385 | |
self._write_file(thumbpath, mem)
|
386 | |
else:
|
387 | |
width, height = self._get_thumbnail_size(pixbuf, size)
|
388 | |
thumbnail = pixbuf.scale_simple(
|
389 | |
width, height, GdkPixbuf.InterpType.BILINEAR)
|
390 | |
thumbnail.savev(thumbpath, 'png', [], [])
|
|
252 |
split_geo_uri(uri)
|
391 | 253 |
except Exception as error:
|
392 | |
GLib.idle_add(
|
393 | |
self._raise_error_dialog,
|
394 | |
_('Could not save file'),
|
395 | |
_('Exception raised while saving thumbnail '
|
396 | |
'for image file (see error log for more '
|
397 | |
'information)'))
|
398 | |
log.exception(error)
|
399 | |
return
|
400 | |
return thumbnail
|
401 | |
|
402 | |
@staticmethod
|
403 | |
def _get_thumbnail_size(pixbuf, size):
|
404 | |
# Calculates the new thumbnail size while preserving the aspect ratio
|
405 | |
image_width = pixbuf.get_width()
|
406 | |
image_height = pixbuf.get_height()
|
407 | |
|
408 | |
if image_width > image_height:
|
409 | |
if image_width > size:
|
410 | |
image_height = math.ceil((size / float(image_width) * image_height))
|
411 | |
image_width = int(size)
|
412 | |
else:
|
413 | |
if image_height > size:
|
414 | |
image_width = math.ceil((size / float(image_height) * image_width))
|
415 | |
image_height = int(size)
|
416 | |
|
417 | |
return image_width, image_height
|
418 | |
|
419 | |
@staticmethod
|
420 | |
def _load_thumbnail(thumbpath):
|
421 | |
ext = os.path.splitext(thumbpath)[1]
|
422 | |
if ext == '.gif':
|
423 | |
return GdkPixbuf.PixbufAnimation.new_from_file(thumbpath)
|
424 | |
return GdkPixbuf.Pixbuf.new_from_file(thumbpath)
|
425 | |
|
426 | |
@staticmethod
|
427 | |
def _write_file(path, data):
|
428 | |
log.info("Writing '%s' of size %d...", path, len(data))
|
429 | |
try:
|
430 | |
with open(path, "wb") as output_file:
|
431 | |
output_file.write(data)
|
432 | |
output_file.closed
|
433 | |
except Exception as e:
|
434 | |
log.error("Failed to write file '%s'!", path)
|
435 | |
raise
|
436 | |
|
437 | |
def _get_at_end(self):
|
438 | |
try:
|
439 | |
# Gajim 1.0.0
|
440 | |
return self.textview.at_the_end()
|
441 | |
except AttributeError:
|
442 | |
# Gajim 1.0.1
|
443 | |
return self.textview.autoscroll
|
444 | |
|
445 | |
def _scroll_to_end(self):
|
446 | |
try:
|
447 | |
# Gajim 1.0.0
|
448 | |
self.textview.scroll_to_end_iter()
|
449 | |
except AttributeError:
|
450 | |
# Gajim 1.0.1
|
451 | |
self.textview.scroll_to_end()
|
452 | |
|
453 | |
def _update_img(self, pixbuf, url, repl_start, repl_end,
|
454 | |
filepath, encrypted):
|
455 | |
if pixbuf is None:
|
456 | |
# If image could not be downloaded, URL is already displayed
|
457 | |
log.error('Could not download image for URL: %s', url)
|
458 | |
return
|
459 | |
|
460 | |
urlparts = urlparse(unquote(url))
|
461 | |
filename = os.path.basename(urlparts.path)
|
462 | |
if os.path.basename(filepath).startswith('location_'):
|
463 | |
filename = os.path.basename(filepath)
|
464 | |
|
465 | |
def add_to_textview():
|
466 | |
try:
|
467 | |
at_end = self._get_at_end()
|
468 | |
|
469 | |
buffer_ = repl_start.get_buffer()
|
470 | |
iter_ = buffer_.get_iter_at_mark(repl_start)
|
471 | |
buffer_.insert(iter_, "\n")
|
472 | |
anchor = buffer_.create_child_anchor(iter_)
|
473 | |
anchor.plaintext = url
|
474 | |
|
475 | |
image = self._create_clickable_image(pixbuf, url)
|
476 | |
|
477 | |
self.textview.tv.add_child_at_anchor(image, anchor)
|
478 | |
buffer_.delete(iter_,
|
479 | |
buffer_.get_iter_at_mark(repl_end))
|
480 | |
|
481 | |
image.connect(
|
482 | |
'button-press-event', self.on_button_press_event,
|
483 | |
filepath, filename, url, encrypted)
|
484 | |
image.get_window().set_cursor(get_cursor('HAND2'))
|
485 | |
|
486 | |
if at_end:
|
487 | |
self._scroll_to_end()
|
488 | |
except Exception as ex:
|
489 | |
log.exception("Exception while loading %s: %s", url, ex)
|
490 | |
return False
|
491 | |
# add to mainloop --> make call threadsafe
|
492 | |
GLib.idle_add(add_to_textview)
|
493 | |
|
494 | |
def _create_clickable_image(self, pixbuf, url):
|
495 | |
if isinstance(pixbuf, GdkPixbuf.PixbufAnimation):
|
496 | |
image = Gtk.Image.new_from_animation(pixbuf)
|
497 | |
else:
|
498 | |
image = Gtk.Image.new_from_pixbuf(pixbuf)
|
499 | |
|
500 | |
css = '''#Preview {
|
501 | |
box-shadow: 0px 0px 3px 0px alpha(@theme_text_color, 0.2);
|
502 | |
margin: 5px 10px 5px 10px; }'''
|
503 | |
gtkgui_helpers.add_css_to_widget(image, css)
|
504 | |
image.set_name('Preview')
|
505 | |
|
506 | |
event_box = Gtk.EventBox()
|
507 | |
event_box.set_tooltip_text(url)
|
508 | |
event_box.add(image)
|
509 | |
event_box.show_all()
|
510 | |
return event_box
|
511 | |
|
512 | |
def _check_mime_size(self, tuple_arg,
|
513 | |
url, weburl, repl_start, repl_end, filepaths,
|
514 | |
key, iv, encrypted):
|
515 | |
file_mime, file_size = tuple_arg
|
516 | |
# Check if mime type is acceptable
|
517 | |
if not file_mime or not file_size:
|
518 | |
log.info("Failed to load HEAD Request for URL: '%s' "
|
519 | |
"mime: %s, size: %s", url, file_mime, file_size)
|
520 | |
# URL is already displayed
|
521 | |
return
|
522 | |
if file_mime.lower() not in ACCEPTED_MIME_TYPES:
|
523 | |
log.info("Not accepted mime type '%s' for URL: '%s'",
|
524 | |
file_mime.lower(), url)
|
525 | |
# URL is already displayed
|
526 | |
return
|
527 | |
# Check if file size is acceptable
|
528 | |
max_size = int(self.plugin.config['MAX_FILE_SIZE'])
|
529 | |
if file_size > max_size or file_size == 0:
|
530 | |
log.info("File size (%s) too big or unknown (zero) for URL: '%s'",
|
531 | |
file_size, url)
|
532 | |
# URL is already displayed
|
533 | |
return
|
534 | |
|
535 | |
attributes = {'src': url,
|
536 | |
'verify': self.plugin.config['VERIFY'],
|
537 | |
'max_size': max_size,
|
538 | |
'filepaths': filepaths,
|
539 | |
'key': key,
|
540 | |
'iv': iv}
|
541 | |
|
542 | |
app.thread_interface(
|
543 | |
self._download_image, [self.textview.account,
|
544 | |
attributes, encrypted],
|
545 | |
self._update_img, [weburl, repl_start, repl_end,
|
546 | |
filepaths[0], encrypted])
|
547 | |
|
548 | |
def _download_image(self, account, attributes, encrypted):
|
549 | |
filepath = attributes['filepaths'][0]
|
550 | |
thumbpath = attributes['filepaths'][1]
|
551 | |
key = attributes['key']
|
552 | |
iv = attributes['iv']
|
553 | |
mem, alt = get_http_file(account, attributes)
|
554 | |
|
555 | |
# Decrypt file if necessary
|
556 | |
if encrypted:
|
557 | |
mem = self._aes_decrypt_fast(key, iv, mem)
|
558 | |
|
559 | |
try:
|
560 | |
# Write file to harddisk
|
561 | |
self._write_file(filepath, mem)
|
562 | |
except Exception as e:
|
563 | |
GLib.idle_add(
|
564 | |
self._raise_error_dialog,
|
565 | |
_('Could not save file'),
|
566 | |
_('Exception raised while saving image file'
|
567 | |
' (see error log for more information)'))
|
568 | |
log.error(str(e))
|
569 | |
|
570 | |
# Create thumbnail, write it to harddisk and return it
|
571 | |
return self._save_thumbnail(thumbpath, mem)
|
572 | |
|
573 | |
def _create_path(self, folder):
|
574 | |
if os.path.exists(folder):
|
575 | |
return
|
576 | |
log.debug("creating folder '%s'" % folder)
|
577 | |
os.mkdir(folder, 0o700)
|
578 | |
|
579 | |
def _aes_decrypt_fast(self, key, iv, payload):
|
580 | |
# Use AES128 GCM with the given key and iv to decrypt the payload.
|
581 | |
if os.name == 'nt':
|
582 | |
be = backend
|
583 | |
else:
|
584 | |
be = default_backend()
|
585 | |
data = payload[:-16]
|
586 | |
tag = payload[-16:]
|
587 | |
decryptor = Cipher(
|
588 | |
algorithms.AES(key),
|
589 | |
GCM(iv, tag=tag),
|
590 | |
backend=be).decryptor()
|
591 | |
return decryptor.update(data) + decryptor.finalize()
|
592 | |
|
593 | |
def make_rightclick_menu(self, event, data):
|
594 | |
xml = Gtk.Builder()
|
595 | |
xml.set_translation_domain('gajim_plugins')
|
596 | |
xml.add_from_file(self.plugin.local_file_path('context_menu.ui'))
|
597 | |
menu = xml.get_object('context_menu')
|
598 | |
|
599 | |
open_menuitem = xml.get_object('open_menuitem')
|
600 | |
save_as_menuitem = xml.get_object('save_as_menuitem')
|
601 | |
copy_link_location_menuitem = \
|
602 | |
xml.get_object('copy_link_location_menuitem')
|
603 | |
open_link_in_browser_menuitem = \
|
604 | |
xml.get_object('open_link_in_browser_menuitem')
|
605 | |
open_file_in_browser_menuitem = \
|
606 | |
xml.get_object('open_file_in_browser_menuitem')
|
607 | |
extras_separator = \
|
608 | |
xml.get_object('extras_separator')
|
609 | |
|
610 | |
if data["encrypted"]:
|
611 | |
open_link_in_browser_menuitem.hide()
|
612 | |
if app.config.get('autodetect_browser_mailer') \
|
613 | |
or app.config.get('custombrowser') == '':
|
614 | |
extras_separator.hide()
|
615 | |
open_file_in_browser_menuitem.hide()
|
616 | |
|
617 | |
id_ = open_menuitem.connect(
|
618 | |
'activate', self.on_open_menuitem_activate, data)
|
619 | |
self.handlers[id_] = open_menuitem
|
620 | |
id_ = save_as_menuitem.connect(
|
621 | |
'activate', self.on_save_as_menuitem_activate_new, data)
|
622 | |
self.handlers[id_] = save_as_menuitem
|
623 | |
id_ = copy_link_location_menuitem.connect(
|
624 | |
'activate', self.on_copy_link_location_menuitem_activate, data)
|
625 | |
self.handlers[id_] = copy_link_location_menuitem
|
626 | |
id_ = open_link_in_browser_menuitem.connect(
|
627 | |
'activate', self.on_open_link_in_browser_menuitem_activate, data)
|
628 | |
self.handlers[id_] = open_link_in_browser_menuitem
|
629 | |
id_ = open_file_in_browser_menuitem.connect(
|
630 | |
'activate', self.on_open_file_in_browser_menuitem_activate, data)
|
631 | |
self.handlers[id_] = open_file_in_browser_menuitem
|
632 | |
|
633 | |
return menu
|
634 | |
|
635 | |
def on_open_menuitem_activate(self, menu, data):
|
636 | |
filepath = data["filepath"]
|
637 | |
original_filename = data["original_filename"]
|
638 | |
url = data["url"]
|
639 | |
if original_filename.startswith('location_'):
|
640 | |
helpers.launch_browser_mailer('url', url)
|
641 | |
return
|
642 | |
helpers.launch_file_manager(filepath)
|
643 | |
|
644 | |
def on_save_as_menuitem_activate_new(self, menu, data):
|
645 | |
filepath = data["filepath"]
|
646 | |
original_filename = data["original_filename"]
|
647 | |
|
|
254 |
log.error(uri)
|
|
255 |
log.error(error)
|
|
256 |
return
|
|
257 |
|
|
258 |
return Preview(uri,
|
|
259 |
None,
|
|
260 |
None,
|
|
261 |
None,
|
|
262 |
start_mark,
|
|
263 |
end_mark,
|
|
264 |
96,
|
|
265 |
control_id)
|
|
266 |
|
|
267 |
def _process_web_uri(self, uri, urlparts, start_mark, end_mark, control_id):
|
|
268 |
size = self.config['PREVIEW_SIZE']
|
|
269 |
orig_path, thumb_path = get_image_paths(uri,
|
|
270 |
urlparts,
|
|
271 |
size,
|
|
272 |
self._orig_dir,
|
|
273 |
self._thumb_dir)
|
|
274 |
return Preview(uri,
|
|
275 |
urlparts,
|
|
276 |
orig_path,
|
|
277 |
thumb_path,
|
|
278 |
start_mark,
|
|
279 |
end_mark,
|
|
280 |
size,
|
|
281 |
control_id)
|
|
282 |
|
|
283 |
def _on_orig_load_finished(self, data, error, preview):
|
|
284 |
if data is None:
|
|
285 |
log.error('%s: %s', preview.orig_path.name, error)
|
|
286 |
return
|
|
287 |
|
|
288 |
if preview.create_thumbnail(data):
|
|
289 |
write_file_async(preview.thumb_path,
|
|
290 |
preview.thumbnail,
|
|
291 |
self._on_thumb_write_finished,
|
|
292 |
preview)
|
|
293 |
|
|
294 |
def _on_thumb_load_finished(self, data, error, preview):
|
|
295 |
if data is None:
|
|
296 |
log.error('%s: %s', preview.thumb_path.name, error)
|
|
297 |
return
|
|
298 |
|
|
299 |
preview.thumbnail = data
|
|
300 |
|
|
301 |
pixbuf = pixbuf_from_data(preview.thumbnail)
|
|
302 |
self._update_textview(pixbuf, preview)
|
|
303 |
|
|
304 |
def _download_content(self, preview):
|
|
305 |
log.info('Start downloading: %s', preview.request_uri)
|
|
306 |
message = Soup.Message.new('GET', preview.request_uri)
|
|
307 |
message.connect('starting', self._check_certificate)
|
|
308 |
message.connect('content-sniffed', self._on_content_sniffed)
|
|
309 |
self._session.queue_message(message, self._on_finished, preview)
|
|
310 |
|
|
311 |
def _check_certificate(self, message):
|
|
312 |
_https_used, _tls_certificate, tls_errors = message.get_https_status()
|
|
313 |
|
|
314 |
if not self.config['VERIFY']:
|
|
315 |
return
|
|
316 |
|
|
317 |
if tls_errors:
|
|
318 |
phrase = get_tls_error_phrase(tls_errors)
|
|
319 |
log.warning('TLS verification failed: %s', phrase)
|
|
320 |
self._session.cancel_message(message, Soup.Status.CANCELLED)
|
|
321 |
return
|
|
322 |
|
|
323 |
def _on_content_sniffed(self, message, type_, _params):
|
|
324 |
size = message.props.response_headers.get_content_length()
|
|
325 |
uri = message.props.uri.to_string(False)
|
|
326 |
if type_ not in ACCEPTED_MIME_TYPES:
|
|
327 |
log.info('Not allowed content type: %s, %s', type_, uri)
|
|
328 |
self._session.cancel_message(message, Soup.Status.CANCELLED)
|
|
329 |
return
|
|
330 |
|
|
331 |
if size == 0 or size > int(self.config['MAX_FILE_SIZE']):
|
|
332 |
log.info('File size (%s) too big or unknown (zero) for URL: \'%s\'',
|
|
333 |
size, uri)
|
|
334 |
self._session.cancel_message(message, Soup.Status.CANCELLED)
|
|
335 |
return
|
|
336 |
|
|
337 |
def _on_finished(self, _session, message, preview):
|
|
338 |
if message.status_code != Soup.Status.OK:
|
|
339 |
log.warning('Download failed: %s', preview.request_uri)
|
|
340 |
log.warning(Soup.Status.get_phrase(message.status_code))
|
|
341 |
return
|
|
342 |
|
|
343 |
data = message.props.response_body_data.get_data()
|
|
344 |
if data is None:
|
|
345 |
return
|
|
346 |
|
|
347 |
if preview.is_aes_encrypted:
|
|
348 |
data = aes_decrypt(preview, data)
|
|
349 |
|
|
350 |
write_file_async(preview.orig_path,
|
|
351 |
data,
|
|
352 |
self._on_orig_write_finished,
|
|
353 |
preview)
|
|
354 |
|
|
355 |
if preview.create_thumbnail(data):
|
|
356 |
write_file_async(preview.thumb_path,
|
|
357 |
preview.thumbnail,
|
|
358 |
self._on_thumb_write_finished,
|
|
359 |
preview)
|
|
360 |
|
|
361 |
@staticmethod
|
|
362 |
def _on_orig_write_finished(_result, error, preview):
|
|
363 |
if error is not None:
|
|
364 |
log.error('%s: %s', preview.orig_path.name, error)
|
|
365 |
return
|
|
366 |
|
|
367 |
log.info('File stored: %s', preview.orig_path.name)
|
|
368 |
|
|
369 |
def _on_thumb_write_finished(self, _result, error, preview):
|
|
370 |
if error is not None:
|
|
371 |
log.error('%s: %s', preview.thumb_path.name, error)
|
|
372 |
return
|
|
373 |
|
|
374 |
log.info('Thumbnail stored: %s ', preview.thumb_path.name)
|
|
375 |
pixbuf = pixbuf_from_data(preview.thumbnail)
|
|
376 |
self._update_textview(pixbuf, preview)
|
|
377 |
|
|
378 |
def _update_textview(self, pixbuf, preview):
|
|
379 |
textview = self._textviews.get(preview.control_id)
|
|
380 |
if textview is None:
|
|
381 |
# Control closed
|
|
382 |
return
|
|
383 |
|
|
384 |
buffer_ = preview.start_mark.get_buffer()
|
|
385 |
iter_ = buffer_.get_iter_at_mark(preview.start_mark)
|
|
386 |
buffer_.insert(iter_, '\n')
|
|
387 |
anchor = buffer_.create_child_anchor(iter_)
|
|
388 |
anchor.plaintext = preview.uri
|
|
389 |
|
|
390 |
image = create_clickable_image(pixbuf, preview)
|
|
391 |
|
|
392 |
textview.tv.add_child_at_anchor(image, anchor)
|
|
393 |
buffer_.delete(iter_,
|
|
394 |
buffer_.get_iter_at_mark(preview.end_mark))
|
|
395 |
|
|
396 |
image.connect('button-press-event',
|
|
397 |
self._on_button_press_event,
|
|
398 |
preview)
|
|
399 |
|
|
400 |
if textview.autoscroll:
|
|
401 |
textview.scroll_to_end()
|
|
402 |
|
|
403 |
def _get_context_menu(self, preview):
|
|
404 |
path = self.local_file_path('context_menu.ui')
|
|
405 |
ui = get_builder(path)
|
|
406 |
if preview.is_aes_encrypted:
|
|
407 |
ui.open_link_in_browser.hide()
|
|
408 |
|
|
409 |
if preview.is_geo_uri:
|
|
410 |
ui.open_link_in_browser.hide()
|
|
411 |
ui.save_as.hide()
|
|
412 |
ui.open_folder.hide()
|
|
413 |
|
|
414 |
ui.open.connect(
|
|
415 |
'activate', self._on_open, preview)
|
|
416 |
ui.save_as.connect(
|
|
417 |
'activate', self._on_save_as, preview)
|
|
418 |
ui.open_folder.connect(
|
|
419 |
'activate', self._on_open_folder, preview)
|
|
420 |
ui.open_link_in_browser.connect(
|
|
421 |
'activate', self._on_open_link_in_browser, preview)
|
|
422 |
ui.copy_link_location.connect(
|
|
423 |
'activate', self._on_copy_link_location, preview)
|
|
424 |
|
|
425 |
def destroy(menu, _pspec):
|
|
426 |
visible = menu.get_property('visible')
|
|
427 |
if not visible:
|
|
428 |
GLib.idle_add(menu.destroy)
|
|
429 |
|
|
430 |
ui.context_menu.connect('notify::visible', destroy)
|
|
431 |
return ui.context_menu
|
|
432 |
|
|
433 |
@staticmethod
|
|
434 |
def _on_open(_menu, preview):
|
|
435 |
if preview.is_geo_uri:
|
|
436 |
open_uri(preview.uri)
|
|
437 |
return
|
|
438 |
open_file(preview.orig_path)
|
|
439 |
|
|
440 |
@staticmethod
|
|
441 |
def _on_save_as(_menu, preview):
|
648 | 442 |
def on_ok(target_path):
|
649 | |
dirname = os.path.dirname(target_path)
|
|
443 |
dirname = Path(target_path).parent
|
650 | 444 |
if not os.access(dirname, os.W_OK):
|
651 | |
dialogs.ErrorDialog(
|
652 | |
_('Directory "%s" is not writable') % dirname,
|
653 | |
_('You do not have permission to '
|
654 | |
'create files in this directory.'))
|
|
445 |
ErrorDialog(
|
|
446 |
_('Directory \'%s\' is not writable') % dirname,
|
|
447 |
_('You do not have the proper permissions to '
|
|
448 |
'create files in this directory.'),
|
|
449 |
transient_for=app.app.get_active_window())
|
655 | 450 |
return
|
656 | |
shutil.copy(filepath, target_path)
|
|
451 |
shutil.copy(str(preview.orig_path), target_path)
|
657 | 452 |
|
658 | 453 |
FileSaveDialog(on_ok,
|
659 | 454 |
path=app.config.get('last_save_dir'),
|
660 | |
file_name=original_filename,
|
|
455 |
file_name=preview.filename,
|
661 | 456 |
transient_for=app.app.get_active_window())
|
662 | 457 |
|
663 | |
def on_copy_link_location_menuitem_activate(self, menu, data):
|
664 | |
url = data["url"]
|
665 | |
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
666 | |
clipboard.set_text(url, -1)
|
667 | |
clipboard.store()
|
668 | |
|
669 | |
def on_open_link_in_browser_menuitem_activate(self, menu, data):
|
670 | |
url = data["url"]
|
671 | |
if data["encrypted"]:
|
672 | |
dialogs.ErrorDialog(
|
673 | |
_('Encrypted file'),
|
674 | |
_('You cannot open encrypted files in your '
|
675 | |
'browser directly. Try "Open Downloaded File '
|
676 | |
'in Browser" instead.'),
|
677 | |
transient_for=app.app.get_active_window())
|
|
458 |
@staticmethod
|
|
459 |
def _on_open_folder(_menu, preview):
|
|
460 |
open_file(preview.orig_path.parent)
|
|
461 |
|
|
462 |
@staticmethod
|
|
463 |
def _on_copy_link_location(_menu, preview):
|
|
464 |
clipboard = Gtk.Clipboard.get_default(Gdk.Display.get_default())
|
|
465 |
clipboard.set_text(preview.uri, -1)
|
|
466 |
|
|
467 |
@staticmethod
|
|
468 |
def _on_open_link_in_browser(_menu, preview):
|
|
469 |
if preview.is_aes_encrypted:
|
|
470 |
if preview.is_geo_uri:
|
|
471 |
open_uri(preview.uri)
|
|
472 |
return
|
|
473 |
open_file(preview.orig_path)
|
678 | 474 |
else:
|
679 | |
helpers.launch_browser_mailer('url', url)
|
680 | |
|
681 | |
def on_open_file_in_browser_menuitem_activate(self, menu, data):
|
682 | |
if os.name == "nt":
|
683 | |
filepath = "file://" + os.path.abspath(data["filepath"])
|
684 | |
else:
|
685 | |
filepath = "file://" + data["filepath"]
|
686 | |
if app.config.get('autodetect_browser_mailer') \
|
687 | |
or app.config.get('custombrowser') == '':
|
688 | |
dialogs.ErrorDialog(
|
689 | |
_('Cannot open downloaded file in browser'),
|
690 | |
_('You have to set a custom browser executable '
|
691 | |
'in your gajim settings for this to work.'),
|
692 | |
transient_for=app.app.get_active_window())
|
693 | |
return
|
694 | |
command = app.config.get('custombrowser')
|
695 | |
command = helpers.build_command(command, filepath)
|
696 | |
try:
|
697 | |
helpers.exec_command(command)
|
698 | |
except Exception:
|
699 | |
pass
|
700 | |
|
701 | |
def on_button_press_event(self, eb, event, filepath,
|
702 | |
original_filename, url, encrypted):
|
703 | |
data = {"filepath": filepath,
|
704 | |
"original_filename": original_filename,
|
705 | |
"url": url,
|
706 | |
"encrypted": encrypted}
|
707 | |
# left click
|
|
475 |
open_uri(preview.uri)
|
|
476 |
|
|
477 |
def _on_button_press_event(self, _image, event, preview):
|
708 | 478 |
if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 1:
|
709 | |
method = getattr(self, "on_"
|
710 | |
+ self.plugin.config['LEFTCLICK_ACTION']
|
711 | |
+ "_activate")
|
712 | |
method(event, data)
|
713 | |
# right klick
|
|
479 |
# Left click
|
|
480 |
action = self.config['LEFTCLICK_ACTION']
|
|
481 |
method = getattr(self, '_on_%s' % action)
|
|
482 |
method(event, preview)
|
|
483 |
|
714 | 484 |
elif event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:
|
715 | |
menu = self.make_rightclick_menu(event, data)
|
716 | |
# menu.attach_to_widget(self.tv, None)
|
717 | |
# menu.popup(None, None, None, event.button, event.time)
|
|
485 |
# Right klick
|
|
486 |
menu = self._get_context_menu(preview)
|
718 | 487 |
menu.popup_at_pointer(event)
|
719 | 488 |
|
720 | |
@staticmethod
|
721 | |
def _raise_error_dialog(pritext, sectext):
|
722 | |
# Used by methods that run in a different thread
|
723 | |
dialogs.ErrorDialog(pritext,
|
724 | |
sectext,
|
725 | |
transient_for=app.app.get_active_window())
|
726 | |
|
727 | |
def disconnect_from_chat_control(self):
|
728 | |
pass
|
|
489 |
|
|
490 |
class Preview:
|
|
491 |
def __init__(self, uri, urlparts, orig_path, thumb_path,
|
|
492 |
start_mark, end_mark, size, control_id):
|
|
493 |
self._uri = uri
|
|
494 |
self._urlparts = urlparts
|
|
495 |
self._filename = filename_from_uri(self._uri)
|
|
496 |
self.size = size
|
|
497 |
self.control_id = control_id
|
|
498 |
self.orig_path = orig_path
|
|
499 |
self.thumb_path = thumb_path
|
|
500 |
self.start_mark = start_mark
|
|
501 |
self.end_mark = end_mark
|
|
502 |
self.thumbnail = None
|
|
503 |
|
|
504 |
self.key, self.iv = None, None
|
|
505 |
if self.is_aes_encrypted:
|
|
506 |
self.key, self.iv = parse_fragment(urlparts.fragment)
|
|
507 |
|
|
508 |
@property
|
|
509 |
def is_geo_uri(self):
|
|
510 |
return self._uri.startswith('geo:')
|
|
511 |
|
|
512 |
@property
|
|
513 |
def is_web_uri(self):
|
|
514 |
return not self.is_geo_uri
|
|
515 |
|
|
516 |
@property
|
|
517 |
def uri(self):
|
|
518 |
return self._uri
|
|
519 |
|
|
520 |
@property
|
|
521 |
def filename(self):
|
|
522 |
return self._filename
|
|
523 |
|
|
524 |
@property
|
|
525 |
def request_uri(self):
|
|
526 |
if self.is_aes_encrypted:
|
|
527 |
# Remove fragments so we dont transmit it to the server
|
|
528 |
urlparts = self._urlparts._replace(scheme='https', fragment='')
|
|
529 |
return urlparts.geturl()
|
|
530 |
return self._urlparts.geturl()
|
|
531 |
|
|
532 |
@property
|
|
533 |
def is_aes_encrypted(self):
|
|
534 |
if self._urlparts is None:
|
|
535 |
return False
|
|
536 |
return self._urlparts.scheme == 'aesgcm'
|
|
537 |
|
|
538 |
def thumb_exists(self):
|
|
539 |
return self.thumb_path.exists()
|
|
540 |
|
|
541 |
def orig_exists(self):
|
|
542 |
return self.orig_path.exists()
|
|
543 |
|
|
544 |
def create_thumbnail(self, data):
|
|
545 |
self.thumbnail = create_thumbnail(data, self.size)
|
|
546 |
if self.thumbnail is None:
|
|
547 |
log.warning('creating thumbnail failed for: %s', self.orig_path)
|
|
548 |
return False
|
|
549 |
return True
|