Merge branch 'httpupload' into 'gtk3'
httpupload 0.6.0 See merge request !25
This commit is contained in:
@@ -14,267 +14,176 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from gi.repository import GObject, Gtk, GLib
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import threading
|
||||||
import time
|
import ssl
|
||||||
|
import urllib
|
||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
import mimetypes # better use the magic packet, but that's not a standard lib
|
import mimetypes
|
||||||
import gtkgui_helpers
|
|
||||||
import logging
|
import logging
|
||||||
from queue import Queue
|
from binascii import hexlify
|
||||||
import binascii
|
import certifi
|
||||||
|
|
||||||
|
import nbxmpp
|
||||||
|
from gi.repository import Gtk, GLib
|
||||||
|
|
||||||
from common import gajim
|
from common import gajim
|
||||||
from common import ged
|
from common import ged
|
||||||
import chat_control
|
|
||||||
from plugins import GajimPlugin
|
from plugins import GajimPlugin
|
||||||
from plugins.helpers import log_calls
|
from dialogs import FileChooserDialog, ErrorDialog
|
||||||
from dialogs import FileChooserDialog, ImageChooserDialog, ErrorDialog
|
|
||||||
import nbxmpp
|
|
||||||
|
|
||||||
from .thumbnail import thumbnail
|
|
||||||
|
|
||||||
log = logging.getLogger('gajim.plugin_system.httpupload')
|
log = logging.getLogger('gajim.plugin_system.httpupload')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if os.name == 'nt':
|
from cryptography.hazmat.backends import default_backend
|
||||||
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 Cipher
|
||||||
from cryptography.hazmat.primitives.ciphers import algorithms
|
from cryptography.hazmat.primitives.ciphers import algorithms
|
||||||
from cryptography.hazmat.primitives.ciphers.modes import GCM
|
from cryptography.hazmat.primitives.ciphers.modes import GCM
|
||||||
encryption_available = True
|
ENCRYPTION_AVAILABLE = True
|
||||||
except Exception as e:
|
except Exception as exc:
|
||||||
DEP_MSG = 'For encryption of files, ' \
|
DEP_MSG = 'For encryption of files, ' \
|
||||||
'please install python-cryptography!'
|
'please install python-cryptography!'
|
||||||
log.debug('Cryptography Import Error: ' + str(e))
|
log.error('Cryptography Import Error: %s', exc)
|
||||||
log.info('Decryption/Encryption disabled due to errors')
|
log.info('Decryption/Encryption disabled due to errors')
|
||||||
encryption_available = False
|
ENCRYPTION_AVAILABLE = False
|
||||||
|
|
||||||
# XEP-0363 (http://xmpp.org/extensions/xep-0363.html)
|
IQ_CALLBACK = {}
|
||||||
NS_HTTPUPLOAD = 'urn:xmpp:http:upload'
|
NS_HTTPUPLOAD = 'urn:xmpp:http:upload'
|
||||||
TAGSIZE = 16
|
TAGSIZE = 16
|
||||||
|
|
||||||
jid_to_servers = {}
|
|
||||||
iq_ids_to_callbacks = {}
|
|
||||||
last_info_query = {}
|
|
||||||
|
|
||||||
|
|
||||||
class HttpuploadPlugin(GajimPlugin):
|
class HttpuploadPlugin(GajimPlugin):
|
||||||
|
|
||||||
@log_calls('HttpuploadPlugin')
|
|
||||||
def init(self):
|
def init(self):
|
||||||
if not encryption_available:
|
if not ENCRYPTION_AVAILABLE:
|
||||||
self.available_text = DEP_MSG
|
self.available_text = DEP_MSG
|
||||||
self.config_dialog = None # HttpuploadPluginConfigDialog(self)
|
self.config_dialog = None
|
||||||
self.controls = []
|
|
||||||
self.events_handlers = {}
|
self.events_handlers = {}
|
||||||
self.events_handlers['agent-info-received'] = (ged.PRECORE,
|
self.events_handlers['agent-info-received'] = (
|
||||||
self.handle_agent_info_received)
|
ged.PRECORE, self.handle_agent_info_received)
|
||||||
self.events_handlers['raw-iq-received'] = (ged.PRECORE,
|
self.events_handlers['raw-iq-received'] = (
|
||||||
self.handle_iq_received)
|
ged.PRECORE, self.handle_iq_received)
|
||||||
self.gui_extension_points = {
|
self.gui_extension_points = {
|
||||||
'chat_control_base': (self.connect_with_chat_control,
|
'chat_control_base': (self.connect_with_chat_control,
|
||||||
self.disconnect_from_chat_control),
|
self.disconnect_from_chat_control),
|
||||||
'chat_control_base_update_toolbar': (self.update_button_state,
|
'chat_control_base_update_toolbar': (self.update_chat_control,
|
||||||
None)}
|
None)}
|
||||||
self.first_run = True
|
self.gui_interfaces = {}
|
||||||
|
|
||||||
def handle_iq_received(self, event):
|
@staticmethod
|
||||||
global iq_ids_to_callbacks
|
def handle_iq_received(event):
|
||||||
id_ = event.stanza.getAttr("id")
|
id_ = event.stanza.getAttr("id")
|
||||||
if str(id_) in iq_ids_to_callbacks:
|
if id_ in IQ_CALLBACK:
|
||||||
try:
|
try:
|
||||||
iq_ids_to_callbacks[str(id_)](event.stanza)
|
IQ_CALLBACK[id_](event.stanza)
|
||||||
except:
|
except:
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
del iq_ids_to_callbacks[str(id_)]
|
del IQ_CALLBACK[id_]
|
||||||
|
|
||||||
def handle_agent_info_received(self, event):
|
def handle_agent_info_received(self, event):
|
||||||
global jid_to_servers
|
if (NS_HTTPUPLOAD in event.features and
|
||||||
if NS_HTTPUPLOAD in event.features and gajim.jid_is_transport(event.jid):
|
gajim.jid_is_transport(event.jid)):
|
||||||
own_jid = gajim.get_jid_without_resource(str(event.stanza.getTo()))
|
account = event.conn.name
|
||||||
jid_to_servers[own_jid] = event.jid # map own jid to upload component's jid
|
interface = self.get_interface(account)
|
||||||
log.info(own_jid + " can do http uploads via component " + event.jid)
|
interface.enabled = True
|
||||||
# update all buttons
|
interface.component = event.jid
|
||||||
for base in self.controls:
|
interface.update_button_states(True)
|
||||||
self.update_button_state(base.chat_control)
|
|
||||||
|
|
||||||
@log_calls('HttpuploadPlugin')
|
def connect_with_chat_control(self, chat_control):
|
||||||
def connect_with_chat_control(self, control):
|
account = chat_control.contact.account.name
|
||||||
self.chat_control = control
|
self.get_interface(account).add_button(chat_control)
|
||||||
base = Base(self, self.chat_control)
|
|
||||||
self.controls.append(base)
|
|
||||||
if self.first_run:
|
|
||||||
# TODO: Potentially add back keyboard shortcut
|
|
||||||
self.first_run = False
|
|
||||||
self.update_button_state(self.chat_control)
|
|
||||||
|
|
||||||
@log_calls('HttpuploadPlugin')
|
|
||||||
def disconnect_from_chat_control(self, chat_control):
|
def disconnect_from_chat_control(self, chat_control):
|
||||||
for control in self.controls:
|
jid = chat_control.contact.jid
|
||||||
control.disconnect_from_chat_control()
|
account = chat_control.account
|
||||||
self.controls = []
|
interface = self.get_interface(account)
|
||||||
|
if jid not in interface.controls:
|
||||||
|
return
|
||||||
|
actions_hbox = chat_control.xml.get_object('actions_hbox')
|
||||||
|
actions_hbox.remove(interface.controls[jid])
|
||||||
|
|
||||||
@log_calls('HttpuploadPlugin')
|
def update_chat_control(self, chat_control):
|
||||||
def update_button_state(self, chat_control):
|
account = chat_control.account
|
||||||
global jid_to_servers
|
if gajim.connections[account].connection is None:
|
||||||
global iq_ids_to_callbacks
|
self.get_interface(account).update_button_states(False)
|
||||||
global last_info_query
|
|
||||||
|
|
||||||
if gajim.connections[chat_control.account].connection == None and \
|
def get_interface(self, account):
|
||||||
gajim.get_jid_from_account(chat_control.account) in jid_to_servers:
|
try:
|
||||||
# maybe don't delete this and detect vanished upload components when actually trying to upload something
|
return self.gui_interfaces[account]
|
||||||
log.info("Deleting %s from jid_to_servers (disconnected)" % gajim.get_jid_from_account(chat_control.account))
|
except KeyError:
|
||||||
del jid_to_servers[gajim.get_jid_from_account(chat_control.account)]
|
self.gui_interfaces[account] = Base(self, account)
|
||||||
|
return self.gui_interfaces[account]
|
||||||
# query info at most every 60 seconds in case something goes wrong
|
|
||||||
if ((not chat_control.account in last_info_query or
|
|
||||||
last_info_query[chat_control.account] + 60 < time.time())
|
|
||||||
and not gajim.get_jid_from_account(chat_control.account) in jid_to_servers
|
|
||||||
and gajim.account_is_connected(chat_control.account)
|
|
||||||
):
|
|
||||||
log.info("Account %s: Using dicovery to find jid of httpupload component" % chat_control.account)
|
|
||||||
id_ = gajim.get_an_id()
|
|
||||||
iq = nbxmpp.Iq(
|
|
||||||
typ='get',
|
|
||||||
to=gajim.get_server_from_jid(gajim.get_jid_from_account(chat_control.account)),
|
|
||||||
queryNS="http://jabber.org/protocol/disco#items"
|
|
||||||
)
|
|
||||||
iq.setID(id_)
|
|
||||||
def query_info(stanza):
|
|
||||||
global last_info_query
|
|
||||||
for item in stanza.getTag("query").getTags("item"):
|
|
||||||
id_ = gajim.get_an_id()
|
|
||||||
iq = nbxmpp.Iq(
|
|
||||||
typ='get',
|
|
||||||
to=item.getAttr("jid"),
|
|
||||||
queryNS="http://jabber.org/protocol/disco#info"
|
|
||||||
)
|
|
||||||
iq.setID(id_)
|
|
||||||
last_info_query[chat_control.account] = time.time()
|
|
||||||
gajim.connections[chat_control.account].connection.send(iq)
|
|
||||||
iq_ids_to_callbacks[str(id_)] = query_info
|
|
||||||
gajim.connections[chat_control.account].connection.send(iq)
|
|
||||||
#send disco query to main server jid
|
|
||||||
id_ = gajim.get_an_id()
|
|
||||||
iq = nbxmpp.Iq(
|
|
||||||
typ='get',
|
|
||||||
to=gajim.get_server_from_jid(gajim.get_jid_from_account(chat_control.account)),
|
|
||||||
queryNS="http://jabber.org/protocol/disco#info"
|
|
||||||
)
|
|
||||||
iq.setID(id_)
|
|
||||||
last_info_query[chat_control.account] = time.time()
|
|
||||||
gajim.connections[chat_control.account].connection.send(iq)
|
|
||||||
|
|
||||||
for base in self.controls:
|
|
||||||
if base.chat_control == chat_control:
|
|
||||||
is_supported = gajim.get_jid_from_account(chat_control.account) in jid_to_servers and \
|
|
||||||
gajim.connections[chat_control.account].connection != None
|
|
||||||
log.info("Account %s: httpupload is_supported: %s" % (str(chat_control.account), str(is_supported)))
|
|
||||||
if not is_supported:
|
|
||||||
text = _('Your server does not support http uploads')
|
|
||||||
image_text = text
|
|
||||||
else:
|
|
||||||
text = _('Send file via http upload')
|
|
||||||
image_text = _('Send image via http upload')
|
|
||||||
base.button.set_sensitive(is_supported)
|
|
||||||
base.button.set_tooltip_text(text)
|
|
||||||
base.image_button.set_sensitive(is_supported)
|
|
||||||
base.image_button.set_tooltip_text(image_text)
|
|
||||||
|
|
||||||
|
|
||||||
class Base(object):
|
class Base(object):
|
||||||
def __init__(self, plugin, chat_control):
|
def __init__(self, plugin, account):
|
||||||
self.dlg = None
|
|
||||||
self.dialog_type = 'file'
|
|
||||||
self.plugin = plugin
|
self.plugin = plugin
|
||||||
|
self.account = account
|
||||||
self.encrypted_upload = False
|
self.encrypted_upload = False
|
||||||
self.chat_control = chat_control
|
self.enabled = False
|
||||||
actions_hbox = chat_control.xml.get_object('actions_hbox')
|
self.component = None
|
||||||
self.button = Gtk.Button(label=None, stock=None, use_underline=True)
|
self.controls = {}
|
||||||
self.button.set_property('can-focus', False)
|
self.conn = gajim.connections[account].connection
|
||||||
self.button.set_sensitive(False)
|
|
||||||
|
def add_button(self, chat_control):
|
||||||
|
jid = chat_control.contact.jid
|
||||||
|
|
||||||
img = Gtk.Image()
|
img = Gtk.Image()
|
||||||
img.set_from_file(self.plugin.local_file_path('httpupload.png'))
|
img.set_from_file(self.plugin.local_file_path('httpupload.png'))
|
||||||
self.button.set_image(img)
|
actions_hbox = chat_control.xml.get_object('actions_hbox')
|
||||||
self.button.set_tooltip_text(_('Your server does not support http uploads'))
|
button = Gtk.Button(label=None, stock=None, use_underline=True)
|
||||||
self.button.set_relief(Gtk.ReliefStyle.NONE)
|
button.set_property('can-focus', False)
|
||||||
self.image_button = Gtk.Button(label=None, stock=None, use_underline=True)
|
button.set_image(img)
|
||||||
self.image_button.set_property('can-focus', False)
|
button.set_relief(Gtk.ReliefStyle.NONE)
|
||||||
self.image_button.set_relief(Gtk.ReliefStyle.NONE)
|
|
||||||
self.image_button.set_sensitive(False)
|
actions_hbox.add(button)
|
||||||
img = Gtk.Image()
|
|
||||||
img.set_from_file(self.plugin.local_file_path('image.png'))
|
|
||||||
self.image_button.set_image(img)
|
|
||||||
self.image_button.set_tooltip_text(_('Your server does not support http uploads'))
|
|
||||||
send_button = chat_control.xml.get_object('send_button')
|
send_button = chat_control.xml.get_object('send_button')
|
||||||
actions_hbox.add(self.button)
|
button_pos = actions_hbox.child_get_property(send_button, 'position')
|
||||||
actions_hbox.add(self.image_button)
|
actions_hbox.child_set_property(button, 'position', button_pos - 1)
|
||||||
|
|
||||||
send_button_pos = actions_hbox.child_get_property(send_button, 'position')
|
self.controls[jid] = button
|
||||||
actions_hbox.child_set_property(self.image_button, 'position', send_button_pos - 1)
|
id_ = button.connect(
|
||||||
actions_hbox.child_set_property(self.button, 'position', send_button_pos - 1)
|
'clicked', self.on_file_button_clicked, jid, chat_control)
|
||||||
|
chat_control.handlers[id_] = button
|
||||||
|
self.set_button_state(self.enabled, button)
|
||||||
|
button.show()
|
||||||
|
|
||||||
file_id = self.button.connect('clicked', self.on_file_button_clicked)
|
@staticmethod
|
||||||
image_id = self.image_button.connect('clicked', self.on_image_button_clicked)
|
def set_button_state(state, button):
|
||||||
chat_control.handlers[file_id] = self.button
|
if state:
|
||||||
chat_control.handlers[image_id] = self.image_button
|
button.set_sensitive(state)
|
||||||
self.button.show()
|
button.set_tooltip_text(_('Send file via http upload'))
|
||||||
self.image_button.show()
|
else:
|
||||||
|
button.set_sensitive(state)
|
||||||
|
button.set_tooltip_text(
|
||||||
|
_('Your server does not support http uploads'))
|
||||||
|
|
||||||
|
def update_button_states(self, state):
|
||||||
|
for jid in self.controls:
|
||||||
|
self.set_button_state(state, self.controls[jid])
|
||||||
|
|
||||||
def disconnect_from_chat_control(self):
|
def encryption_activated(self, jid):
|
||||||
actions_hbox = self.chat_control.xml.get_object('actions_hbox')
|
if not ENCRYPTION_AVAILABLE:
|
||||||
actions_hbox.remove(self.button)
|
|
||||||
actions_hbox.remove(self.image_button)
|
|
||||||
|
|
||||||
def encryption_activated(self):
|
|
||||||
if not encryption_available:
|
|
||||||
return False
|
return False
|
||||||
jid = self.chat_control.contact.jid
|
|
||||||
account = self.chat_control.account
|
|
||||||
for plugin in gajim.plugin_manager.active_plugins:
|
for plugin in gajim.plugin_manager.active_plugins:
|
||||||
if type(plugin).__name__ == 'OmemoPlugin':
|
if type(plugin).__name__ == 'OmemoPlugin':
|
||||||
omemo = plugin
|
state = plugin.get_omemo_state(self.account)
|
||||||
break
|
encryption = state.encryption.is_active(jid)
|
||||||
if omemo:
|
log.info('Encryption is: %s', bool(encryption))
|
||||||
state = omemo.get_omemo_state(account)
|
return bool(encryption)
|
||||||
log.info('Encryption is: ' +
|
log.info('OMEMO not found, encryption disabled')
|
||||||
str(state.encryption.is_active(jid)))
|
|
||||||
return state.encryption.is_active(jid)
|
|
||||||
log.info('Encryption is: False / OMEMO not found')
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def on_file_dialog_ok(self, widget, path_to_file=None):
|
def on_file_dialog_ok(self, widget, jid, chat_control):
|
||||||
global jid_to_servers
|
path = widget.get_filename()
|
||||||
|
widget.destroy()
|
||||||
|
|
||||||
try:
|
if not path or not os.path.exists(path):
|
||||||
self.encrypted_upload = self.encryption_activated()
|
|
||||||
except Exception as e:
|
|
||||||
log.debug(e)
|
|
||||||
self.encrypted_upload = False
|
|
||||||
|
|
||||||
if not path_to_file:
|
|
||||||
path_to_file = self.dlg.get_filename()
|
|
||||||
if not path_to_file:
|
|
||||||
self.dlg.destroy()
|
|
||||||
return
|
|
||||||
self.dlg.destroy()
|
|
||||||
if not os.path.exists(path_to_file):
|
|
||||||
return
|
return
|
||||||
if self.encrypted_upload:
|
|
||||||
filesize = os.path.getsize(path_to_file) + TAGSIZE # in bytes
|
|
||||||
else:
|
|
||||||
filesize = os.path.getsize(path_to_file)
|
|
||||||
invalid_file = False
|
invalid_file = False
|
||||||
msg = ''
|
if os.path.isfile(path):
|
||||||
if os.path.isfile(path_to_file):
|
stat = os.stat(path)
|
||||||
stat = os.stat(path_to_file)
|
|
||||||
if stat[6] == 0:
|
if stat[6] == 0:
|
||||||
invalid_file = True
|
invalid_file = True
|
||||||
msg = _('File is empty')
|
msg = _('File is empty')
|
||||||
@@ -282,207 +191,177 @@ class Base(object):
|
|||||||
invalid_file = True
|
invalid_file = True
|
||||||
msg = _('File does not exist')
|
msg = _('File does not exist')
|
||||||
if invalid_file:
|
if invalid_file:
|
||||||
ErrorDialog(_('Could not open file'), msg, transient_for=self.chat_control.parent_win.window)
|
ErrorDialog(_('Could not open file'), msg,
|
||||||
|
transient_for=chat_control.parent_win.window)
|
||||||
return
|
return
|
||||||
|
|
||||||
mime_type = mimetypes.MimeTypes().guess_type(path_to_file)[0]
|
encrypted = self.encryption_activated(jid)
|
||||||
if not mime_type:
|
size = os.path.getsize(path)
|
||||||
mime_type = 'application/octet-stream' # fallback mime type
|
key, iv = None, None
|
||||||
log.info("Detected MIME Type of file: " + str(mime_type))
|
if encrypted:
|
||||||
progress_messages = Queue(8)
|
key = os.urandom(32)
|
||||||
progress_window = ProgressWindow(_('HTTP Upload'), _('Requesting HTTP Upload Slot...'),
|
iv = os.urandom(16)
|
||||||
progress_messages, self.plugin, parent=self.chat_control.parent_win.window)
|
size += TAGSIZE
|
||||||
def upload_file(stanza):
|
|
||||||
slot = stanza.getTag("slot")
|
|
||||||
if not slot:
|
|
||||||
log.error("got unexpected stanza: "+str(stanza))
|
|
||||||
progress_window.close_dialog()
|
|
||||||
error = stanza.getTag("error")
|
|
||||||
if error and error.getTag("text"):
|
|
||||||
ErrorDialog(_('Could not request upload slot'),
|
|
||||||
_('Got unexpected response from server: %s') % str(error.getTagData("text")),
|
|
||||||
transient_for=self.chat_control.parent_win.window)
|
|
||||||
else:
|
|
||||||
ErrorDialog(_('Could not request upload slot'),
|
|
||||||
_('Got unexpected response from server (protocol mismatch??)'),
|
|
||||||
transient_for=self.chat_control.parent_win.window)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
mime = mimetypes.MimeTypes().guess_type(path)[0]
|
||||||
if self.encrypted_upload:
|
if not mime:
|
||||||
key = os.urandom(32)
|
mime = 'application/octet-stream' # fallback mime type
|
||||||
iv = os.urandom(16)
|
log.info("Detected MIME type of file: %s", mime)
|
||||||
data = StreamFileWithProgress(path_to_file,
|
|
||||||
"rb",
|
|
||||||
progress_window.update_progress,
|
|
||||||
self.encrypted_upload, key, iv)
|
|
||||||
else:
|
|
||||||
data = StreamFileWithProgress(path_to_file,
|
|
||||||
"rb",
|
|
||||||
progress_window.update_progress)
|
|
||||||
except:
|
|
||||||
log.error("Could not open file")
|
|
||||||
progress_window.close_dialog()
|
|
||||||
ErrorDialog(_('Could not open file'),
|
|
||||||
_('Exception raised while opening file (see error log for more information)'),
|
|
||||||
transient_for=self.chat_control.parent_win.window)
|
|
||||||
raise # fill error log with useful information
|
|
||||||
|
|
||||||
put = slot.getTag("put")
|
event = threading.Event()
|
||||||
get = slot.getTag("get")
|
progress = ProgressWindow(
|
||||||
if not put or not get:
|
self.plugin, chat_control.parent_win.window, event)
|
||||||
log.error("got unexpected stanza: " + str(stanza))
|
|
||||||
progress_window.close_dialog()
|
|
||||||
ErrorDialog(_('Could not request upload slot'),
|
|
||||||
_('Got unexpected response from server (protocol mismatch??)'),
|
|
||||||
transient_for=self.chat_control.parent_win.window)
|
|
||||||
return
|
|
||||||
|
|
||||||
def upload_complete(response_code):
|
file = File(path=path, size=size, mime=mime, encrypted=encrypted,
|
||||||
if response_code == 0:
|
key=key, iv=iv, control=chat_control,
|
||||||
return # Upload was aborted
|
progress=progress, event=event)
|
||||||
if 200 <= response_code < 300:
|
self.request_slot(file)
|
||||||
log.info("Upload completed successfully")
|
|
||||||
xhtml = None
|
|
||||||
is_image = mime_type.split('/', 1)[0] == 'image'
|
|
||||||
if ((not isinstance(self.chat_control, chat_control.ChatControl)
|
|
||||||
or not self.chat_control.gpg_is_active)
|
|
||||||
and self.dialog_type == 'image'
|
|
||||||
and is_image
|
|
||||||
and not self.encrypted_upload
|
|
||||||
):
|
|
||||||
progress_messages.put(_('Calculating (possible) image thumbnail...'))
|
|
||||||
thumb = thumbnail(path_to_file)
|
|
||||||
if thumb:
|
|
||||||
xhtml = '<body><br/><a href="%s"><img alt="%s" src="data:image/jpeg;base64,%s"/></a></body>' % \
|
|
||||||
(get.getData(), get.getData(), thumb)
|
|
||||||
progress_window.close_dialog()
|
|
||||||
id_ = gajim.get_an_id()
|
|
||||||
def add_oob_tag():
|
|
||||||
pass
|
|
||||||
if self.encrypted_upload:
|
|
||||||
keyAndIv = '#' + binascii.hexlify(iv) + binascii.hexlify(key)
|
|
||||||
self.chat_control.send_message(message=get.getData() + keyAndIv, xhtml=None)
|
|
||||||
else:
|
|
||||||
self.chat_control.send_message(message=get.getData(), xhtml=xhtml)
|
|
||||||
self.chat_control.msg_textview.grab_focus()
|
|
||||||
else:
|
|
||||||
progress_window.close_dialog()
|
|
||||||
log.error("got unexpected http upload response code: " + str(response_code))
|
|
||||||
ErrorDialog(_('Could not upload file'),
|
|
||||||
_('Got unexpected http response code from server: ') + str(response_code),
|
|
||||||
transient_for=self.chat_control.parent_win.window)
|
|
||||||
|
|
||||||
def on_upload_error():
|
def on_file_button_clicked(self, widget, jid, chat_control):
|
||||||
progress_window.close_dialog()
|
FileChooserDialog(
|
||||||
ErrorDialog(_('Could not upload file'),
|
on_response_ok=lambda widget: self.on_file_dialog_ok(widget, jid,
|
||||||
_('Got unexpected exception while uploading file'
|
chat_control),
|
||||||
' (see error log for more information)'),
|
title_text=_('Choose file to send'),
|
||||||
transient_for=self.chat_control.parent_win.window)
|
action=Gtk.FileChooserAction.OPEN,
|
||||||
return 0
|
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
|
||||||
|
Gtk.STOCK_OPEN, Gtk.ResponseType.OK),
|
||||||
|
default_response=Gtk.ResponseType.OK,
|
||||||
|
transient_for=chat_control.parent_win.window)
|
||||||
|
|
||||||
def uploader():
|
def request_slot(self, file):
|
||||||
progress_messages.put(_('Uploading file via HTTP...'))
|
iq = nbxmpp.Iq(typ='get', to=self.component)
|
||||||
try:
|
|
||||||
headers = {'User-Agent': 'Gajim %s' % gajim.version,
|
|
||||||
'Content-Type': mime_type}
|
|
||||||
request = Request(put.getData(), data=data, headers=headers, method='PUT')
|
|
||||||
log.debug("opening urllib upload request...")
|
|
||||||
transfer = urlopen(request, timeout=30)
|
|
||||||
data.close()
|
|
||||||
log.debug("urllib upload request done, response code: " + str(transfer.getcode()))
|
|
||||||
return transfer.getcode()
|
|
||||||
except UploadAbortedException:
|
|
||||||
log.info("Upload aborted")
|
|
||||||
except:
|
|
||||||
log.error("Exception during upload", exc_info=sys.exc_info())
|
|
||||||
GLib.idle_add(on_upload_error)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
log.info("Uploading file to '%s'..." % str(put.getData()))
|
|
||||||
log.info("Please download from '%s' later..." % str(get.getData()))
|
|
||||||
|
|
||||||
gajim.thread_interface(uploader, [], upload_complete)
|
|
||||||
|
|
||||||
is_supported = gajim.get_jid_from_account(self.chat_control.account) in jid_to_servers and \
|
|
||||||
gajim.connections[self.chat_control.account].connection != None
|
|
||||||
log.info("jid_to_servers of %s: %s ; connection: %s",
|
|
||||||
gajim.get_jid_from_account(self.chat_control.account),
|
|
||||||
str(jid_to_servers[gajim.get_jid_from_account(self.chat_control.account)]),
|
|
||||||
str(gajim.connections[self.chat_control.account].connection))
|
|
||||||
if not is_supported:
|
|
||||||
progress_window.close_dialog()
|
|
||||||
log.error("upload component vanished, account got disconnected??")
|
|
||||||
ErrorDialog(_('Your server does not support http uploads or you just got disconnected.\nPlease try to reconnect or reopen the chat window to fix this.'),
|
|
||||||
transient_for=self.chat_control.parent_win.window)
|
|
||||||
return
|
|
||||||
|
|
||||||
# create iq for slot request
|
|
||||||
id_ = gajim.get_an_id()
|
id_ = gajim.get_an_id()
|
||||||
iq = nbxmpp.Iq(
|
|
||||||
typ='get',
|
|
||||||
to=jid_to_servers[gajim.get_jid_from_account(self.chat_control.account)],
|
|
||||||
queryNS=None
|
|
||||||
)
|
|
||||||
iq.setID(id_)
|
iq.setID(id_)
|
||||||
request = iq.addChild(
|
request = iq.setTag(name="request", namespace=NS_HTTPUPLOAD)
|
||||||
name="request",
|
request.addChild('filename', payload=os.path.basename(file.path))
|
||||||
namespace=NS_HTTPUPLOAD
|
request.addChild('size', payload=file.size)
|
||||||
)
|
request.addChild('content-type', payload=file.mime)
|
||||||
filename = request.addChild(
|
|
||||||
name="filename",
|
|
||||||
)
|
|
||||||
filename.addData(os.path.basename(path_to_file))
|
|
||||||
size = request.addChild(
|
|
||||||
name="size",
|
|
||||||
)
|
|
||||||
size.addData(filesize)
|
|
||||||
content_type = request.addChild(
|
|
||||||
name="content-type",
|
|
||||||
)
|
|
||||||
content_type.addData(mime_type)
|
|
||||||
|
|
||||||
# send slot request and register callback
|
log.info("Sending request for slot")
|
||||||
log.debug("sending httpupload slot request iq...")
|
IQ_CALLBACK[id_] = lambda stanza: self.received_slot(stanza, file)
|
||||||
iq_ids_to_callbacks[str(id_)] = upload_file
|
self.conn.send(iq)
|
||||||
gajim.connections[self.chat_control.account].connection.send(iq)
|
|
||||||
|
|
||||||
self.chat_control.msg_textview.grab_focus()
|
def received_slot(self, stanza, file):
|
||||||
|
log.info("Received slot")
|
||||||
|
if stanza.getType() == 'error':
|
||||||
|
file.progress.close_dialog()
|
||||||
|
ErrorDialog(_('Could not request upload slot'),
|
||||||
|
stanza.getErrorMsg(),
|
||||||
|
transient_for=file.control.parent_win.window)
|
||||||
|
log.error(stanza)
|
||||||
|
return
|
||||||
|
|
||||||
def on_file_button_clicked(self, widget):
|
try:
|
||||||
self.dialog_type = 'file'
|
file.put = stanza.getTag("slot").getTag("put").getData()
|
||||||
self.dlg = FileChooserDialog(on_response_ok=self.on_file_dialog_ok, on_response_cancel=None,
|
file.get = stanza.getTag("slot").getTag("get").getData()
|
||||||
title_text = _('Choose file to send'), action = Gtk.FileChooserAction.OPEN,
|
except Exception:
|
||||||
buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK),
|
file.progress.close_dialog()
|
||||||
default_response = Gtk.ResponseType.OK,)
|
log.error("Got unexpected stanza: %s", stanza)
|
||||||
self.dlg.set_transient_for(self.chat_control.parent_win.window)
|
log.exception('Error')
|
||||||
|
ErrorDialog(_('Could not request upload slot'),
|
||||||
|
_('Got unexpected response from server (see log)'),
|
||||||
|
transient_for=file.control.parent_win.window)
|
||||||
|
return
|
||||||
|
|
||||||
def on_image_button_clicked(self, widget):
|
try:
|
||||||
self.dialog_type = 'image'
|
file.stream = StreamFileWithProgress(file, "rb")
|
||||||
self.dlg = ImageChooserDialog(on_response_ok=self.on_file_dialog_ok, on_response_cancel=None)
|
except Exception as exc:
|
||||||
self.dlg.set_transient_for(self.chat_control.parent_win.window)
|
file.progress.close_dialog()
|
||||||
|
log.exception("Could not open file")
|
||||||
|
ErrorDialog(_('Could not open file'),
|
||||||
|
_('Exception raised while opening file (see log)'),
|
||||||
|
transient_for=file.control.parent_win.window)
|
||||||
|
return
|
||||||
|
|
||||||
|
log.info('Uploading file to %s', file.put)
|
||||||
|
log.info('Please download from %s', file.get)
|
||||||
|
|
||||||
|
thread = threading.Thread(target=self.upload_file, args=(file,))
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def upload_file(self, file):
|
||||||
|
GLib.idle_add(file.progress.label.set_text,
|
||||||
|
_('Uploading file via HTTP...'))
|
||||||
|
try:
|
||||||
|
headers = {'User-Agent': 'Gajim %s' % gajim.version,
|
||||||
|
'Content-Type': file.mime}
|
||||||
|
request = Request(
|
||||||
|
file.put, data=file.stream, headers=headers, method='PUT')
|
||||||
|
log.info("Opening Urllib upload request...")
|
||||||
|
if os.name == 'nt':
|
||||||
|
transfer = urlopen(request, cafile=certifi.where(), timeout=30)
|
||||||
|
else:
|
||||||
|
transfer = urlopen(request, timeout=30)
|
||||||
|
file.stream.close()
|
||||||
|
log.info('Urllib upload request done, response code: %s',
|
||||||
|
transfer.getcode())
|
||||||
|
GLib.idle_add(self.upload_complete, transfer.getcode(), file)
|
||||||
|
return
|
||||||
|
except UploadAbortedException as exc:
|
||||||
|
log.info(exc)
|
||||||
|
error_msg = exc
|
||||||
|
except urllib.error.URLError as exc:
|
||||||
|
if isinstance(exc.reason, ssl.SSLError):
|
||||||
|
error_msg = exc.reason.reason
|
||||||
|
if error_msg == 'CERTIFICATE_VERIFY_FAILED':
|
||||||
|
log.exception('Certificate verify failed')
|
||||||
|
except Exception as exc:
|
||||||
|
log.exception("Exception during upload")
|
||||||
|
error_msg = exc
|
||||||
|
GLib.idle_add(file.progress.close_dialog)
|
||||||
|
GLib.idle_add(self.on_upload_error, file, error_msg)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def upload_complete(response_code, file):
|
||||||
|
file.progress.close_dialog()
|
||||||
|
if 200 <= response_code < 300:
|
||||||
|
log.info("Upload completed successfully")
|
||||||
|
message = file.get
|
||||||
|
if file.encrypted:
|
||||||
|
message += '#' + hexlify(file.iv + file.key).decode('utf-8')
|
||||||
|
file.control.send_message(message=message)
|
||||||
|
file.control.msg_textview.grab_focus()
|
||||||
|
else:
|
||||||
|
log.error('Got unexpected http upload response code: %s',
|
||||||
|
response_code)
|
||||||
|
ErrorDialog(
|
||||||
|
_('Could not upload file'),
|
||||||
|
_('HTTP response code from server: %s') % response_code,
|
||||||
|
transient_for=file.control.parent_win.window)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def on_upload_error(file, reason):
|
||||||
|
file.progress.close_dialog()
|
||||||
|
ErrorDialog(_('Error'), str(reason),
|
||||||
|
transient_for=file.control.parent_win.window)
|
||||||
|
|
||||||
|
|
||||||
|
class File:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
setattr(self, k, v)
|
||||||
|
self.stream = None
|
||||||
|
self.put = None
|
||||||
|
self.get = None
|
||||||
|
|
||||||
|
|
||||||
class StreamFileWithProgress:
|
class StreamFileWithProgress:
|
||||||
def __init__(self, path, mode, callback=None,
|
def __init__(self, file, mode, *args):
|
||||||
encrypted_upload=False, key=None, iv=None, *args):
|
self.event = file.event
|
||||||
self.backing = open(path, mode)
|
self.backing = open(file.path, mode)
|
||||||
self.encrypted_upload = encrypted_upload
|
self.encrypted = file.encrypted
|
||||||
self.backing.seek(0, os.SEEK_END)
|
self.backing.seek(0, os.SEEK_END)
|
||||||
if self.encrypted_upload:
|
if self.encrypted:
|
||||||
if os.name == 'nt':
|
|
||||||
self.backend = backend
|
|
||||||
else:
|
|
||||||
self.backend = default_backend()
|
|
||||||
self.encryptor = Cipher(
|
self.encryptor = Cipher(
|
||||||
algorithms.AES(key),
|
algorithms.AES(file.key),
|
||||||
GCM(iv),
|
GCM(file.iv),
|
||||||
backend=self.backend).encryptor()
|
backend=default_backend()).encryptor()
|
||||||
self._total = self.backing.tell() + TAGSIZE
|
self._total = self.backing.tell() + TAGSIZE
|
||||||
else:
|
else:
|
||||||
self._total = self.backing.tell()
|
self._total = self.backing.tell()
|
||||||
self.backing.seek(0)
|
self.backing.seek(0)
|
||||||
self._callback = callback
|
self._callback = file.progress.update_progress
|
||||||
self._args = args
|
self._args = args
|
||||||
self._seen = 0
|
self._seen = 0
|
||||||
|
|
||||||
@@ -490,7 +369,9 @@ class StreamFileWithProgress:
|
|||||||
return self._total
|
return self._total
|
||||||
|
|
||||||
def read(self, size):
|
def read(self, size):
|
||||||
if self.encrypted_upload:
|
if self.event.isSet():
|
||||||
|
raise UploadAbortedException
|
||||||
|
if self.encrypted:
|
||||||
data = self.backing.read(size)
|
data = self.backing.read(size)
|
||||||
if len(data) > 0:
|
if len(data) > 0:
|
||||||
data = self.encryptor.update(data)
|
data = self.encryptor.update(data)
|
||||||
@@ -500,13 +381,15 @@ class StreamFileWithProgress:
|
|||||||
data += self.encryptor.tag
|
data += self.encryptor.tag
|
||||||
self._seen += TAGSIZE
|
self._seen += TAGSIZE
|
||||||
if self._callback:
|
if self._callback:
|
||||||
self._callback(self._seen, self._total, *self._args)
|
GLib.idle_add(
|
||||||
|
self._callback, self._seen, self._total, *self._args)
|
||||||
return data
|
return data
|
||||||
else:
|
else:
|
||||||
data = self.backing.read(size)
|
data = self.backing.read(size)
|
||||||
self._seen += len(data)
|
self._seen += len(data)
|
||||||
if self._callback:
|
if self._callback:
|
||||||
self._callback(self._seen, self._total, *self._args)
|
GLib.idle_add(
|
||||||
|
self._callback, self._seen, self._total, *self._args)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
@@ -514,67 +397,47 @@ class StreamFileWithProgress:
|
|||||||
|
|
||||||
|
|
||||||
class ProgressWindow:
|
class ProgressWindow:
|
||||||
def __init__(self, title_text, during_text, messages_queue, plugin, parent):
|
def __init__(self, plugin, parent, event):
|
||||||
self.plugin = plugin
|
self.plugin = plugin
|
||||||
self.xml = gtkgui_helpers.get_gtk_builder(self.plugin.local_file_path('upload_progress_dialog.ui'))
|
self.event = event
|
||||||
self.messages_queue = messages_queue
|
glade_file = self.plugin.local_file_path('upload_progress_dialog.ui')
|
||||||
|
self.xml = Gtk.Builder()
|
||||||
|
self.xml.add_from_file(glade_file)
|
||||||
self.dialog = self.xml.get_object('progress_dialog')
|
self.dialog = self.xml.get_object('progress_dialog')
|
||||||
self.dialog.set_transient_for(parent)
|
self.dialog.set_transient_for(parent)
|
||||||
|
self.dialog.set_title('HTTP Upload')
|
||||||
self.label = self.xml.get_object('label')
|
self.label = self.xml.get_object('label')
|
||||||
self.cancel_button = self.xml.get_object('close_button')
|
self.label.set_text(_('Requesting HTTP Upload Slot...'))
|
||||||
self.label.set_markup('<big>' + during_text + '</big>')
|
|
||||||
self.progressbar = self.xml.get_object('progressbar')
|
self.progressbar = self.xml.get_object('progressbar')
|
||||||
self.progressbar.set_text("")
|
|
||||||
self.dialog.set_title(title_text)
|
|
||||||
#self.dialog.set_geometry_hints(min_width=400, min_height=96)
|
|
||||||
#self.dialog.set_position(Gtk.WIN_POS_CENTER_ON_PARENT)
|
|
||||||
self.dialog.show_all()
|
self.dialog.show_all()
|
||||||
self.xml.connect_signals(self)
|
self.xml.connect_signals(self)
|
||||||
|
|
||||||
self.stopped = False
|
self.pulse = GLib.timeout_add(100, self.pulse_progressbar)
|
||||||
self.pulse_progressbar_timeout_id = GLib.timeout_add(100, self.pulse_progressbar)
|
|
||||||
self.process_messages_queue_timeout_id = GLib.timeout_add(100, self.process_messages_queue)
|
|
||||||
|
|
||||||
|
|
||||||
def pulse_progressbar(self):
|
def pulse_progressbar(self):
|
||||||
if self.dialog:
|
if self.dialog:
|
||||||
self.progressbar.pulse()
|
self.progressbar.pulse()
|
||||||
return True # loop forever
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def process_messages_queue(self):
|
def on_destroy(self, *args):
|
||||||
if not self.messages_queue.empty():
|
self.event.set()
|
||||||
self.label.set_markup('<big>' + self.messages_queue.get() + '</big>')
|
if self.pulse:
|
||||||
if self.dialog:
|
GLib.source_remove(self.pulse)
|
||||||
return True # loop forever
|
|
||||||
return False
|
|
||||||
|
|
||||||
def on_progress_dialog_delete_event(self, widget, event):
|
|
||||||
self.stopped = True
|
|
||||||
if self.pulse_progressbar_timeout_id:
|
|
||||||
GLib.source_remove(self.pulse_progressbar_timeout_id)
|
|
||||||
GLib.source_remove(self.process_messages_queue_timeout_id)
|
|
||||||
|
|
||||||
def on_cancel(self, widget):
|
|
||||||
self.stopped = True
|
|
||||||
if self.pulse_progressbar_timeout_id:
|
|
||||||
GLib.source_remove(self.pulse_progressbar_timeout_id)
|
|
||||||
GLib.source_remove(self.process_messages_queue_timeout_id)
|
|
||||||
self.dialog.destroy()
|
|
||||||
|
|
||||||
def update_progress(self, seen, total):
|
def update_progress(self, seen, total):
|
||||||
if self.stopped == True:
|
if self.event.isSet():
|
||||||
raise UploadAbortedException
|
return
|
||||||
if self.pulse_progressbar_timeout_id:
|
if self.pulse:
|
||||||
GLib.source_remove(self.pulse_progressbar_timeout_id)
|
GLib.source_remove(self.pulse)
|
||||||
self.pulse_progressbar_timeout_id = None
|
self.pulse = None
|
||||||
pct = (float(seen) / total) * 100.0
|
pct = (float(seen) / total) * 100.0
|
||||||
self.progressbar.set_fraction(float(seen) / total)
|
self.progressbar.set_fraction(float(seen) / total)
|
||||||
self.progressbar.set_text(str(int(pct)) + "%")
|
self.progressbar.set_text(str(int(pct)) + "%")
|
||||||
log.debug('upload progress: %.2f%% (%d of %d bytes)' % (pct, seen, total))
|
|
||||||
|
|
||||||
def close_dialog(self):
|
def close_dialog(self, *args):
|
||||||
self.on_cancel(None)
|
self.dialog.destroy()
|
||||||
|
|
||||||
|
|
||||||
class UploadAbortedException(Exception):
|
class UploadAbortedException(Exception):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
[info]
|
[info]
|
||||||
name: HttpUpload
|
name: HttpUpload
|
||||||
short_name: httpupload
|
short_name: httpupload
|
||||||
version: 0.5.2
|
version: 0.6.0
|
||||||
description: This plugin is designed to send a file to a contact or muc by using httpupload.<br/>
|
description: This plugin is designed to send a file to a contact or muc by using httpupload.<br/>
|
||||||
Your server must support <a href="http://xmpp.org/extensions/xep-0363.html">XEP-0363: HTTP Upload</a>.<br/>
|
Your server must support <a href="http://xmpp.org/extensions/xep-0363.html">XEP-0363: HTTP Upload</a>.<br/>
|
||||||
Conversations supported this.<br/>
|
|
||||||
If the receiving side supports <a href="http://xmpp.org/extensions/xep-0071.html">XEP-0071: XHTML-IM</a>
|
|
||||||
and maintains the scheme data: URI, a thumbnail image is send along the link to the full size image.
|
|
||||||
If the receiving side doesn't support this, only a text message containing the link to the image is send.
|
|
||||||
authors: Thilo Molitor <thilo@eightysoft.de>
|
authors: Thilo Molitor <thilo@eightysoft.de>
|
||||||
Philipp Hörist <philipp@hoerist.com>
|
Philipp Hörist <philipp@hoerist.com>
|
||||||
Linus Heckemann <linus@sphalerite.org>
|
Linus Heckemann <linus@sphalerite.org>
|
||||||
homepage: https://trac-plugins.gajim.org/wiki/HttpUploadPlugin
|
homepage: https://dev.gajim.org/gajim/gajim-plugins/wikis/HttpUploadPlugin
|
||||||
min_gajim_version: 0.16.10
|
min_gajim_version: 0.16.10
|
||||||
max_gajim_version: 0.16.10.1
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!-- Generated with glade 3.18.3 -->
|
<!-- Generated with glade 3.20.0 -->
|
||||||
<interface>
|
<interface>
|
||||||
<requires lib="gtk+" version="2.16"/>
|
<requires lib="gtk+" version="3.14"/>
|
||||||
<object class="GtkDialog" id="progress_dialog">
|
<object class="GtkDialog" id="progress_dialog">
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="resizable">False</property>
|
<property name="resizable">False</property>
|
||||||
<property name="icon_name">upload-media</property>
|
<property name="icon_name">upload-media</property>
|
||||||
<property name="type_hint">dialog</property>
|
<property name="type_hint">dialog</property>
|
||||||
<signal name="delete-event" handler="on_progress_dialog_delete_event" swapped="no"/>
|
<signal name="destroy" handler="on_destroy" swapped="no"/>
|
||||||
<child internal-child="vbox">
|
<child internal-child="vbox">
|
||||||
<object class="GtkBox" id="dialog-vbox">
|
<object class="GtkBox" id="dialog-vbox">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
@@ -25,7 +25,6 @@
|
|||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="top_padding">2</property>
|
<property name="top_padding">2</property>
|
||||||
<property name="bottom_padding">4</property>
|
<property name="bottom_padding">4</property>
|
||||||
<property name="left_padding">0</property>
|
|
||||||
<property name="right_padding">3</property>
|
<property name="right_padding">3</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="close_button">
|
<object class="GtkButton" id="close_button">
|
||||||
@@ -35,7 +34,7 @@
|
|||||||
<property name="can_default">True</property>
|
<property name="can_default">True</property>
|
||||||
<property name="receives_default">False</property>
|
<property name="receives_default">False</property>
|
||||||
<property name="use_stock">True</property>
|
<property name="use_stock">True</property>
|
||||||
<signal name="clicked" handler="on_cancel" swapped="no"/>
|
<signal name="clicked" handler="close_dialog" swapped="no"/>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
@@ -65,8 +64,10 @@
|
|||||||
<object class="GtkLabel" id="label">
|
<object class="GtkLabel" id="label">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="xalign">0</property>
|
<attributes>
|
||||||
<property name="use_markup">True</property>
|
<attribute name="weight" value="bold"/>
|
||||||
|
<attribute name="variant" value="normal"/>
|
||||||
|
</attributes>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
|||||||
Reference in New Issue
Block a user