1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - MoinForms library 4 5 @copyright: 2012, 2013 by Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 """ 8 9 from compiler import parse 10 from compiler.ast import Const, Dict, Discard, List, Module, Stmt 11 from MoinMoin.action import do_show 12 from MoinMoin.Page import Page 13 from MoinMoin import security, wikiutil 14 from MoinSupport import * 15 import re 16 17 __version__ = "0.1" 18 19 form_field_regexp_str = r"<<Form(Field|Message)\((.*?)\)>>" 20 form_field_regexp = re.compile(form_field_regexp_str, re.DOTALL) 21 22 # Common action functionality. 23 24 class MoinFormHandlerAction: 25 26 "A handler action that can be specialised for individual forms." 27 28 def __init__(self, pagename, request): 29 self.pagename = pagename 30 self.request = request 31 self.access_handler = None 32 self.attributes = None 33 34 def getAccessHandler(self): 35 36 """ 37 Return an access handler for the form whose attributes have been 38 obtained and stored in this instance. 39 """ 40 41 return FormAccess(self.pagename, self.request, self.attributes) 42 43 def processForm(self): 44 45 """ 46 Interpret the request details and modify them according to the structure 47 of the interpreted information. 48 """ 49 50 _ = self.request.getText 51 52 # Get the form fields and obtain the hierarchical field structure. 53 54 form = get_form(self.request) 55 fields = getFields(form, remove=True) 56 57 # Detect any request to load data. 58 59 if fields.has_key("load"): 60 try: 61 number = int(fields["load"][0]) 62 except ValueError: 63 fields = {} 64 else: 65 self.attributes, text = self.getFormForFragment(fields) 66 self.access_handler = self.getAccessHandler() 67 fields = self.loadFields(number) 68 69 self.unfinished(fields, form) 70 71 # Otherwise, process any supplied data. 72 73 else: 74 # Modify and validate the form. 75 76 self.modifyFields(fields) 77 78 # Get the form definition. 79 80 self.attributes, text = self.getFormForFragment(fields) 81 self.access_handler = self.getAccessHandler() 82 structure = getFormStructure(text, self.request) 83 84 # Check the permissions on the form. 85 86 if not self.checkPermissions("write"): 87 self.request.theme.add_msg(_("You do not appear to have access to this form."), "error") 88 do_show(self.pagename, self.request) 89 return 90 91 # Without any form definition, the page is probably the wrong one. 92 93 if not structure: 94 self.request.theme.add_msg(_("This page does not provide a form."), "error") 95 do_show(self.pagename, self.request) 96 return 97 98 # With a form definition, attempt to validate the fields. 99 100 if self.validateFields(fields, structure): 101 if self.shouldFinish(fields): 102 self.finished(fields, form) 103 return 104 105 self.unfinished(fields, form) 106 107 def finished(self, fields, form): 108 109 "Handle the finished 'fields' and 'form'." 110 111 self.storeFields(fields) 112 self.unfinished(fields, form) 113 114 def unfinished(self, fields, form): 115 116 "Handle the unfinished 'fields' and 'form'." 117 118 # Serialise and show the form. 119 120 self.serialiseFields(fields, form) 121 do_show(self.pagename, self.request) 122 123 def shouldFinish(self, fields): 124 125 """ 126 Subject to the attributes stored for the form in this instance, return 127 whether any field referenced by the "finishing" attribute is present 128 and thus indicate whether the form handling should finish. 129 """ 130 131 finishing = self.attributes.has_key("finishing") and self.attributes["finishing"].split(",") 132 133 if finishing: 134 for name in finishing: 135 if fields.has_key(name): 136 return True 137 138 return False 139 140 def getFormForFragment(self, fields): 141 142 "Return the attributes and text of the form being handled." 143 144 fragment = fields.get("fragment", [None])[0] 145 text = Page(self.request, self.pagename).get_raw_body() 146 return getFormForFragment(text, fragment) 147 148 def checkPermissions(self, action): 149 150 """ 151 Check the permissions of the user against any restrictions specified in 152 the form's 'attributes'. 153 """ 154 155 return self.access_handler.checkPermissions(action) 156 157 def validateFields(self, fields, structure): 158 159 """ 160 Validate the given 'fields' using the given form 'structure', 161 introducing error fields where the individual fields do not conform to 162 their descriptions. 163 """ 164 165 return self.validateFieldsUsingStructure(fields, structure) 166 167 def validateFieldsUsingStructure(self, fields, structure): 168 169 "Validate the given 'fields' using the given 'structure'." 170 171 _ = self.request.getText 172 valid = True 173 174 for key, definition in structure.items(): 175 value = fields.get(key) 176 177 # Enter form sections and validate them. 178 179 if isinstance(definition, dict): 180 if value: 181 for element in getSectionElements(value): 182 valid = self.validateFieldsUsingStructure(element, structure[key]) and valid 183 184 # Validate individual fields. 185 186 elif structure.has_key(key): 187 path, dictpage, label, section, field_args, allowed_values = definition 188 errors = [] 189 190 # Test for obligatory values. 191 192 if not value or not value[0]: 193 if field_args.get("required"): 194 195 # Detect new parts of the structure and avoid producing 196 # premature error messages. 197 198 if not fields.has_key("_new"): 199 errors.append(_("This field must be filled out.")) 200 else: 201 valid = False 202 else: 203 # Test for unacceptable values. 204 205 if allowed_values and set(value).difference(allowed_values): 206 errors.append(_("At least one of the choices is not acceptable.")) 207 208 # Test the number of values. 209 210 if field_args.get("type") == "select": 211 if field_args.has_key("maxselected"): 212 if len(value) > int(field_args["maxselected"]): 213 errors.append(_("Incorrect number of choices given: need %s.") % field_args["maxselected"]) 214 215 if errors: 216 fields["%s-error" % key] = errors 217 valid = False 218 219 return valid 220 221 def serialiseFields(self, fields, form, path=None): 222 223 """ 224 Serialise the given 'fields' to the given 'form', using the given 'path' 225 to name the entries. 226 """ 227 228 for key, value in fields.items(): 229 230 # Serialise sections. 231 232 if isinstance(value, dict): 233 for index, element in enumerate(getSectionElements(value)): 234 element_ref = "%s$%s" % (key, index) 235 236 self.serialiseFields(element, form, 237 path and ("%s/%s" % (path, element_ref)) or element_ref 238 ) 239 240 # Serialise fields. 241 242 else: 243 form[path and ("%s/%s" % (path, key)) or key] = value 244 245 def modifyFields(self, fields): 246 247 "Modify the given 'fields', removing and adding items." 248 249 # First, remove fields. 250 251 for key in fields.keys(): 252 if key.startswith("_remove="): 253 self.removeField(key[8:], fields) 254 255 # Then, add fields. 256 257 for key in fields.keys(): 258 if key.startswith("_add="): 259 self.addField(key[5:], fields) 260 261 def removeField(self, path, fields): 262 263 """ 264 Remove the section element indicated by the given 'path' from the 265 'fields'. 266 """ 267 268 section, (name, index) = getSectionForPath(path, fields) 269 try: 270 del section[name][index] 271 except KeyError: 272 pass 273 274 def addField(self, path, fields): 275 276 """ 277 Add a section element indicated by the given 'path' to the 'fields'. 278 """ 279 280 section, (name, index) = getSectionForPath(path, fields) 281 placeholder = {"_new" : ""} 282 283 if section.has_key(name): 284 indexes = section[name].keys() 285 max_index = max(map(int, indexes)) 286 section[name][max_index + 1] = placeholder 287 else: 288 max_index = -1 289 section[name] = {0 : placeholder} 290 291 # Storage of form submissions. 292 293 def storeFields(self, fields): 294 295 "Store the given 'fields' as a Python object representation." 296 297 store = FormStore(self.access_handler) 298 store.append(repr(fields)) 299 300 def loadFields(self, number): 301 302 "Load the fields associated with the given submission 'number'." 303 304 store = FormStore(self.access_handler) 305 return loadFields(store, number) 306 307 def loadFields(store, number): 308 309 """ 310 Load the fields from the 'store' that are associated with the given 311 submission 'number'. 312 """ 313 314 try: 315 text = store[number] 316 module = parse(text) 317 if checkStoredFormData(module): 318 return eval(text) 319 320 # NOTE: Should indicate any errors in retrieving form data. 321 322 except: 323 pass 324 325 return {} 326 327 def checkStoredFormData(node): 328 329 """ 330 Check the syntax 'node' and its descendants for suitability as parts of 331 a field definition. 332 """ 333 334 for child in node.getChildNodes(): 335 if isinstance(child, Const): 336 pass 337 elif not isinstance(child, (Dict, Discard, List, Module, Stmt)) or not checkStoredFormData(child): 338 return False 339 340 return True 341 342 class FormAccess: 343 344 "A means of checking access to form data." 345 346 def __init__(self, pagename, request, attributes): 347 self.pagename = pagename 348 self.request = request 349 self.attributes = attributes 350 351 def checkPermissions(self, action): 352 353 """ 354 Check the permissions of the user against any restrictions specified in 355 the form's 'attributes'. 356 """ 357 358 user = self.request.user 359 360 # Use the access definition if one is given. 361 362 if self.attributes.has_key("access"): 363 access = self.attributes["access"] 364 acl = security.AccessControlList(self.request.cfg, [access]) 365 policy = lambda request, pagename, username, action: acl.may(request, username, action) 366 367 # Otherwise, use the page permissions. 368 369 else: 370 policy = security._check 371 372 # The "read" action is only satisfied by the "admin" role. 373 374 return user and ( 375 action != "read" and policy(self.request, self.pagename, user.name, action) or 376 action == "read" and policy(self.request, self.pagename, user.name, "admin") 377 ) 378 379 class FormStore(ItemStore): 380 381 "A form-specific storage mechanism." 382 383 def __init__(self, handler): 384 385 "Initialise the store with the form 'handler'." 386 387 self.handler = handler 388 page = Page(handler.request, handler.pagename) 389 ItemStore.__init__(self, page, "forms", "form-locks") 390 391 def can_write(self): 392 393 """ 394 Permit writing of form data using the form attributes or page 395 permissions. 396 """ 397 398 return self.handler.checkPermissions("write") 399 400 def can_read(self): 401 402 """ 403 Permit reading of form data using the form attributes or page 404 permissions. 405 """ 406 407 return self.handler.checkPermissions("read") 408 409 # Form and field information. 410 411 def getFormStructure(text, request, path=None, structure=None): 412 413 """ 414 For the given form 'text' and using the 'request', return details of the 415 form for the section at the given 'path' (or the entire form if 'path' is 416 omitted), populating the given 'structure' (or populating a new structure if 417 'structure' is omitted). 418 """ 419 420 if structure is None: 421 structure = {} 422 423 for format, attributes, body in getFragments(text, True): 424 425 # Get field details at the current level. 426 427 if format is None: 428 structure.update(getFormFields(body, path, request)) 429 430 # Where a section is found, get details from within the section. 431 432 elif format == "form": 433 if attributes.has_key("section"): 434 section_name = attributes["section"] 435 section = structure[section_name] = {} 436 getFormStructure(body, request, path and ("%s/%s" % (path, section_name)) or section_name, section) 437 elif attributes.has_key("message"): 438 getFormStructure(body, request, path, structure) 439 elif attributes.has_key("not-message"): 440 getFormStructure(body, request, path, structure) 441 442 # Get field details from other kinds of region. 443 444 elif format != "form": 445 getFormStructure(body, request, path, structure) 446 447 return structure 448 449 def getFormForFragment(text, fragment=None): 450 451 """ 452 Return the form region from the given 'text' for the specified 'fragment'. 453 If no fragment is specified, the first form region is returned. The form 454 region is described using a tuple containing the attributes for the form 455 and the body text of the form. 456 """ 457 458 for format, attributes, body in getFragments(text): 459 if format == "form" and (not fragment or attributes.get("fragment") == fragment): 460 return attributes, body 461 462 return {}, None 463 464 def getFieldArguments(field_definition): 465 466 "Return the parsed arguments from the given 'field_definition' string." 467 468 field_args = {} 469 470 for field_arg in field_definition.split(): 471 if field_arg == "required": 472 field_args[field_arg] = True 473 continue 474 475 # Record the key-value details. 476 477 try: 478 argname, argvalue = field_arg.split("=", 1) 479 field_args[argname] = argvalue 480 481 # Single keywords are interpreted as type descriptions. 482 483 except ValueError: 484 if not field_args.has_key("type"): 485 field_args["type"] = field_arg 486 487 return field_args 488 489 # Common formatting functions. 490 491 def getFormOutput(text, fields, form_fragment=None, path=None, fragment=None, repeating=None, index=None): 492 493 """ 494 Combine regions found in the given 'text' and then return them as a single 495 block. The reason for doing this, as opposed to just passing each region to 496 a suitable parser for formatting, is that form sections may break up 497 regions, and such sections may not define separate subregions but instead 498 act as a means of conditional inclusion of text into an outer region. 499 500 The given 'fields' are used to populate fields provided in forms and to 501 control whether sections are populated or not. 502 503 The optional 'form_fragment' is used to indicate the form to which the 504 fields belong. 505 506 The optional 'path' is used to adjust form fields to refer to the correct 507 part of the form hierarchy. 508 509 The optional 'fragment' is used to indicate the form being output. If this 510 value is different to 'form_fragment', the structure of the form should not 511 be influenced by the 'fields'. 512 513 The optional 'repeating' and 'index' is used to refer to individual values 514 of a designated field. 515 """ 516 517 this_form = fragment and form_fragment == fragment or not fragment and not form_fragment 518 519 output = [] 520 section = fields 521 522 for region in getRegions(text, True): 523 format, attributes, body, header, close = getFragmentFromRegion(region) 524 525 # Adjust any FormField macros to use hierarchical names. 526 527 if format is None: 528 output.append((path or fragment or repeating) and 529 adjustFormFields(body, path, fragment, repeating, index) or body) 530 531 # Include form sections only if fields exist for those sections. 532 533 elif format == "form": 534 section_name = attributes.get("section") 535 message_name = attributes.get("message") 536 absent_message_name = attributes.get("not-message") 537 538 # Ignore sections not related to the supplied field data. 539 540 if not this_form: 541 pass 542 543 # Sections are groups of fields in their own namespace. 544 545 elif section_name and section.has_key(section_name): 546 547 # Iterate over the section contents ignoring the given indexes. 548 549 for index, element in enumerate(getSectionElements(section[section_name])): 550 element_ref = "%s$%s" % (section_name, index) 551 552 # Get the output for the section. 553 554 output.append(getFormOutput(body, element, form_fragment, 555 path and ("%s/%s" % (path, element_ref)) or element_ref, fragment)) 556 557 # Message regions are conditional on a particular field and 558 # reference the current namespace. 559 560 elif message_name and section.has_key(message_name): 561 562 if attributes.get("repeating"): 563 for index in range(0, len(section[message_name])): 564 output.append(getFormOutput(body, section, form_fragment, path, fragment, message_name, index)) 565 else: 566 output.append(getFormOutput(body, section, form_fragment, path, fragment)) 567 568 # Not-message regions are conditional on a particular field being 569 # absent. They reference the current namespace. 570 571 elif absent_message_name and not section.has_key(absent_message_name): 572 output.append(getFormOutput(body, section, form_fragment, path, fragment)) 573 574 # Inspect and include other regions. 575 576 else: 577 output.append(header) 578 output.append(getFormOutput(body, section, form_fragment, path, fragment, repeating, index)) 579 output.append(close) 580 581 return "".join(output) 582 583 def getFormFields(body, path, request): 584 585 "Return a dictionary of fields from the given 'body' at the given 'path'." 586 587 fields = {} 588 cache = {} 589 type = None 590 591 for i, match in enumerate(form_field_regexp.split(body)): 592 state = i % 3 593 594 if state == 1: 595 type = match 596 elif state == 2 and type == "Field": 597 args = {} 598 599 # Obtain the macro arguments, adjusted to consider the path. 600 601 name, path, dictpage, label, section, fragment = \ 602 getMacroArguments(adjustMacroArguments(parseMacroArguments(match), path)) 603 604 # Obtain field information from the cache, if possible. 605 606 cache_key = (name, dictpage) 607 608 if cache.has_key(cache_key): 609 field_args, allowed_values = cache[cache_key] 610 611 # Otherwise, obtain field information from any WikiDict. 612 613 else: 614 field_args = {} 615 allowed_values = None 616 617 if dictpage: 618 wikidict = getWikiDict(dictpage, request) 619 if wikidict: 620 field_definition = wikidict.get(name) 621 if field_definition: 622 field_args = getFieldArguments(field_definition) 623 if field_args.has_key("source"): 624 sourcedict = getWikiDict(field_args["source"], request) 625 if sourcedict: 626 allowed_values = sourcedict.keys() 627 628 cache[cache_key] = field_args, allowed_values 629 630 # Store the field information. 631 632 fields[name] = path, dictpage, label, section, field_args, allowed_values 633 634 return fields 635 636 def adjustFormFields(body, path, fragment, repeating=None, index=None): 637 638 """ 639 Return a version of the 'body' with the names in FormField macros updated to 640 incorporate the given 'path' and 'fragment'. If 'repeating' is specified, 641 any field with such a name will be adjusted to reference the value with the 642 given 'index'. 643 """ 644 645 result = [] 646 type = None 647 648 for i, match in enumerate(form_field_regexp.split(body)): 649 state = i % 3 650 651 # Reproduce normal text as is. 652 653 if state == 0: 654 result.append(match) 655 656 # Capture the macro type. 657 658 elif state == 1: 659 type = match 660 661 # Substitute the macro and modified arguments. 662 663 else: 664 result.append("<<Form%s(%s)>>" % (type, ",".join( 665 adjustMacroArguments(parseMacroArguments(match), path, fragment, repeating, index) 666 ))) 667 668 return "".join(result) 669 670 def adjustMacroArguments(args, path, fragment=None, repeating=None, index=None): 671 672 """ 673 Adjust the given 'args' so that the path incorporates the given 674 'path' and 'fragment', returning a new list containing the revised path, 675 fragment and remaining arguments. If 'repeating' is specified, any field 676 with such a name will be adjusted to reference the value with the given 677 'index'. 678 """ 679 680 if not path and not fragment and not repeating: 681 return args 682 683 result = [] 684 old_path = None 685 found_name = None 686 687 for arg in args: 688 if arg.startswith("path="): 689 old_path = arg[5:] 690 elif arg.startswith("fragment=") and fragment: 691 pass 692 else: 693 result.append(arg) 694 if arg.startswith("name="): 695 found_name = arg[5:] 696 elif found_name is None: 697 found_name = arg 698 699 if path: 700 qualified = old_path and ("%s/%s" % (old_path, path)) or path 701 result.append("path=%s" % qualified) 702 703 if fragment: 704 result.append("fragment=%s" % fragment) 705 706 if repeating and repeating == found_name: 707 result.append("index=%s" % index) 708 709 return result 710 711 def parseMacroArguments(args): 712 713 """ 714 Interpret the arguments. To support commas in labels, the label argument 715 should be quoted. For example: 716 717 "label=No, thanks!" 718 """ 719 720 try: 721 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 722 except AttributeError: 723 parsed_args = args.split(",") 724 725 return [arg for arg in parsed_args if arg] 726 727 def getMacroArguments(parsed_args): 728 729 "Return the macro arguments decoded from 'parsed_args'." 730 731 name = None 732 path = None 733 dictpage = None 734 label = None 735 section = None 736 fragment = None 737 738 for arg in parsed_args: 739 if arg.startswith("name="): 740 name = arg[5:] 741 742 elif arg.startswith("path="): 743 path = arg[5:] 744 745 elif arg.startswith("dict="): 746 dictpage = arg[5:] 747 748 elif arg.startswith("label="): 749 label = arg[6:] 750 751 elif arg.startswith("section="): 752 section = arg[8:] 753 754 elif arg.startswith("fragment="): 755 fragment = arg[9:] 756 757 elif name is None: 758 name = arg 759 760 elif dictpage is None: 761 dictpage = arg 762 763 return name, path, dictpage, label, section, fragment 764 765 def getFields(d, remove=False): 766 767 """ 768 Return the form fields hierarchy for the given dictionary 'd'. If the 769 optional 'remove' parameter is set to a true value, remove the entries for 770 the fields from 'd'. 771 """ 772 773 fields = {} 774 775 for key, value in d.items(): 776 777 # Detect modifying fields. 778 779 if key.find("=") != -1: 780 fields[key] = value 781 if remove: 782 del d[key] 783 continue 784 785 # Reproduce the original hierarchy of the fields. 786 787 section = fields 788 parts = getPathDetails(key) 789 790 for name, index in parts[:-1]: 791 792 # Add an entry for instances of the section. 793 794 if not section.has_key(name): 795 section[name] = {} 796 797 # Add an entry for the specific instance of the section. 798 799 if not section[name].has_key(index): 800 section[name][index] = {} 801 802 section = section[name][index] 803 804 section[parts[-1][0]] = value 805 806 if remove: 807 del d[key] 808 809 return fields 810 811 def getPathDetails(path): 812 813 """ 814 Return the given 'path' as a list of (name, index) tuples providing details 815 of section instances, with any specific field appearing as the last element 816 and having the form (name, None). 817 """ 818 819 parts = [] 820 821 for part in path.split("/"): 822 try: 823 name, index = part.split("$", 1) 824 index = int(index) 825 except ValueError: 826 name, index = part, None 827 828 parts.append((name, index)) 829 830 return parts 831 832 def getSectionForPath(path, fields): 833 834 """ 835 Obtain the section indicated by the given 'path' from the 'fields', 836 returning a tuple of the form (parent section, (name, index)), where the 837 parent section contains the referenced section, where name is the name of 838 the referenced section, and where index, if not None, is the index of a 839 specific section instance within the named section. 840 """ 841 842 parts = getPathDetails(path) 843 section = fields 844 845 for name, index in parts[:-1]: 846 section = fields[name][index] 847 848 return section, parts[-1] 849 850 def getSectionElements(section_elements): 851 852 "Return the given 'section_elements' as an ordered collection." 853 854 keys = map(int, section_elements.keys()) 855 keys.sort() 856 857 elements = [] 858 859 for key in keys: 860 elements.append(section_elements[key]) 861 862 return elements 863 864 # Parser-related formatting functions. 865 866 def formatForm(text, request, fmt, attrs=None, write=None): 867 868 """ 869 Format the given 'text' using the specified 'request' and formatter 'fmt'. 870 The optional 'attrs' can be used to control the presentation of the form. 871 872 If the 'write' parameter is specified, use it to write output; otherwise, 873 write output using the request. 874 """ 875 876 write = write or request.write 877 page = request.page 878 879 form = get_form(request) 880 form_fragment = form.get("fragment", [None])[0] 881 fields = getFields(form) 882 883 # Prepare the query string for the form action URL. 884 885 queryparams = [] 886 887 for argname, default in [("fragment", None), ("action", "MoinFormHandler")]: 888 if attrs and attrs.has_key(argname): 889 queryparams.append("%s=%s" % (argname, attrs[argname])) 890 elif default: 891 queryparams.append("%s=%s" % (argname, default)) 892 893 querystr = "&".join(queryparams) 894 fragment = attrs.get("fragment") 895 896 write(fmt.rawHTML('<form method="post" action="%s%s"%s>' % ( 897 escattr(page.url(request, querystr)), 898 fragment and ("#%s" % escattr(fragment)) or "", 899 fragment and (' id="%s"' % escattr(fragment)) or "" 900 ))) 901 902 # Obtain page text for the form, incorporating subregions and applicable 903 # sections. 904 905 output = getFormOutput(text, fields, form_fragment=form_fragment, fragment=fragment) 906 write(formatText(output, request, fmt, inhibit_p=False)) 907 908 write(fmt.rawHTML('</form>')) 909 910 def formatFormForOutputType(text, request, mimetype, attrs=None, write=None): 911 912 """ 913 Format the given 'text' using the specified 'request' for the given output 914 'mimetype'. 915 916 The optional 'attrs' can be used to control the presentation of the form. 917 918 If the 'write' parameter is specified, use it to write output; otherwise, 919 write output using the request. 920 """ 921 922 write = write or request.write 923 924 if mimetype == "text/html": 925 write('<html>') 926 write('<body>') 927 fmt = request.html_formatter 928 fmt.setPage(request.page) 929 formatForm(text, request, fmt, attrs, write) 930 write('</body>') 931 write('</html>') 932 933 # vim: tabstop=4 expandtab shiftwidth=4