New DataFormWidget: Add Captcha support
This commit is contained in:
		
							parent
							
								
									4be4998704
								
							
						
					
					
						commit
						7f1dfe5a8c
					
				
					 8 changed files with 190 additions and 8 deletions
				
			
		| 
						 | 
				
			
			@ -57,6 +57,7 @@ config = c_config.Config()
 | 
			
		|||
version = gajim.__version__
 | 
			
		||||
connections = {}  # type: Dict[str, ConnectionT]
 | 
			
		||||
avatar_cache = {}  # type: Dict[str, Dict[str, Any]]
 | 
			
		||||
bob_cache = {} # type: Dict[str, bytes]
 | 
			
		||||
ipython_window = None
 | 
			
		||||
app = None  # Gtk.Application
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -668,3 +669,14 @@ def get_win_debug_mode() -> bool:
 | 
			
		|||
    debug_folder = Path(configpaths.get('DEBUG'))
 | 
			
		||||
    debug_enabled = debug_folder / 'debug-enabled'
 | 
			
		||||
    return debug_enabled.exists()
 | 
			
		||||
 | 
			
		||||
def get_stored_bob_data(algo_hash: str) -> Optional[bytes]:
 | 
			
		||||
    try:
 | 
			
		||||
        return bob_cache[algo_hash]
 | 
			
		||||
    except KeyError:
 | 
			
		||||
        filepath = Path(configpaths.get('BOB')) / algo_hash
 | 
			
		||||
        if filepath.exists():
 | 
			
		||||
            with open(str(filepath), 'r+b') as file:
 | 
			
		||||
                data = file.read()
 | 
			
		||||
            return data
 | 
			
		||||
    return None
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -229,6 +229,7 @@ class ConfigPaths:
 | 
			
		|||
            # Cache paths
 | 
			
		||||
            ('CACHE_DB', 'cache.db', PathLocation.CACHE, PathType.FILE),
 | 
			
		||||
            ('AVATAR', 'avatars', PathLocation.CACHE, PathType.FOLDER),
 | 
			
		||||
            ('BOB', 'bob', PathLocation.CACHE, PathType.FOLDER),
 | 
			
		||||
 | 
			
		||||
            # Config paths
 | 
			
		||||
            ('MY_THEME', 'theme', PathLocation.CONFIG, PathType.FOLDER),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,9 +15,15 @@
 | 
			
		|||
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
import hashlib
 | 
			
		||||
from base64 import b64decode
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
import nbxmpp
 | 
			
		||||
 | 
			
		||||
from gajim.common import app
 | 
			
		||||
from gajim.common import configpaths
 | 
			
		||||
 | 
			
		||||
log = logging.getLogger('gajim.c.m.bob')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -91,5 +97,78 @@ class BitsOfBinary:
 | 
			
		|||
            iq, self._on_bob_received, {'cid': cid})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_bob_data(stanza):
 | 
			
		||||
    data_node = stanza.getTag('data', namespace=nbxmpp.NS_BOB)
 | 
			
		||||
    if data_node is None:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    cid = data_node.getAttr('cid')
 | 
			
		||||
    type_ = data_node.getAttr('type')
 | 
			
		||||
    max_age = data_node.getAttr('max-age')
 | 
			
		||||
    if max_age is not None:
 | 
			
		||||
        try:
 | 
			
		||||
            max_age = int(max_age)
 | 
			
		||||
        except Exception:
 | 
			
		||||
            log.exception(stanza)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
    if cid is None or type_ is None:
 | 
			
		||||
        log.warning('Invalid data node (no cid or type attr): %s', stanza)
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        algo_hash = cid.split('@')[0]
 | 
			
		||||
        algo, hash_ = algo_hash.split('+')
 | 
			
		||||
    except Exception:
 | 
			
		||||
        log.exception('Invalid cid: %s', stanza)
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    bob_data = data_node.getData()
 | 
			
		||||
    if not bob_data:
 | 
			
		||||
        log.warning('No data found: %s', stanza)
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    filepath = Path(configpaths.get('BOB')) / algo_hash
 | 
			
		||||
    if algo_hash in app.bob_cache or filepath.exists():
 | 
			
		||||
        log.info('BoB data already cached')
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        bob_data = b64decode(bob_data)
 | 
			
		||||
    except Exception:
 | 
			
		||||
        log.warning('Unable to decode data')
 | 
			
		||||
        log.exception(stanza)
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if len(bob_data) > 10000:
 | 
			
		||||
        log.warning('%s: data > 10000 bytes', stanza.getFrom())
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        sha = hashlib.new(algo)
 | 
			
		||||
    except ValueError as error:
 | 
			
		||||
        log.warning(stanza)
 | 
			
		||||
        log.warning(error)
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    sha.update(bob_data)
 | 
			
		||||
    if sha.hexdigest() != hash_:
 | 
			
		||||
        log.warning('Invalid hash: %s', stanza)
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if max_age == 0:
 | 
			
		||||
        app.bob_cache[algo_hash] = bob_data
 | 
			
		||||
    else:
 | 
			
		||||
        try:
 | 
			
		||||
            with open(str(filepath), 'w+b') as file:
 | 
			
		||||
                file.write(bob_data)
 | 
			
		||||
        except Exception:
 | 
			
		||||
            log.warning('Unable to save data')
 | 
			
		||||
            log.exception(stanza)
 | 
			
		||||
            return
 | 
			
		||||
    log.info('BoB data stored: %s', algo_hash)
 | 
			
		||||
    return filepath
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_instance(*args, **kwargs):
 | 
			
		||||
    return BitsOfBinary(*args, **kwargs), 'BitsOfBinary'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,6 +26,7 @@ from gajim.common.nec import NetworkIncomingEvent, NetworkEvent
 | 
			
		|||
