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