Issue #268 resolved

Way for a method to recieve keyword as list or as string

Anonymous created an issue

Define a way for a method to recieve keyword as list or as string.

For example: * http://cpy/?q=1 invokes index( q = "1" ) and * http://cpy/?q=1&q=2 invokes index( q = ["1", "2"] )

In real world scenarios you want always a list if you use e.g. checkboxes and you want always a string if you use single form input. It would simplify web application code if there'd be any way to define whether you expect a list or string in method handler.

Reported by Kuba Winnicki yn@yn.pl

Comments (7)

  1. Robert Brewer

    This should be done with a filter instead of being written into the core; if the filter becomes popoular, then we can consider folding it in. Example:

    class ParamContainmentModelFilter:
        def beforeMain(self):
            path = cherrypy.request.objectPath or cherrypy.request.path
            while True:
                try:
                    func, objectPathList, virtualPathList = _cphttptools.mapPathToObject(path)
                    if hasattr(func, "param_containment"):
                        pm = cherrypy.request.paramMap
                        for name, type_ in func.param_containment.iteritems():
                            if name in pm:
                                val = pm[name]
                                if not isinstance(val, type_):
                                    if type_ is list:
                                        pm[name] = [pval]
                                    elif type_ is str:
                                        if isinstance(pval, list):
                                            pm[name] = pval[0]
                    
                    # Remove "root" from objectPathList and join it to get objectPath
                    cherrypy.request.objectPath = '/' + '/'.join(objectPathList[1:])
                    body = func(*(virtualPathList + cherrypy.request.paramList),
                                **(cherrypy.request.paramMap))
                    cherrypy.response.body = _cphttptools.iterable(body)
                    return
                except cherrypy.InternalRedirect, x:
                    # Try again with the new path
                    path = x.path
    

    ...which would be used like this, to set the `q` param to always be passed as a list:

    class Root:
        _cpFilterList = [ParamContainmentModelFilter()]
        def index(self, q=None):
            return repr(q)
        index.exposed = True
        index.param_containment = {'q': list}
    
  2. Anonymous

    As I think about it now this can have also security implications. If you look at generic CherryPy apps they expect params as strings and if some malicious user passes couple of params with a same name it will be sent to a method as a list requiring different handling in code.

    How about having it defined as a function defaults:

    def index( self, checkboxen = [], textarea = "", **params ):
            return repr( (checkboxen, textarea) )
    

    and setting params as defined in paramDefaultsMap:

    paramDefaultsMap = dict( map( None, index.func_code.co_varnames,
                          index.func_defaults ) )
    

    If param is defaulted to [] it would get param as a list, if defined in other way or undefined it would get first param occurence in a get or post request as a string.

    It'd be secure and would have simple interface. Or am I missing something?

  3. Robert Brewer

    Well, first, paramDefaultsMap needs more logic (it needs to inspect co_argcount as well, at least, to handle initial args without defaults): make it work with def method(self, a, b=[1,2,3], c="OK", *args, **kwargs) and you'll be closer.

    "If you look at generic !CherryPy apps they expect params as strings and if some malicious user passes a couple of params with the same name it will be sent to a method as a list requiring different handling in code."

    I don't think this is a huge security consideration; most apps will simply either fail with an error, or produce garbage output, when the value is consumed.

    But more importantly: although using func_defaults is a clever idea, I think it will fall down in practice. There are enough wrapped functions in CherryPy apps that you can't guarantee that the signature of "index" is the "final" function signature. That is, there are plenty of "default(self, *args, kwargs)" methods out there, which then dispatch to another function with the more concrete signature; the CP core would then at least have to fall back to sending each kwarg as either type, and force the method to handle both as it does now.

    Given that, it might be better to provide simple type-coercion functions to app developers. For example, put the following in cherrypy/__init__.py:

    def as_list(value):
        if isinstance(value, list):
            return value
        else:
            return [value]
    
    def as_string(value):
        if isinstance(value, basestring):
            return value
        else:
            return value[0]
    

    Then, in your app code, write:

    from cherrypy import as_list, as_string
    
    class Root:
        def index(self, checkboxen=[], textarea="", **params):
            for val in as_list(checkboxen):
                yield repr(val)
            yield as_string(textarea)
    
  4. Anonymous

    For now, no casting or type checking will be performed to parameters coming from the browser.

  5. Log in to comment