1# Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
2# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
3# Copyright (C) 2007 Stephan Erb <steve-e AT h3c.de>
4# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
5#
6# This file is part of nbxmpp.
7#
8# nbxmpp is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published
10# by the Free Software Foundation; version 3 only.
11#
12# nbxmpp is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with nbxmpp. If not, see <http://www.gnu.org/licenses/>.
19
20# XEP-0004: Data Forms
21
22from nbxmpp.namespaces import Namespace
23from nbxmpp.protocol import JID
24from nbxmpp.simplexml import Node
25
26
27# exceptions used in this module
28class Error(Exception):
29    pass
30
31
32# when we get nbxmpp.Node which we do not understand
33class UnknownDataForm(Error):
34    pass
35
36
37# when we get nbxmpp.Node which contains bad fields
38class WrongFieldValue(Error):
39    pass
40
41
42# helper class to change class of already existing object
43class ExtendedNode(Node):
44    @classmethod
45    def __new__(cls, *args, **kwargs):
46        if 'extend' not in kwargs.keys() or not kwargs['extend']:
47            return object.__new__(cls)
48
49        extend = kwargs['extend']
50        assert issubclass(cls, extend.__class__)
51        extend.__class__ = cls
52        return extend
53
54
55# helper to create fields from scratch
56def create_field(typ, **attrs):
57    ''' Helper function to create a field of given type. '''
58    field = {
59        'boolean': BooleanField,
60        'fixed': StringField,
61        'hidden': StringField,
62        'text-private': StringField,
63        'text-single': StringField,
64        'jid-multi': JidMultiField,
65        'jid-single': JidSingleField,
66        'list-multi': ListMultiField,
67        'list-single': ListSingleField,
68        'text-multi': TextMultiField,
69    }[typ](typ=typ, **attrs)
70    return field
71
72
73def extend_field(node):
74    """
75    Helper function to extend a node to field of appropriate type
76    """
77    # when validation (XEP-122) will go in, we could have another classes
78    # like DateTimeField - so that dicts in create_field() and
79    # extend_field() will be different...
80    typ = node.getAttr('type')
81    field = {
82        'boolean': BooleanField,
83        'fixed': StringField,
84        'hidden': StringField,
85        'text-private': StringField,
86        'text-single': StringField,
87        'jid-multi': JidMultiField,
88        'jid-single': JidSingleField,
89        'list-multi': ListMultiField,
90        'list-single': ListSingleField,
91        'text-multi': TextMultiField,
92    }
93    if typ not in field:
94        typ = 'text-single'
95    return field[typ](extend=node)
96
97
98def extend_form(node):
99    """
100    Helper function to extend a node to form of appropriate type
101    """
102    if node.getTag('reported') is not None:
103        return MultipleDataForm(extend=node)
104    return SimpleDataForm(extend=node)
105
106
107class DataField(ExtendedNode):
108    """
109    Keeps data about one field - var, field type, labels, instructions... Base
110    class for different kinds of fields. Use create_field() function to
111    construct one of these
112    """
113
114    def __init__(self, typ=None, var=None, value=None, label=None, desc=None,
115                 required=False, options=None, extend=None):
116
117        if extend is None:
118            ExtendedNode.__init__(self, 'field')
119
120            self.type_ = typ
121            self.var = var
122            if value is not None:
123                self.value = value
124            if label is not None:
125                self.label = label
126            if desc is not None:
127                self.desc = desc
128            self.required = required
129            self.options = options
130
131    @property
132    def type_(self):
133        """
134        Type of field. Recognized values are: 'boolean', 'fixed', 'hidden',
135        'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi',
136        'text-private', 'text-single'. If you set this to something different,
137        DataField will store given name, but treat all data as text-single
138        """
139        type_ = self.getAttr('type')
140        if type_ is None:
141            return 'text-single'
142        return type_
143
144    @type_.setter
145    def type_(self, value):
146        assert isinstance(value, str)
147        self.setAttr('type', value)
148
149    @property
150    def var(self):
151        """
152        Field identifier
153        """
154        return self.getAttr('var')
155
156    @var.setter
157    def var(self, value):
158        assert isinstance(value, str)
159        self.setAttr('var', value)
160
161    @var.deleter
162    def var(self):
163        self.delAttr('var')
164
165    @property
166    def label(self):
167        """
168        Human-readable field name
169        """
170        label_ = self.getAttr('label')
171        if not label_:
172            label_ = self.var
173        return label_
174
175    @label.setter
176    def label(self, value):
177        assert isinstance(value, str)
178        self.setAttr('label', value)
179
180    @label.deleter
181    def label(self):
182        if self.getAttr('label'):
183            self.delAttr('label')
184
185    @property
186    def description(self):
187        """
188        Human-readable description of field meaning
189        """
190        return self.getTagData('desc') or ''
191
192    @description.setter
193    def description(self, value):
194        assert isinstance(value, str)
195        if value == '':
196            del self.description
197        else:
198            self.setTagData('desc', value)
199
200    @description.deleter
201    def description(self):
202        desc = self.getTag('desc')
203        if desc is not None:
204            self.delChild(desc)
205
206    @property
207    def required(self):
208        """
209        Controls whether this field required to fill. Boolean
210        """
211        return bool(self.getTag('required'))
212
213    @required.setter
214    def required(self, value):
215        required = self.getTag('required')
216        if required and not value:
217            self.delChild(required)
218        elif not required and value:
219            self.addChild('required')
220
221    @property
222    def media(self):
223        """
224        Media data
225        """
226        media = self.getTag('media', namespace=Namespace.DATA_MEDIA)
227        if media:
228            return Media(media)
229        return None
230
231    @media.setter
232    def media(self, value):
233        del self.media
234        self.addChild(node=value)
235
236    @media.deleter
237    def media(self):
238        media = self.getTag('media')
239        if media is not None:
240            self.delChild(media)
241
242    @staticmethod
243    def is_valid():
244        return True, ''
245
246
247class Uri(Node):
248    def __init__(self, uri_tag):
249        Node.__init__(self, node=uri_tag)
250
251    @property
252    def type_(self):
253        """
254        uri type
255        """
256        return self.getAttr('type')
257
258    @type_.setter
259    def type_(self, value):
260        self.setAttr('type', value)
261
262    @type_.deleter
263    def type_(self):
264        self.delAttr('type')
265
266    @property
267    def uri_data(self):
268        """
269        uri data
270        """
271        return self.getData()
272
273    @uri_data.setter
274    def uri_data(self, value):
275        self.setData(value)
276
277    @uri_data.deleter
278    def uri_data(self):
279        self.setData(None)
280
281
282class Media(Node):
283    def __init__(self, media_tag):
284        Node.__init__(self, node=media_tag)
285
286    @property
287    def uris(self):
288        """
289        URIs of the media element.
290        """
291        return map(Uri, self.getTags('uri'))
292
293    @uris.setter
294    def uris(self, value):
295        del self.uris
296        for uri in value:
297            self.addChild(node=uri)
298
299    @uris.deleter
300    def uris(self):
301        for element in self.getTags('uri'):
302            self.delChild(element)
303
304
305class BooleanField(DataField):
306    @property
307    def value(self):
308        """
309        Value of field. May contain True, False or None
310        """
311        value = self.getTagData('value')
312        if value in ('0', 'false'):
313            return False
314        if value in ('1', 'true'):
315            return True
316        if value is None:
317            return False  # default value is False
318        raise WrongFieldValue
319
320    @value.setter
321    def value(self, value):
322        self.setTagData('value', value and '1' or '0')
323
324    @value.deleter
325    def value(self):
326        value = self.getTag('value')
327        if value is not None:
328            self.delChild(value)
329
330
331class StringField(DataField):
332    """
333    Covers fields of types: fixed, hidden, text-private, text-single
334    """
335
336    @property
337    def value(self):
338        """
339        Value of field. May be any string
340        """
341        return self.getTagData('value') or ''
342
343    @value.setter
344    def value(self, value):
345        if value is None:
346            value = ''
347        self.setTagData('value', value)
348
349    @value.deleter
350    def value(self):
351        try:
352            self.delChild(self.getTag('value'))
353        except ValueError:  # if there already were no value tag
354            pass
355
356    def is_valid(self):
357        if not self.required:
358            return True, ''
359        if not self.value:
360            return False, ''
361        return True, ''
362
363
364class ListField(DataField):
365    """
366    Covers fields of types: jid-multi, jid-single, list-multi, list-single
367    """
368
369    @property
370    def options(self):
371        """
372        Options
373        """
374        options = []
375        for element in self.getTags('option'):
376            value = element.getTagData('value')
377            if value is None:
378                raise WrongFieldValue
379            label = element.getAttr('label')
380            if not label:
381                label = value
382            options.append((label, value))
383        return options
384
385    @options.setter
386    def options(self, values):
387        del self.options
388        for value, label in values:
389            self.addChild('option',
390                          {'label': label}).setTagData('value', value)
391
392    @options.deleter
393    def options(self):
394        for element in self.getTags('option'):
395            self.delChild(element)
396
397    def iter_options(self):
398        for element in self.iterTags('option'):
399            value = element.getTagData('value')
400            if value is None:
401                raise WrongFieldValue
402            label = element.getAttr('label')
403            if not label:
404                label = value
405            yield (value, label)
406
407
408class ListSingleField(ListField, StringField):
409    """
410    Covers list-single field
411    """
412    def is_valid(self):
413        if not self.required:
414            return True, ''
415        if not self.value:
416            return False, ''
417        return True, ''
418
419
420class JidSingleField(ListSingleField):
421    """
422    Covers jid-single fields
423    """
424    def is_valid(self):
425        if self.value:
426            try:
427                JID.from_string(self.value)
428                return True, ''
429            except Exception as error:
430                return False, error
431        if self.required:
432            return False, ''
433        return True, ''
434
435
436class ListMultiField(ListField):
437    """
438    Covers list-multi fields
439    """
440
441    @property
442    def values(self):
443        """
444        Values held in field
445        """
446        values = []
447        for element in self.getTags('value'):
448            values.append(element.getData())
449        return values
450
451    @values.setter
452    def values(self, values):
453        del self.values
454        for value in values:
455            self.addChild('value').setData(value)
456
457    @values.deleter
458    def values(self):
459        for element in self.getTags('value'):
460            self.delChild(element)
461
462    def iter_values(self):
463        for element in self.getTags('value'):
464            yield element.getData()
465
466    def is_valid(self):
467        if not self.required:
468            return True, ''
469        if not self.values:
470            return False, ''
471        return True, ''
472
473
474class JidMultiField(ListMultiField):
475    """
476    Covers jid-multi fields
477    """
478    def is_valid(self):
479        if self.values:
480            for value in self.values:
481                try:
482                    JID.from_string(value)
483                except Exception as error:
484                    return False, error
485            return True, ''
486        if self.required:
487            return False, ''
488        return True, ''
489
490
491class TextMultiField(DataField):
492    @property
493    def value(self):
494        """
495        Value held in field
496        """
497        value = ''
498        for element in self.iterTags('value'):
499            value += '\n' + element.getData()
500        return value[1:]
501
502    @value.setter
503    def value(self, value):
504        del self.value
505        if value == '':
506            return
507        for line in value.split('\n'):
508            self.addChild('value').setData(line)
509
510    @value.deleter
511    def value(self):
512        for element in self.getTags('value'):
513            self.delChild(element)
514
515    def is_valid(self):
516        if not self.required:
517            return True, ''
518        if not self.value:
519            return False, ''
520        return True, ''
521
522
523class DataRecord(ExtendedNode):
524    """
525    The container for data fields - an xml element which has DataField elements
526    as children
527    """
528    def __init__(self, fields=None, associated=None, extend=None):
529        self.associated = associated
530        self.vars = {}
531        if extend is None:
532            # we have to build this object from scratch
533            Node.__init__(self)
534
535            if fields is not None:
536                self.fields = fields
537        else:
538            # we already have nbxmpp.Node inside - try to convert all
539            # fields into DataField objects
540            if fields is None:
541                for field in self.iterTags('field'):
542                    if not isinstance(field, DataField):
543                        extend_field(field)
544                    self.vars[field.var] = field
545            else:
546                self.fields = fields
547
548    @property
549    def fields(self):
550        """
551        List of fields in this record
552        """
553        return self.getTags('field')
554
555    @fields.setter
556    def fields(self, fields):
557        del self.fields
558        for field in fields:
559            if not isinstance(field, DataField):
560                extend_field(field)
561            self.addChild(node=field)
562            self.vars[field.var] = field
563
564    @fields.deleter
565    def fields(self):
566        for element in self.getTags('field'):
567            self.delChild(element)
568            self.vars.clear()
569
570    def iter_fields(self):
571        """
572        Iterate over fields in this record. Do not take associated into account
573        """
574        for field in self.iterTags('field'):
575            yield field
576
577    def iter_with_associated(self):
578        """
579        Iterate over associated, yielding both our field and associated one
580        together
581        """
582        for field in self.associated.iter_fields():
583            yield self[field.var], field
584
585    def __getitem__(self, item):
586        return self.vars[item]
587
588    def is_valid(self):
589        for field in self.iter_fields():
590            if not field.is_valid()[0]:
591                return False
592        return True
593
594    def is_fake_form(self):
595        return bool(self.vars.get('fakeform', False))
596
597
598class DataForm(ExtendedNode):
599    def __init__(self, type_=None, title=None, instructions=None, extend=None):
600        if extend is None:
601            # we have to build form from scratch
602            Node.__init__(self, 'x', attrs={'xmlns': Namespace.DATA})
603
604        if type_ is not None:
605            self.type_ = type_
606        if title is not None:
607            self.title = title
608        if instructions is not None:
609            self.instructions = instructions
610
611    @property
612    def type_(self):
613        """
614        Type of the form. Must be one of: 'form', 'submit', 'cancel', 'result'.
615        'form' - this form is to be filled in; you will be able soon to do:
616        filledform = DataForm(replyto=thisform)
617        """
618        return self.getAttr('type')
619
620    @type_.setter
621    def type_(self, type_):
622        assert type_ in ('form', 'submit', 'cancel', 'result')
623        self.setAttr('type', type_)
624
625    @property
626    def title(self):
627        """
628        Title of the form
629
630        Human-readable, should not contain any \\r\\n.
631        """
632        return self.getTagData('title')
633
634    @title.setter
635    def title(self, title):
636        self.setTagData('title', title)
637
638    @title.deleter
639    def title(self):
640        try:
641            self.delChild('title')
642        except ValueError:
643            pass
644
645    @property
646    def instructions(self):
647        """
648        Instructions for this form
649
650        Human-readable, may contain \\r\\n.
651        """
652        # TODO: the same code is in TextMultiField. join them
653        value = ''
654        for valuenode in self.getTags('instructions'):
655            value += '\n' + valuenode.getData()
656        return value[1:]
657
658    @instructions.setter
659    def instructions(self, value):
660        del self.instructions
661        if value == '':
662            return
663        for line in value.split('\n'):
664            self.addChild('instructions').setData(line)
665
666    @instructions.deleter
667    def instructions(self):
668        for value in self.getTags('instructions'):
669            self.delChild(value)
670
671    @property
672    def is_reported(self):
673        return self.getTag('reported') is not None
674
675
676class SimpleDataForm(DataForm, DataRecord):
677    def __init__(self, type_=None, title=None, instructions=None, fields=None,
678                 extend=None):
679        DataForm.__init__(self, type_=type_, title=title,
680                          instructions=instructions, extend=extend)
681        DataRecord.__init__(self, fields=fields, extend=self, associated=self)
682
683    def get_purged(self):
684        simple_form = SimpleDataForm(extend=self)
685        del simple_form.title
686        simple_form.instructions = ''
687        to_be_removed = []
688        for field in simple_form.iter_fields():
689            if field.required:
690                # add <value> if there is not
691                if hasattr(field, 'value') and not field.value:
692                    field.value = ''
693                # Keep all required fields
694                continue
695            if ((hasattr(field, 'value') and
696                 not field.value and
697                 field.value != 0) or
698                    (hasattr(field, 'values') and not field.values)):
699                to_be_removed.append(field)
700            else:
701                del field.label
702                del field.description
703                del field.media
704        for field in to_be_removed:
705            simple_form.delChild(field)
706        return simple_form
707
708
709class MultipleDataForm(DataForm):
710    def __init__(self, type_=None, title=None, instructions=None, items=None,
711                 extend=None):
712        DataForm.__init__(self, type_=type_, title=title,
713                          instructions=instructions, extend=extend)
714        # all records, recorded into DataRecords
715        if extend is None:
716            if items is not None:
717                self.items = items
718        else:
719            # we already have nbxmpp.Node inside - try to convert all
720            # fields into DataField objects
721            if items is None:
722                self.items = list(self.iterTags('item'))
723            else:
724                for item in self.getTags('item'):
725                    self.delChild(item)
726                self.items = items
727        reported_tag = self.getTag('reported')
728        self.reported = DataRecord(extend=reported_tag)
729
730    @property
731    def items(self):
732        """
733        A list of all records
734        """
735        return list(self.iter_records())
736
737    @items.setter
738    def items(self, records):
739        del self.items
740        for record in records:
741            if not isinstance(record, DataRecord):
742                DataRecord(extend=record)
743            self.addChild(node=record)
744
745    @items.deleter
746    def items(self):
747        for record in self.getTags('item'):
748            self.delChild(record)
749
750    def iter_records(self):
751        for record in self.getTags('item'):
752            yield record
753