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