"Fossies" - the Fresh Open Source Software Archive

Member "roundup-2.0.0/roundup/cgi/form_parser.py" (29 Feb 2020, 28234 Bytes) of package /linux/www/roundup-2.0.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "form_parser.py": 1.6.1_vs_2.0.0.

    1 import re, mimetypes
    2 
    3 from roundup import hyperdb, date, password
    4 from roundup.cgi import templating
    5 from roundup.cgi.exceptions import FormError
    6 
    7 
    8 class FormParser:
    9     # edit form variable handling (see unit tests)
   10     FV_LABELS = r'''
   11        ^(
   12          (?P<note>[@:]note)|
   13          (?P<file>[@:]file)|
   14          (
   15           ((?P<classname>%s)(?P<id>[-\d]+))?  # optional leading designator
   16           ((?P<required>[@:]required$)|       # :required
   17            (
   18             (
   19              (?P<add>[@:]add[@:])|            # :add:<prop>
   20              (?P<remove>[@:]remove[@:])|      # :remove:<prop>
   21              (?P<confirm>[@:]confirm[@:])|    # :confirm:<prop>
   22              (?P<link>[@:]link[@:])|          # :link:<prop>
   23              ([@:])                           # just a separator
   24             )?
   25             (?P<propname>[^@:]+)             # <prop>
   26            )
   27           )
   28          )
   29         )$'''
   30 
   31     def __init__(self, client):
   32         self.client = client
   33         self.db = client.db
   34         self.form = client.form
   35         self.classname = client.classname
   36         self.nodeid = client.nodeid
   37         try:
   38             self._ = self.gettext = client.gettext
   39             self.ngettext = client.ngettext
   40         except AttributeError:
   41             _translator = templating.translationService
   42             self._ = self.gettext = _translator.gettext
   43             self.ngettext = _translator.ngettext
   44 
   45     def parse(self, create=0, num_re=re.compile(r'^\d+$')):
   46         """ Item properties and their values are edited with html FORM
   47             variables and their values. You can:
   48 
   49             - Change the value of some property of the current item.
   50             - Create a new item of any class, and edit the new item's
   51               properties,
   52             - Attach newly created items to a multilink property of the
   53               current item.
   54             - Remove items from a multilink property of the current item.
   55             - Specify that some properties are required for the edit
   56               operation to be successful.
   57 
   58             In the following, <bracketed> values are variable, "@" may be
   59             either ":" or "@", and other text "required" is fixed.
   60 
   61             Most properties are specified as form variables:
   62 
   63              <propname>
   64               - property on the current context item
   65 
   66              <designator>"@"<propname>
   67               - property on the indicated item (for editing related
   68                 information)
   69 
   70             Designators name a specific item of a class.
   71 
   72             <classname><N>
   73 
   74                 Name an existing item of class <classname>.
   75 
   76             <classname>"-"<N>
   77 
   78                 Name the <N>th new item of class <classname>. If the form
   79                 submission is successful, a new item of <classname> is
   80                 created. Within the submitted form, a particular
   81                 designator of this form always refers to the same new
   82                 item.
   83 
   84             Once we have determined the "propname", we look at it to see
   85             if it's special:
   86 
   87             @required
   88                 The associated form value is a comma-separated list of
   89                 property names that must be specified when the form is
   90                 submitted for the edit operation to succeed.
   91 
   92                 When the <designator> is missing, the properties are
   93                 for the current context item.  When <designator> is
   94                 present, they are for the item specified by
   95                 <designator>.
   96 
   97                 The "@required" specifier must come before any of the
   98                 properties it refers to are assigned in the form.
   99 
  100             @remove@<propname>=id(s) or @add@<propname>=id(s)
  101                 The "@add@" and "@remove@" edit actions apply only to
  102                 Multilink properties.  The form value must be a
  103                 comma-separate list of keys for the class specified by
  104                 the simple form variable.  The listed items are added
  105                 to (respectively, removed from) the specified
  106                 property.
  107 
  108             @link@<propname>=<designator>
  109                 If the edit action is "@link@", the simple form
  110                 variable must specify a Link or Multilink property.
  111                 The form value is a comma-separated list of
  112                 designators.  The item corresponding to each
  113                 designator is linked to the property given by simple
  114                 form variable.  These are collected up and returned in
  115                 all_links.
  116 
  117             None of the above (ie. just a simple form value)
  118                 The value of the form variable is converted
  119                 appropriately, depending on the type of the property.
  120 
  121                 For a Link('klass') property, the form value is a
  122                 single key for 'klass', where the key field is
  123                 specified in dbinit.py.
  124 
  125                 For a Multilink('klass') property, the form value is a
  126                 comma-separated list of keys for 'klass', where the
  127                 key field is specified in dbinit.py.
  128 
  129                 Note that for simple-form-variables specifiying Link
  130                 and Multilink properties, the linked-to class must
  131                 have a key field.
  132 
  133                 For a String() property specifying a filename, the
  134                 file named by the form value is uploaded. This means we
  135                 try to set additional properties "filename" and "type" (if
  136                 they are valid for the class).  Otherwise, the property
  137                 is set to the form value.
  138 
  139                 For Date(), Interval(), Boolean(), and Number(), Integer()
  140                 properties, the form value is converted to the
  141                 appropriate
  142 
  143             Any of the form variables may be prefixed with a classname or
  144             designator.
  145 
  146             Two special form values are supported for backwards
  147             compatibility:
  148 
  149             @note
  150                 This is equivalent to::
  151 
  152                     @link@messages=msg-1
  153                     msg-1@content=value
  154 
  155                 except that in addition, the "author" and "date"
  156                 properties of "msg-1" are set to the userid of the
  157                 submitter, and the current time, respectively.
  158 
  159             @file
  160                 This is equivalent to::
  161 
  162                     @link@files=file-1
  163                     file-1@content=value
  164 
  165                 The String content value is handled as described above for
  166                 file uploads.
  167                 If "multiple" is turned on for file uploads in the html
  168                 template, multiple links are generated::
  169 
  170                     @link@files=file-2
  171                     file-2@content=value
  172                     ...
  173 
  174                 depending on how many files the user has attached.
  175 
  176             If both the "@note" and "@file" form variables are
  177             specified, the action::
  178 
  179                     @link@msg-1@files=file-1
  180 
  181             is also performed. If "multiple" is specified this is
  182             carried out for each of the attached files.
  183 
  184             We also check that FileClass items have a "content" property with
  185             actual content, otherwise we remove them from all_props before
  186             returning.
  187 
  188             The return from this method is a dict of
  189                 (classname, id): properties
  190             ... this dict _always_ has an entry for the current context,
  191             even if it's empty (ie. a submission for an existing issue that
  192             doesn't result in any changes would return {('issue','123'): {}})
  193             The id may be None, which indicates that an item should be
  194             created.
  195         """
  196         # some very useful variables
  197         db = self.db
  198         form = self.form
  199 
  200         if not hasattr(self, 'FV_SPECIAL'):
  201             # generate the regexp for handling special form values
  202             classes = '|'.join(db.classes.keys())
  203             # specials for parsePropsFromForm
  204             # handle the various forms (see unit tests)
  205             self.FV_SPECIAL = re.compile(self.FV_LABELS % classes, re.VERBOSE)
  206             self.FV_DESIGNATOR = re.compile(r'(%s)([-\d]+)' % classes)
  207 
  208         # these indicate the default class / item
  209         default_cn = self.classname
  210         default_cl = self.db.classes[default_cn]
  211         default_nodeid = self.nodeid
  212 
  213         # we'll store info about the individual class/item edit in these
  214         all_required = {}       # required props per class/item
  215         all_props = {}          # props to set per class/item
  216         got_props = {}          # props received per class/item
  217         all_propdef = {}        # note - only one entry per class
  218         all_links = []          # as many as are required
  219 
  220         # we should always return something, even empty, for the context
  221         all_props[(default_cn, default_nodeid)] = {}
  222 
  223         keys = form.keys()
  224         timezone = db.getUserTimezone()
  225 
  226         # sentinels for the :note and :file props
  227         have_note = have_file = 0
  228 
  229         # extract the usable form labels from the form
  230         matches = []
  231         for key in keys:
  232             m = self.FV_SPECIAL.match(key)
  233             if m:
  234                 matches.append((key, m.groupdict()))
  235 
  236         # now handle the matches
  237         for key, d in matches:
  238             if d['classname']:
  239                 # we got a designator
  240                 cn = d['classname']
  241                 cl = self.db.classes[cn]
  242                 nodeid = d['id']
  243                 propname = d['propname']
  244             elif d['note']:
  245                 # the special note field
  246                 cn = 'msg'
  247                 cl = self.db.classes[cn]
  248                 nodeid = '-1'
  249                 propname = 'content'
  250                 all_links.append((default_cn, default_nodeid, 'messages',
  251                                   [('msg', '-1')]))
  252                 have_note = 1
  253             elif d['file']:
  254                 # the special file field
  255                 cn = default_cn
  256                 cl = default_cl
  257                 nodeid = default_nodeid
  258                 propname = 'files'
  259             else:
  260                 # default
  261                 cn = default_cn
  262                 cl = default_cl
  263                 nodeid = default_nodeid
  264                 propname = d['propname']
  265 
  266             # the thing this value relates to is...
  267             this = (cn, nodeid)
  268 
  269             # skip implicit create if this isn't a create action
  270             if not create and nodeid is None:
  271                 continue
  272 
  273             # get more info about the class, and the current set of
  274             # form props for it
  275             if cn not in all_propdef:
  276                 all_propdef[cn] = cl.getprops()
  277             propdef = all_propdef[cn]
  278             if this not in all_props:
  279                 all_props[this] = {}
  280             props = all_props[this]
  281             if this not in got_props:
  282                 got_props[this] = {}
  283 
  284             # is this a link command?
  285             if d['link']:
  286                 value = []
  287                 for entry in self.extractFormList(form[key]):
  288                     m = self.FV_DESIGNATOR.match(entry)
  289                     if not m:
  290                         raise FormError(self._('link "%(key)s" '
  291                             'value "%(entry)s" not a designator') % locals())
  292                     value.append((m.group(1), m.group(2)))
  293 
  294                     # get details of linked class
  295                     lcn = m.group(1)
  296                     lcl = self.db.classes[lcn]
  297                     lnodeid = m.group(2)
  298                     if lcn not in all_propdef:
  299                         all_propdef[lcn] = lcl.getprops()
  300                     if (lcn, lnodeid) not in all_props:
  301                         all_props[(lcn, lnodeid)] = {}
  302                     if (lcn, lnodeid) not in got_props:
  303                         got_props[(lcn, lnodeid)] = {}
  304 
  305                 # make sure the link property is valid
  306                 if (not isinstance(propdef[propname], hyperdb.Multilink) and
  307                         not isinstance(propdef[propname], hyperdb.Link)):
  308                     raise FormError(self._('%(class)s %(property)s '
  309                             'is not a link or multilink property') % {
  310                             'class':cn, 'property':propname})
  311 
  312                 all_links.append((cn, nodeid, propname, value))
  313                 continue
  314 
  315             # detect the special ":required" variable
  316             if d['required']:
  317                 for entry in self.extractFormList(form[key]):
  318                     m = self.FV_SPECIAL.match(entry)
  319                     if not m:
  320                         raise FormError(self._('The form action claims to '
  321                             'require property "%(property)s" '
  322                             'which doesn\'t exist') % {
  323                             'property':propname})
  324                     if m.group('classname'):
  325                         this = (m.group('classname'), m.group('id'))
  326                         entry = m.group('propname')
  327                     if this not in all_required:
  328                         all_required[this] = []
  329                     all_required[this].append(entry)
  330                 continue
  331 
  332             # see if we're performing a special multilink action
  333             mlaction = 'set'
  334             if d['remove']:
  335                 mlaction = 'remove'
  336             elif d['add']:
  337                 mlaction = 'add'
  338 
  339             # does the property exist?
  340             if propname not in propdef:
  341                 if mlaction != 'set':
  342                     raise FormError(self._('You have submitted a %(action)s '
  343                         'action for the property "%(property)s" '
  344                         'which doesn\'t exist') % {
  345                         'action': mlaction, 'property': propname})
  346                 # the form element is probably just something we don't care
  347                 # about - ignore it
  348                 continue
  349             proptype = propdef[propname]
  350 
  351             # Get the form value. This value may be a MiniFieldStorage
  352             # or a list of MiniFieldStorages.
  353             value = form[key]
  354 
  355             # handle unpacking of the MiniFieldStorage / list form value
  356             if d['file']:
  357                 assert isinstance(proptype, hyperdb.Multilink)
  358                 # value is a file upload... we *always* handle multiple
  359                 # files here (html5)
  360                 if not isinstance(value, type([])):
  361                     value = [value]
  362             elif isinstance(proptype, hyperdb.Multilink):
  363                 value = self.extractFormList(value)
  364             else:
  365                 # multiple values are not OK
  366                 if isinstance(value, type([])):
  367                     raise FormError(self._('You have submitted more than one '
  368                         'value for the %s property') % propname)
  369                 # value might be a single file upload
  370                 if not getattr(value, 'filename', None):
  371                     value = value.value.strip()
  372 
  373             # now that we have the props field, we need a teensy little
  374             # extra bit of help for the old :note field...
  375             if d['note'] and value:
  376                 props['author'] = self.db.getuid()
  377                 props['date'] = date.Date()
  378 
  379             # handle by type now
  380             if isinstance(proptype, hyperdb.Password):
  381                 if not value:
  382                     # ignore empty password values
  383                     continue
  384                 if d['confirm']:
  385                     # ignore the "confirm" password value by itself
  386                     continue
  387                 for key, d in matches:
  388                     if d['confirm'] and d['propname'] == propname:
  389                         confirm = form[key]
  390                         break
  391                 else:
  392                     raise FormError(self._('Password and confirmation text '
  393                                            'do not match'))
  394                 if isinstance(confirm, type([])):
  395                     raise FormError(self._('You have submitted more than one '
  396                         'value for the %s property') % propname)
  397                 if value != confirm.value:
  398                     raise FormError(self._('Password and confirmation text '
  399                                            'do not match'))
  400                 try:
  401                     value = password.Password(value, scheme=proptype.scheme,
  402                                               config=self.db.config)
  403                 except hyperdb.HyperdbValueError as msg:
  404                     raise FormError(msg)
  405             elif d['file']:
  406                 # This needs to be a Multilink and is checked above
  407                 fcn = 'file'
  408                 fcl = self.db.classes[fcn]
  409                 fpropname = 'content'
  410                 if fcn not in all_propdef:
  411                     all_propdef[fcn] = fcl.getprops()
  412                 fpropdef = all_propdef[fcn]
  413                 have_file = []
  414                 for n, v in enumerate(value):
  415                     if not hasattr(v, 'filename'):
  416                         raise FormError(self._('Not a file attachment'))
  417                     # skip if the upload is empty
  418                     if not v.filename:
  419                         continue
  420                     fnodeid = str(-(n+1))
  421                     have_file.append(fnodeid)
  422                     fthis = (fcn, fnodeid)
  423                     if fthis not in all_props:
  424                         all_props[fthis] = {}
  425                     fprops = all_props[fthis]
  426                     all_links.append((cn, nodeid, 'files',
  427                                       [('file', fnodeid)]))
  428 
  429                     fprops['content'] = self.parse_file(fpropdef, fprops, v)
  430                 value = None
  431                 nodeid = None
  432             elif isinstance(proptype, hyperdb.Multilink):
  433                 # convert input to list of ids
  434                 try:
  435                     l = hyperdb.rawToHyperdb(self.db, cl, nodeid,
  436                                              propname, value)
  437                 except hyperdb.HyperdbValueError as msg:
  438                     raise FormError(msg)
  439 
  440                 # now use that list of ids to modify the multilink
  441                 if mlaction == 'set':
  442                     value = l
  443                 else:
  444                     # we're modifying the list - get the current list of ids
  445                     if propname in props:
  446                         existing = props[propname]
  447                     elif nodeid and not nodeid.startswith('-'):
  448                         existing = cl.get(nodeid, propname, [])
  449                     else:
  450                         existing = []
  451 
  452                     # now either remove or add
  453                     if mlaction == 'remove':
  454                         # remove - handle situation where the id isn't in
  455                         # the list
  456                         for entry in l:
  457                             try:
  458                                 existing.remove(entry)
  459                             except ValueError:
  460                                 raise FormError(self._('property '
  461                                     '"%(propname)s": "%(value)s" '
  462                                     'not currently in list') % {
  463                                     'propname': propname, 'value': entry})
  464                     else:
  465                         # add - easy, just don't dupe
  466                         for entry in l:
  467                             if entry not in existing:
  468                                 existing.append(entry)
  469                     value = existing
  470                     # Sort the value in the same order used by
  471                     # Multilink.from_raw.
  472                     value.sort(key=int)
  473 
  474             elif value == '' or value == b'':
  475                 # other types should be None'd if there's no value
  476                 value = None
  477             else:
  478                 # handle all other types
  479                 try:
  480                     # Try handling file upload
  481                     if (isinstance(proptype, hyperdb.String) and
  482                         hasattr(value, 'filename') and
  483                         value.filename is not None):
  484                         value = self.parse_file(propdef, props, value)
  485                     else:
  486                         value = hyperdb.rawToHyperdb(self.db, cl, nodeid,
  487                                                      propname, value)
  488                 except hyperdb.HyperdbValueError as msg:
  489                     raise FormError(msg)
  490 
  491             # register that we got this property
  492             if isinstance(proptype, hyperdb.Multilink):
  493                 if value != []:
  494                     got_props[this][propname] = 1
  495             elif value is not None:
  496                 got_props[this][propname] = 1
  497 
  498             # get the old value
  499             if nodeid and not nodeid.startswith('-'):
  500                 try:
  501                     existing = cl.get(nodeid, propname)
  502                 except KeyError:
  503                     # this might be a new property for which there is
  504                     # no existing value
  505                     if propname not in propdef:
  506                         raise
  507                 except IndexError as message:
  508                     raise FormError(str(message))
  509 
  510                 # make sure the existing multilink is sorted.  We must
  511                 # be sure to use the same sort order in all places,
  512                 # since we want to compare values with "=" or "!=".
  513                 # The canonical order (given in Multilink.from_raw) is
  514                 # by the numeric value of the IDs.
  515                 if isinstance(proptype, hyperdb.Multilink):
  516                     existing.sort(key=int)
  517 
  518                 # "missing" existing values may not be None
  519                 if not existing:
  520                     if isinstance(proptype, hyperdb.String):
  521                         # some backends store "missing" Strings as
  522                         # empty strings
  523                         if existing == self.db.BACKEND_MISSING_STRING:
  524                             existing = None
  525                     elif isinstance(proptype, hyperdb.Number) or \
  526                          isinstance(proptype, hyperdb.Integer):
  527                         # some backends store "missing" Numbers as 0 :(
  528                         if existing == self.db.BACKEND_MISSING_NUMBER:
  529                             existing = None
  530                     elif isinstance(proptype, hyperdb.Boolean):
  531                         # likewise Booleans
  532                         if existing == self.db.BACKEND_MISSING_BOOLEAN:
  533                             existing = None
  534 
  535                 # if changed, set it
  536                 if value != existing:
  537                     props[propname] = value
  538             else:
  539                 # don't bother setting empty/unset values
  540                 if value is None:
  541                     continue
  542                 elif isinstance(proptype, hyperdb.Multilink) and value == []:
  543                     continue
  544                 elif isinstance(proptype, hyperdb.String) and value == '':
  545                     continue
  546 
  547                 props[propname] = value
  548 
  549         # check to see if we need to specially link files to the note
  550         if have_note and have_file:
  551             for fid in have_file:
  552                 all_links.append(('msg', '-1', 'files', [('file', fid)]))
  553 
  554         # see if all the required properties have been supplied
  555         s = []
  556         for thing, required in all_required.items():
  557             # register the values we got
  558             got = got_props.get(thing, {})
  559             for entry in required[:]:
  560                 if entry in got:
  561                     required.remove(entry)
  562 
  563             # If a user doesn't have edit permission for a given property,
  564             # but the property is already set in the database, we don't
  565             # require a value.
  566             if not (create or nodeid is None):
  567                 for entry in required[:]:
  568                     if not self.db.security.hasPermission('Edit',
  569                                                           self.client.userid,
  570                                                           self.classname,
  571                                                           entry):
  572                         cl = self.db.classes[self.classname]
  573                         if cl.get(nodeid, entry) is not None:
  574                             required.remove(entry)
  575 
  576             # any required values not present?
  577             if not required:
  578                 continue
  579 
  580             # tell the user to entry the values required
  581             s.append(self.ngettext(
  582                 'Required %(class)s property %(property)s not supplied',
  583                 'Required %(class)s properties %(property)s not supplied',
  584                 len(required)
  585             ) % {
  586                 'class': self._(thing[0]),
  587                 'property': ', '.join(map(self.gettext, required))
  588             })
  589         if s:
  590             raise FormError('\n'.join(s))
  591 
  592         # When creating a FileClass node, it should have a non-empty content
  593         # property to be created. When editing a FileClass node, it should
  594         # either have a non-empty content property or no property at all. In
  595         # the latter case, nothing will change.
  596         for (cn, id), props in list(all_props.items()):
  597             if id is not None and id.startswith('-') and not props:
  598                 # new item (any class) with no content - ignore
  599                 del all_props[(cn, id)]
  600             elif isinstance(self.db.classes[cn], hyperdb.FileClass):
  601                 # three cases:
  602                 # id references existng file. If content is empty,
  603                 #    remove content from form so we don't wipe
  604                 #    existing file contents.
  605                 # id is -1, -2 ... I.E. a new file.
  606                 #  if content is not defined remove all fields that
  607                 #     reference that file.
  608                 #  if content is defined, let it pass through even if
  609                 #     content is empty. Yes people can upload/create
  610                 #     empty files.
  611                 if 'content' in props:
  612                     if id is not None and \
  613                        not id.startswith('-') and \
  614                        not props['content']:
  615                         # This is an existing file with emtpy content
  616                         # value in the form.
  617                         del props['content']
  618                 else:
  619                     # this is a new file without any content property.
  620                     if id is not None and id.startswith('-'):
  621                         del all_props[(cn, id)]
  622                 # if this is a new file with content (even 0 length content)
  623                 # allow it through and create the zero length file.
  624         return all_props, all_links
  625 
  626     def parse_file(self, fpropdef, fprops, v):
  627         # try to determine the file content-type
  628         fn = v.filename.split('\\')[-1]
  629         if 'name' in fpropdef:
  630             fprops['name'] = fn
  631         # use this info as the type/filename properties
  632         if 'type' in fpropdef:
  633             if hasattr(v, 'type') and v.type:
  634                 fprops['type'] = v.type
  635             elif mimetypes.guess_type(fn)[0]:
  636                 fprops['type'] = mimetypes.guess_type(fn)[0]
  637             else:
  638                 fprops['type'] = "application/octet-stream"
  639         # finally, read the content RAW
  640         return v.value
  641 
  642     def extractFormList(self, value):
  643         ''' Extract a list of values from the form value.
  644 
  645             It may be one of:
  646              [MiniFieldStorage('value'),
  647              MiniFieldStorage('value','value',...), ...]
  648              MiniFieldStorage('value,value,...')
  649              MiniFieldStorage('value')
  650         '''
  651         # multiple values are OK
  652         if isinstance(value, type([])):
  653             # it's a list of MiniFieldStorages - join then into
  654             values = ','.join([i.value.strip() for i in value])
  655         else:
  656             # it's a MiniFieldStorage, but may be a comma-separated list
  657             # of values
  658             values = value.value
  659 
  660         value = [i.strip() for i in values.split(',')]
  661 
  662         # filter out the empty bits
  663         return list(filter(None, value))
  664 
  665 # vim: set et sts=4 sw=4 :