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