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