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 text = store[number] 288 module = parse(text) 289 if checkStoredFormData(module): 290 return eval(text) 291 292 # NOTE: Should indicate any errors in retrieving form data. 293 294 except: 295 pass 296 297 return {} 298 299 def checkStoredFormData(node): 300 301 """ 302 Check the syntax 'node' and its descendants for suitability as parts of 303 a field definition. 304 """ 305 306 for child in node.getChildNodes(): 307 if isinstance(child, Const): 308 pass 309 elif not isinstance(child, (Dict, Discard, List, Module, Stmt)) or not checkStoredFormData(child): 310 return False 311 312 return True 313 314 class FormAccess: 315 316 "A means of checking access to form data." 317 318 def __init__(self, pagename, request, attributes): 319 self.pagename = pagename 320 self.request = request 321 self.attributes = attributes 322 323 def checkPermissions(self, action): 324 325 """ 326 Check the permissions of the user against any restrictions specified in 327 the form's 'attributes'. 328 """ 329 330 user = self.request.user 331 332 # Use the access definition if one is given. 333 334 if self.attributes.has_key("access"): 335 access = self.attributes["access"] 336 acl = security.AccessControlList(self.request.cfg, [access]) 337 policy = lambda request, pagename, username, action: acl.may(request, username, action) 338 339 # Otherwise, use the page permissions. 340 341 else: 342 policy = security._check 343 344 # The "read" action is only satisfied by the "admin" role. 345 346 return user and ( 347 action != "read" and policy(self.request, self.pagename, user.name, action) or 348 action == "read" and policy(self.request, self.pagename, user.name, "admin") 349 ) 350 351 class FormStore(ItemStore): 352 353 "A form-specific storage mechanism." 354 355 def __init__(self, handler): 356 357 "Initialise the store with the form 'handler'." 358 359 self.handler = handler 360 page = Page(handler.request, handler.pagename) 361 ItemStore.__init__(self, page, "forms", "form-locks") 362 363 def can_write(self): 364 365 """ 366 Permit writing of form data using the form attributes or page 367 permissions. 368 """ 369 370 return self.handler.checkPermissions("write") 371 372 def can_read(self): 373 374 """ 375 Permit reading of form data using the form attributes or page 376 permissions. 377 """ 378 379 return self.handler.checkPermissions("read") 380 381 # Form and field information. 382 383 def getFormStructure(text, request, path=None, structure=None): 384 385 """ 386 For the given form 'text' and using the 'request', return details of the 387 form for the section at the given 'path' (or the entire form if 'path' is 388 omitted), populating the given 'structure' (or populating a new structure if 389 'structure' is omitted). 390 """ 391 392 if structure is None: 393 structure = {} 394 395 for format, attributes, body in getFragments(text, True): 396 397 # Get field details at the current level. 398 399 if format is None: 400 structure.update(getFormFields(body, path, request)) 401 402 # Where a section is found, get details from within the section. 403 404 elif format == "form": 405 if attributes.has_key("section"): 406 section_name = attributes["section"] 407 section = structure[section_name] = {} 408 getFormStructure(body, request, path and ("%s/%s" % (path, section_name)) or section_name, section) 409 elif attributes.has_key("message"): 410 getFormStructure(body, request, path, structure) 411 elif attributes.has_key("not-message"): 412 getFormStructure(body, request, path, structure) 413 414 # Get field details from other kinds of region. 415 416 elif format != "form": 417 getFormStructure(body, request, path, structure) 418 419 return structure 420 421 def getFormForFragment(text, fragment=None): 422 423 """ 424 Return the form region from the given 'text' for the specified 'fragment'. 425 If no fragment is specified, the first form region is returned. The form 426 region is described using a tuple containing the attributes for the form 427 and the body text of the form. 428 """ 429 430 for format, attributes, body in getFragments(text): 431 if format == "form" and (not fragment or attributes.get("fragment") == fragment): 432 return attributes, body 433 434 return {}, None 435 436 def getFieldArguments(field_definition): 437 438 "Return the parsed arguments from the given 'field_definition' string." 439 440 field_args = {} 441 442 for field_arg in field_definition.split(): 443 if field_arg == "required": 444 field_args[field_arg] = True 445 continue 446 447 # Record the key-value details. 448 449 try: 450 argname, argvalue = field_arg.split("=", 1) 451 field_args[argname] = argvalue 452 453 # Single keywords are interpreted as type descriptions. 454 455 except ValueError: 456 if not field_args.has_key("type"): 457 field_args["type"] = field_arg 458 459 return field_args 460 461 # Common formatting functions. 462 463 def getFormOutput(text, fields, path=None, fragment=None, repeating=None, index=None): 464 465 """ 466 Combine regions found in the given 'text' and then return them as a single 467 block. The reason for doing this, as opposed to just passing each region to 468 a suitable parser for formatting, is that form sections may break up 469 regions, and such sections may not define separate subregions but instead 470 act as a means of conditional inclusion of text into an outer region. 471 472 The given 'fields' are used to populate fields provided in forms and to 473 control whether sections are populated or not. 474 475 The optional 'path' is used to adjust form fields to refer to the correct 476 part of the form hierarchy. 477 478 The optional 'fragment' is used to indicate the form to which the fields 479 belong. 480 481 The optional 'repeating' and 'index' is used to refer to individual values 482 of a designated field. 483 """ 484 485 output = [] 486 section = fields 487 488 for region in getRegions(text, True): 489 format, attributes, body, header, close = getFragmentFromRegion(region) 490 491 # Adjust any FormField macros to use hierarchical names. 492 493 if format is None: 494 output.append((path or fragment or repeating) and 495 adjustFormFields(body, path, fragment, repeating, index) or body) 496 497 # Include form sections only if fields exist for those sections. 498 499 elif format == "form": 500 section_name = attributes.get("section") 501 message_name = attributes.get("message") 502 absent_message_name = attributes.get("not-message") 503 504 # Sections are groups of fields in their own namespace. 505 506 if section_name and section.has_key(section_name): 507 508 # Iterate over the section contents ignoring the given indexes. 509 510 for index, element in enumerate(getSectionElements(section[section_name])): 511 element_ref = "%s$%s" % (section_name, index) 512 513 # Get the output for the section. 514 515 output.append(getFormOutput(body, element, 516 path and ("%s/%s" % (path, element_ref)) or element_ref, fragment)) 517 518 # Message regions are conditional on a particular field and 519 # reference the current namespace. 520 521 elif message_name and section.has_key(message_name): 522 523 if attributes.get("repeating"): 524 for index in range(0, len(section[message_name])): 525 output.append(getFormOutput(body, section, path, fragment, message_name, index)) 526 else: 527 output.append(getFormOutput(body, section, path, fragment)) 528 529 # Not-message regions are conditional on a particular field being 530 # absent. They reference the current namespace. 531 532 elif absent_message_name and not section.has_key(absent_message_name): 533 output.append(getFormOutput(body, section, path, fragment)) 534 535 # Inspect and include other regions. 536 537 else: 538 output.append(header) 539 output.append(getFormOutput(body, section, path, fragment, repeating, index)) 540 output.append(close) 541 542 return "".join(output) 543 544 def getFormFields(body, path, request): 545 546 "Return a dictionary of fields from the given 'body' at the given 'path'." 547 548 fields = {} 549 cache = {} 550 type = None 551 552 for i, match in enumerate(form_field_regexp.split(body)): 553 state = i % 3 554 555 if state == 1: 556 type = match 557 elif state == 2 and type == "Field": 558 args = {} 559 560 # Obtain the macro arguments, adjusted to consider the path. 561 562 name, path, dictpage, label, section, fragment = \ 563 getMacroArguments(adjustMacroArguments(parseMacroArguments(match), path)) 564 565 # Obtain field information from the cache, if possible. 566 567 cache_key = (name, dictpage) 568 569 if cache.has_key(cache_key): 570 field_args, allowed_values = cache[cache_key] 571 572 # Otherwise, obtain field information from any WikiDict. 573 574 else: 575 field_args = {} 576 allowed_values = None 577 578 if dictpage: 579 wikidict = getWikiDict(dictpage, request) 580 if wikidict: 581 field_definition = wikidict.get(name) 582 if field_definition: 583 field_args = getFieldArguments(field_definition) 584 if field_args.has_key("source"): 585 sourcedict = getWikiDict(field_args["source"], request) 586 if sourcedict: 587 allowed_values = sourcedict.keys() 588 589 cache[cache_key] = field_args, allowed_values 590 591 # Store the field information. 592 593 fields[name] = path, dictpage, label, section, field_args, allowed_values 594 595 return fields 596 597 def adjustFormFields(body, path, fragment, repeating=None, index=None): 598 599 """ 600 Return a version of the 'body' with the names in FormField macros updated to 601 incorporate the given 'path' and 'fragment'. If 'repeating' is specified, 602 any field with such a name will be adjusted to reference the value with the 603 given 'index'. 604 """ 605 606 result = [] 607 type = None 608 609 for i, match in enumerate(form_field_regexp.split(body)): 610 state = i % 3 611 612 # Reproduce normal text as is. 613 614 if state == 0: 615 result.append(match) 616 617 # Capture the macro type. 618 619 elif state == 1: 620 type = match 621 622 # Substitute the macro and modified arguments. 623 624 else: 625 result.append("<<Form%s(%s)>>" % (type, ",".join( 626 adjustMacroArguments(parseMacroArguments(match), path, fragment, repeating, index) 627 ))) 628 629 return "".join(result) 630 631 def adjustMacroArguments(args, path, fragment=None, repeating=None, index=None): 632 633 """ 634 Adjust the given 'args' so that the path incorporates the given 635 'path' and 'fragment', returning a new list containing the revised path, 636 fragment and remaining arguments. If 'repeating' is specified, any field 637 with such a name will be adjusted to reference the value with the given 638 'index'. 639 """ 640 641 if not path and not fragment and not repeating: 642 return args 643 644 result = [] 645 old_path = None 646 found_name = None 647 648 for arg in args: 649 if arg.startswith("path="): 650 old_path = arg[5:] 651 elif arg.startswith("fragment=") and fragment: 652 pass 653 else: 654 result.append(arg) 655 if arg.startswith("name="): 656 found_name = arg[5:] 657 elif found_name is None: 658 found_name = arg 659 660 if path: 661 qualified = old_path and ("%s/%s" % (old_path, path)) or path 662 result.append("path=%s" % qualified) 663 664 if fragment: 665 result.append("fragment=%s" % fragment) 666 667 if repeating and repeating == found_name: 668 result.append("index=%s" % index) 669 670 return result 671 672 def parseMacroArguments(args): 673 674 """ 675 Interpret the arguments. To support commas in labels, the label argument 676 should be quoted. For example: 677 678 "label=No, thanks!" 679 """ 680 681 try: 682 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 683 except AttributeError: 684 parsed_args = args.split(",") 685 686 return [arg for arg in parsed_args if arg] 687 688 def getMacroArguments(parsed_args): 689 690 "Return the macro arguments decoded from 'parsed_args'." 691 692 name = None 693 path = None 694 dictpage = None 695 label = None 696 section = None 697 fragment = None 698 699 for arg in parsed_args: 700 if arg.startswith("name="): 701 name = arg[5:] 702 703 elif arg.startswith("path="): 704 path = arg[5:] 705 706 elif arg.startswith("dict="): 707 dictpage = arg[5:] 708 709 elif arg.startswith("label="): 710 label = arg[6:] 711 712 elif arg.startswith("section="): 713 section = arg[8:] 714 715 elif arg.startswith("fragment="): 716 fragment = arg[9:] 717 718 elif name is None: 719 name = arg 720 721 elif dictpage is None: 722 dictpage = arg 723 724 return name, path, dictpage, label, section, fragment 725 726 def getFields(d, remove=False): 727 728 """ 729 Return the form fields hierarchy for the given dictionary 'd'. If the 730 optional 'remove' parameter is set to a true value, remove the entries for 731 the fields from 'd'. 732 """ 733 734 fields = {} 735 736 for key, value in d.items(): 737 738 # Detect modifying fields. 739 740 if key.find("=") != -1: 741 fields[key] = value 742 if remove: 743 del d[key] 744 continue 745 746 # Reproduce the original hierarchy of the fields. 747 748 section = fields 749 parts = getPathDetails(key) 750 751 for name, index in parts[:-1]: 752 753 # Add an entry for instances of the section. 754 755 if not section.has_key(name): 756 section[name] = {} 757 758 # Add an entry for the specific instance of the section. 759 760 if not section[name].has_key(index): 761 section[name][index] = {} 762 763 section = section[name][index] 764 765 section[parts[-1][0]] = value 766 767 if remove: 768 del d[key] 769 770 return fields 771 772 def getPathDetails(path): 773 774 """ 775 Return the given 'path' as a list of (name, index) tuples providing details 776 of section instances, with any specific field appearing as the last element 777 and having the form (name, None). 778 """ 779 780 parts = [] 781 782 for part in path.split("/"): 783 try: 784 name, index = part.split("$", 1) 785 index = int(index) 786 except ValueError: 787 name, index = part, None 788 789 parts.append((name, index)) 790 791 return parts 792 793 def getSectionForPath(path, fields): 794 795 """ 796 Obtain the section indicated by the given 'path' from the 'fields', 797 returning a tuple of the form (parent section, (name, index)), where the 798 parent section contains the referenced section, where name is the name of 799 the referenced section, and where index, if not None, is the index of a 800 specific section instance within the named section. 801 """ 802 803 parts = getPathDetails(path) 804 section = fields 805 806 for name, index in parts[:-1]: 807 section = fields[name][index] 808 809 return section, parts[-1] 810 811 def getSectionElements(section_elements): 812 813 "Return the given 'section_elements' as an ordered collection." 814 815 keys = map(int, section_elements.keys()) 816 keys.sort() 817 818 elements = [] 819 820 for key in keys: 821 elements.append(section_elements[key]) 822 823 return elements 824 825 # Parser-related formatting functions. 826 827 def formatForm(text, request, fmt, attrs=None, write=None): 828 829 """ 830 Format the given 'text' using the specified 'request' and formatter 'fmt'. 831 The optional 'attrs' can be used to control the presentation of the form. 832 833 If the 'write' parameter is specified, use it to write output; otherwise, 834 write output using the request. 835 """ 836 837 write = write or request.write 838 page = request.page 839 840 fields = getFields(get_form(request)) 841 842 # Prepare the query string for the form action URL. 843 844 queryparams = [] 845 846 for argname, default in [("fragment", None), ("action", "MoinFormHandler")]: 847 if attrs and attrs.has_key(argname): 848 queryparams.append("%s=%s" % (argname, attrs[argname])) 849 elif default: 850 queryparams.append("%s=%s" % (argname, default)) 851 852 querystr = "&".join(queryparams) 853 fragment = attrs.get("fragment") 854 855 write(fmt.rawHTML('<form method="post" action="%s%s"%s>' % ( 856 escattr(page.url(request, querystr)), 857 fragment and ("#%s" % escattr(fragment)) or "", 858 fragment and (' id="%s"' % escattr(fragment)) or "" 859 ))) 860 861 # Obtain page text for the form, incorporating subregions and applicable 862 # sections. 863 864 output = getFormOutput(text, fields, fragment=fragment) 865 write(formatText(output, request, fmt, inhibit_p=False)) 866 867 write(fmt.rawHTML('</form>')) 868 869 def formatFormForOutputType(text, request, mimetype, attrs=None, write=None): 870 871 """ 872 Format the given 'text' using the specified 'request' for the given output 873 'mimetype'. 874 875 The optional 'attrs' can be used to control the presentation of the form. 876 877 If the 'write' parameter is specified, use it to write output; otherwise, 878 write output using the request. 879 """ 880 881 write = write or request.write 882 883 if mimetype == "text/html": 884 write('<html>') 885 write('<body>') 886 fmt = request.html_formatter 887 fmt.setPage(request.page) 888 formatForm(text, request, fmt, attrs, write) 889 write('</body>') 890 write('</html>') 891 892 # vim: tabstop=4 expandtab shiftwidth=4