from gajim.common.modules.security_labels import parse_securitylabel
 | 
			
		||||
from gajim.common.modules.user_nickname import parse_nickname
 | 
			
		||||
from gajim.common.modules.carbons import parse_carbon
 | 
			
		||||
from gajim.common.modules.bits_of_binary import parse_bob_data
 | 
			
		||||
from gajim.common.modules.misc import parse_delay
 | 
			
		||||
from gajim.common.modules.misc import parse_eme
 | 
			
		||||
from gajim.common.modules.misc import parse_correction
 | 
			
		||||
| 
						 | 
				
			
			@ -228,6 +229,8 @@ class Message:
 | 
			
		|||
            timestamp = time.time()
 | 
			
		||||
            delayed = False
 | 
			
		||||
 | 
			
		||||
        parse_bob_data(event.stanza)
 | 
			
		||||
 | 
			
		||||
        event_attr = {
 | 
			
		||||
            'popup': False,
 | 
			
		||||
            'msg_log_id': None,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@ import weakref
 | 
			
		|||
import nbxmpp
 | 
			
		||||
 | 
			
		||||
from gajim.common import app
 | 
			
		||||
from gajim.common.modules.bits_of_binary import parse_bob_data
 | 
			
		||||
 | 
			
		||||
log = logging.getLogger('gajim.c.m.register')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -125,6 +126,7 @@ class Register:
 | 
			
		|||
                error_cb()(error)
 | 
			
		||||
        else:
 | 
			
		||||
            log.info('Register form received')
 | 
			
		||||
            parse_bob_data(stanza.getQuery())
 | 
			
		||||
            form = stanza.getQuery().getTag('x', namespace=nbxmpp.NS_DATA)
 | 
			
		||||
            is_form = form is not None
 | 
			
		||||
            if not is_form:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -250,3 +250,6 @@ button.flat.link { padding: 0; border: 0; }
 | 
			
		|||
.data-form-widget grid { margin: 18px; }
 | 
			
		||||
.data-form-widget treeview { padding: 5px; }
 | 
			
		||||
.data-form-widget scrolledwindow { border: 1px solid; border-color:@unfocused_borders; }
 | 
			
		||||
 | 
			
		||||
/*Image Preview*/
 | 
			
		||||
.preview-image { box-shadow: 0px 0px 3px 0px alpha(@theme_text_color, 0.2); }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,9 @@ from gi.repository import GLib
 | 
			
		|||
from gi.repository import GObject
 | 
			
		||||
from gi.repository import Pango
 | 
			
		||||
 | 
			
		||||
from gajim.gtkgui_helpers import scale_pixbuf_from_data
 | 
			
		||||
 | 
			
		||||
from gajim.common import app
 | 
			
		||||
from gajim.common.i18n import _
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -82,21 +85,21 @@ class FormGrid(Gtk.Grid):
 | 
			
		|||
 | 
			
		||||
        if form_node.title is not None:
 | 
			
		||||
            self.title = form_node.title
 | 
			
		||||
            self.add_row(Title(form_node.title))
 | 
			
		||||
            self._add_row(Title(form_node.title))
 | 
			
		||||
        if form_node.instructions is not None:
 | 
			
		||||
            self.instructions = form_node.instructions
 | 
			
		||||
            self.add_row(Instructions(form_node.instructions))
 | 
			
		||||
            self._add_row(Instructions(form_node.instructions))
 | 
			
		||||
 | 
			
		||||
        self.analyse_fields(form_node, options)
 | 
			
		||||
        self.parse_form(form_node, options)
 | 
			
		||||
        self._analyse_fields(form_node, options)
 | 
			
		||||
        self._parse_form(form_node, options)
 | 
			
		||||
 | 
			
		||||
    def add_row(self, field):
 | 
			
		||||
    def _add_row(self, field):
 | 
			
		||||
        field.add(self, self.row_count)
 | 
			
		||||
        self.row_count += 1
 | 
			
		||||
        self.rows.append(field)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def analyse_fields(form_node, options):
 | 
			
		||||
    def _analyse_fields(form_node, options):
 | 
			
		||||
        if 'right_align' in options:
 | 
			
		||||
            # Dont overwrite option
 | 
			
		||||
            return
 | 
			
		||||
| 
						 | 
				
			
			@ -113,13 +116,33 @@ class FormGrid(Gtk.Grid):
 | 
			
		|||
 | 
			
		||||
        options['right_align'] = max(label_lengths) < 30
 | 
			
		||||
 | 
			
		||||
    def parse_form(self, form_node, options):
 | 
			
		||||
    def _parse_form(self, form_node, options):
 | 
			
		||||
        for field in form_node.iter_fields():
 | 
			
		||||
            if field.type_ == 'hidden':
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            if field.media:
 | 
			
		||||
                if not self._add_media_field(field, options):
 | 
			
		||||
                    # We dont understand this media element, ignore it
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
            widget = self._fields[field.type_]
 | 
			
		||||
            self.add_row(widget(field, self, options))
 | 
			
		||||
            self._add_row(widget(field, self, options))
 | 
			
		||||
 | 
			
		||||
    def _add_media_field(self, field, options):
 | 
			
		||||
        if not field.type_ in ('text-single', 'text-private', 'text-multi'):
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        for uri in field.media.uris:
 | 
			
		||||
            if not uri.type_.startswith('image/'):
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            if not uri.uri_data.startswith('cid'):
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            self._add_row(ImageMediaField(uri, self, options))
 | 
			
		||||
            return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def validate(self, is_valid):
 | 
			
		||||
        value = self._data_form.is_valid() if is_valid else False
 | 
			
		||||
| 
						 | 
				
			
			@ -460,3 +483,23 @@ class TextMultiField(Field):
 | 
			
		|||
    def _changed(self, widget):
 | 
			
		||||
        self._field.value = widget.get_text(*widget.get_bounds(), False)
 | 
			
		||||
        self._validate()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ImageMediaField():
 | 
			
		||||
    def __init__(self, uri, form_grid, options):
 | 
			
		||||
        self._uri = uri
 | 
			
		||||
        self._form_grid = form_grid
 | 
			
		||||
 | 
			
		||||
        filename = uri.uri_data.split(':')[1].split('@')[0]
 | 
			
		||||
        data = app.bob_cache.get(filename)
 | 
			
		||||
        if data is None:
 | 
			
		||||
            self._image = Gtk.Image()
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        pixbuf = scale_pixbuf_from_data(data, 170)
 | 
			
		||||
        self._image = Gtk.Image.new_from_pixbuf(pixbuf)
 | 
			
		||||
        self._image.set_halign(Gtk.Align.CENTER)
 | 
			
		||||
        self._image.get_style_context().add_class('preview-image')
 | 
			
		||||
 | 
			
		||||
    def add(self, form_grid, row_number):
 | 
			
		||||
        form_grid.attach(self._image, 1, row_number, 1, 1)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,45 @@
 | 
			
		|||
from base64 import b64decode
 | 
			
		||||
 | 
			
		||||
from gi.repository import Gtk
 | 
			
		||||
import nbxmpp
 | 
			
		||||
 | 
			
		||||
from gajim.gtk.dataform import DataFormWidget
 | 
			
		||||
from gajim.common.modules.dataforms import extend_form
 | 
			
		||||
from gajim.common.const import CSSPriority
 | 
			
		||||
from gajim.common import app
 | 
			
		||||
 | 
			
		||||
from test.gtk import util
 | 
			
		||||
util.load_style('gajim.css', CSSPriority.APPLICATION)
 | 
			
		||||
 | 
			
		||||
image = '''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'''
 | 
			
		||||
 | 
			
		||||
app.bob_cache['sha1+8f35fef110ffc5df08d579a50083ff9308fb6242'] = b64decode(image)
 | 
			
		||||
 | 
			
		||||
FORM = '''
 | 
			
		||||
<x xmlns='jabber:x:data' type='form'>
 | 
			
		||||
  <title>Bot Configuration</title>
 | 
			
		||||
| 
						 | 
				
			
			@ -66,9 +98,16 @@ FORM = '''
 | 
			
		|||
    <desc>Tell all your friends about your new bot!</desc>
 | 
			
		||||
    <required/>
 | 
			
		||||
  </field>
 | 
			
		||||
  <field var='ocr' type='text-single' label='Fill in what you see'>
 | 
			
		||||
    <media xmlns='urn:xmpp:media-element'>
 | 
			
		||||
      <uri type='image/png'>cid:sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org</uri>
 | 
			
		||||
    </media>
 | 
			
		||||
    <required/>
 | 
			
		||||
  </field>
 | 
			
		||||
</x>
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DataFormWindow(Gtk.Window):
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        Gtk.Window.__init__(self, title="Data Form Test")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue