appy.gen: added param 'maxChars' for any Type, allowing to limit the amount of data in user input fields. Wherever possible, it is implemented using HTML attribute 'maxlength'; on server-side, content that is bigger than specified by maxChars is truncated (ie, it is not possible to force a maxlength for html textareas); implemented a first protection against XSS attacks (Javasscript detection in user input).
This commit is contained in:
parent
9e7ddcc771
commit
bce384e2da
121
gen/__init__.py
121
gen/__init__.py
|
@ -360,7 +360,7 @@ class Type:
|
|||
def __init__(self, validator, multiplicity, index, default, optional,
|
||||
editDefault, show, page, group, layouts, move, indexed,
|
||||
searchable, specificReadPermission, specificWritePermission,
|
||||
width, height, colspan, master, masterValue, focus,
|
||||
width, height, maxChars, colspan, master, masterValue, focus,
|
||||
historized, sync, mapping):
|
||||
# The validator restricts which values may be defined. It can be an
|
||||
# interval (1,None), a list of string values ['choice1', 'choice2'],
|
||||
|
@ -425,6 +425,11 @@ class Type:
|
|||
# Widget width and height
|
||||
self.width = width
|
||||
self.height = height
|
||||
# While width and height refer to widget dimensions, maxChars hereafter
|
||||
# represents the maximum number of chars that a given input field may
|
||||
# accept (corresponds to HTML "maxlength" property). "None" means
|
||||
# "unlimited".
|
||||
self.maxChars = maxChars
|
||||
# If the widget is in a group with multiple columns, the following
|
||||
# attribute specifies on how many columns to span the widget.
|
||||
self.colspan = colspan
|
||||
|
@ -807,6 +812,16 @@ class Type:
|
|||
type-specific validation. p_value is never empty.'''
|
||||
return None
|
||||
|
||||
def securityCheck(self, obj, value):
|
||||
'''This method performs some security checks on the p_value that
|
||||
represents user input.'''
|
||||
if not isinstance(value, basestring): return
|
||||
# Search Javascript code in the value (prevent XSS attacks).
|
||||
if '<script' in value:
|
||||
obj.log('Detected Javascript in user input.', type='error')
|
||||
raise 'Your behaviour is considered a security attack. System ' \
|
||||
'administrator has been warned.'
|
||||
|
||||
def validate(self, obj, value):
|
||||
'''This method checks that p_value, coming from the request (p_obj is
|
||||
being created or edited) and formatted through a call to
|
||||
|
@ -821,6 +836,8 @@ class Type:
|
|||
return obj.translate('field_required')
|
||||
else:
|
||||
return None
|
||||
# Perform security checks on p_value
|
||||
self.securityCheck(obj, value)
|
||||
# Triggers the sub-class-specific validation for this value
|
||||
message = self.validateValue(obj, value)
|
||||
if message: return message
|
||||
|
@ -914,13 +931,13 @@ class Integer(Type):
|
|||
page='main', group=None, layouts=None, move=0, indexed=False,
|
||||
searchable=False, specificReadPermission=False,
|
||||
specificWritePermission=False, width=6, height=None,
|
||||
colspan=1, master=None, masterValue=None, focus=False,
|
||||
historized=False, mapping=None):
|
||||
maxChars=13, colspan=1, master=None, masterValue=None,
|
||||
focus=False, historized=False, mapping=None):
|
||||
Type.__init__(self, validator, multiplicity, index, default, optional,
|
||||
editDefault, show, page, group, layouts, move, indexed,
|
||||
searchable, specificReadPermission,
|
||||
specificWritePermission, width, height, colspan, master,
|
||||
masterValue, focus, historized, True, mapping)
|
||||
specificWritePermission, width, height, maxChars, colspan,
|
||||
master, masterValue, focus, historized, True, mapping)
|
||||
self.pythonType = long
|
||||
|
||||
def validateValue(self, obj, value):
|
||||
|
@ -943,8 +960,8 @@ class Float(Type):
|
|||
page='main', group=None, layouts=None, move=0, indexed=False,
|
||||
searchable=False, specificReadPermission=False,
|
||||
specificWritePermission=False, width=6, height=None,
|
||||
colspan=1, master=None, masterValue=None, focus=False,
|
||||
historized=False, mapping=None, precision=None,
|
||||
maxChars=13, colspan=1, master=None, masterValue=None,
|
||||
focus=False, historized=False, mapping=None, precision=None,
|
||||
sep=(',', '.')):
|
||||
# The precision is the number of decimal digits. This number is used
|
||||
# for rendering the float, but the internal float representation is not
|
||||
|
@ -963,8 +980,8 @@ class Float(Type):
|
|||
Type.__init__(self, validator, multiplicity, index, default, optional,
|
||||
editDefault, show, page, group, layouts, move, indexed,
|
||||
False, specificReadPermission, specificWritePermission,
|
||||
width, height, colspan, master, masterValue, focus,
|
||||
historized, True, mapping)
|
||||
width, height, maxChars, colspan, master, masterValue,
|
||||
focus, historized, True, mapping)
|
||||
self.pythonType = float
|
||||
|
||||
def getFormattedValue(self, obj, value):
|
||||
|
@ -1112,8 +1129,8 @@ class String(Type):
|
|||
show=True, page='main', group=None, layouts=None, move=0,
|
||||
indexed=False, searchable=False, specificReadPermission=False,
|
||||
specificWritePermission=False, width=None, height=None,
|
||||
colspan=1, master=None, masterValue=None, focus=False,
|
||||
historized=False, mapping=None, transform='none'):
|
||||
maxChars=None, colspan=1, master=None, masterValue=None,
|
||||
focus=False, historized=False, mapping=None, transform='none'):
|
||||
self.format = format
|
||||
# The following field has a direct impact on the text entered by the
|
||||
# user. It applies a transformation on it, exactly as does the CSS
|
||||
|
@ -1124,10 +1141,10 @@ class String(Type):
|
|||
Type.__init__(self, validator, multiplicity, index, default, optional,
|
||||
editDefault, show, page, group, layouts, move, indexed,
|
||||
searchable, specificReadPermission,
|
||||
specificWritePermission, width, height, colspan, master,
|
||||
masterValue, focus, historized, True, mapping)
|
||||
specificWritePermission, width, height, maxChars, colspan,
|
||||
master, masterValue, focus, historized, True, mapping)
|
||||
self.isSelect = self.isSelection()
|
||||
# Default width and height vary according to String format
|
||||
# Default width, height and maxChars vary according to String format
|
||||
if width == None:
|
||||
if format == String.TEXT: self.width = 60
|
||||
else: self.width = 30
|
||||
|
@ -1135,6 +1152,12 @@ class String(Type):
|
|||
if format == String.TEXT: self.height = 5
|
||||
elif self.isSelect: self.height = 4
|
||||
else: self.height = 1
|
||||
if maxChars == None:
|
||||
if self.isSelect: pass
|
||||
elif format == String.LINE: self.maxChars = 256
|
||||
elif format == String.TEXT: self.maxChars = 9999
|
||||
elif format == String.XHTML: self.maxChars = 9999
|
||||
elif format == String.PASSWORD: self.maxChars = 20
|
||||
self.filterable = self.indexed and (self.format == String.LINE) and \
|
||||
not self.isSelect
|
||||
|
||||
|
@ -1330,6 +1353,10 @@ class String(Type):
|
|||
def store(self, obj, value):
|
||||
if self.isMultiValued() and isinstance(value, basestring):
|
||||
value = [value]
|
||||
# Truncate the result if longer than self.maxChars
|
||||
if self.maxChars and isinstance(value, basestring) and \
|
||||
(len(value) > self.maxChars):
|
||||
value = value[:self.maxChars]
|
||||
exec 'obj.%s = value' % self.name
|
||||
|
||||
def getIndexType(self):
|
||||
|
@ -1346,13 +1373,13 @@ class Boolean(Type):
|
|||
page='main', group=None, layouts = None, move=0, indexed=False,
|
||||
searchable=False, specificReadPermission=False,
|
||||
specificWritePermission=False, width=None, height=None,
|
||||
colspan=1, master=None, masterValue=None, focus=False,
|
||||
historized=False, mapping=None):
|
||||
maxChars=None, colspan=1, master=None, masterValue=None,
|
||||
focus=False, historized=False, mapping=None):
|
||||
Type.__init__(self, validator, multiplicity, index, default, optional,
|
||||
editDefault, show, page, group, layouts, move, indexed,
|
||||
searchable, specificReadPermission,
|
||||
specificWritePermission, width, height, colspan, master,
|
||||
masterValue, focus, historized, True, mapping)
|
||||
specificWritePermission, width, height, None, colspan,
|
||||
master, masterValue, focus, historized, True, mapping)
|
||||
self.pythonType = bool
|
||||
|
||||
def getDefaultLayouts(self):
|
||||
|
@ -1389,8 +1416,8 @@ class Date(Type):
|
|||
show=True, page='main', group=None, layouts=None, move=0,
|
||||
indexed=False, searchable=False, specificReadPermission=False,
|
||||
specificWritePermission=False, width=None, height=None,
|
||||
colspan=1, master=None, masterValue=None, focus=False,
|
||||
historized=False, mapping=None):
|
||||
maxChars=None, colspan=1, master=None, masterValue=None,
|
||||
focus=False, historized=False, mapping=None):
|
||||
self.format = format
|
||||
self.calendar = calendar
|
||||
self.startYear = startYear
|
||||
|
@ -1401,8 +1428,8 @@ class Date(Type):
|
|||
Type.__init__(self, validator, multiplicity, index, default, optional,
|
||||
editDefault, show, page, group, layouts, move, indexed,
|
||||
searchable, specificReadPermission,
|
||||
specificWritePermission, width, height, colspan, master,
|
||||
masterValue, focus, historized, True, mapping)
|
||||
specificWritePermission, width, height, None, colspan,
|
||||
master, masterValue, focus, historized, True, mapping)
|
||||
|
||||
def getCss(self, layoutType):
|
||||
if (layoutType == 'edit') and self.calendar:
|
||||
|
@ -1462,13 +1489,13 @@ class File(Type):
|
|||
page='main', group=None, layouts=None, move=0, indexed=False,
|
||||
searchable=False, specificReadPermission=False,
|
||||
specificWritePermission=False, width=None, height=None,
|
||||
colspan=1, master=None, masterValue=None, focus=False,
|
||||
historized=False, mapping=None, isImage=False):
|
||||
maxChars=None, colspan=1, master=None, masterValue=None,
|
||||
focus=False, historized=False, mapping=None, isImage=False):
|
||||
self.isImage = isImage
|
||||
Type.__init__(self, validator, multiplicity, index, default, optional,
|
||||
editDefault, show, page, group, layouts, move, indexed,
|
||||
False, specificReadPermission, specificWritePermission,
|
||||
width, height, colspan, master, masterValue, focus,
|
||||
width, height, None, colspan, master, masterValue, focus,
|
||||
historized, True, mapping)
|
||||
|
||||
@staticmethod
|
||||
|
@ -1612,8 +1639,8 @@ class Ref(Type):
|
|||
select=None, maxPerPage=30, move=0, indexed=False,
|
||||
searchable=False, specificReadPermission=False,
|
||||
specificWritePermission=False, width=None, height=5,
|
||||
colspan=1, master=None, masterValue=None, focus=False,
|
||||
historized=False, mapping=None, queryable=False,
|
||||
maxChars=None, colspan=1, master=None, masterValue=None,
|
||||
focus=False, historized=False, mapping=None, queryable=False,
|
||||
queryFields=None, queryNbCols=1):
|
||||
self.klass = klass
|
||||
self.attribute = attribute
|
||||
|
@ -1663,7 +1690,7 @@ class Ref(Type):
|
|||
Type.__init__(self, validator, multiplicity, index, default, optional,
|
||||
editDefault, show, page, group, layouts, move, indexed,
|
||||
False, specificReadPermission, specificWritePermission,
|
||||
width, height, colspan, master, masterValue, focus,
|
||||
width, height, None, colspan, master, masterValue, focus,
|
||||
historized, sync, mapping)
|
||||
self.validable = self.link
|
||||
|
||||
|
@ -1829,9 +1856,9 @@ class Computed(Type):
|
|||
page='main', group=None, layouts=None, move=0, indexed=False,
|
||||
searchable=False, specificReadPermission=False,
|
||||
specificWritePermission=False, width=None, height=None,
|
||||
colspan=1, method=None, plainText=True, master=None,
|
||||
masterValue=None, focus=False, historized=False, sync=True,
|
||||
mapping=None, context={}):
|
||||
maxChars=None, colspan=1, method=None, plainText=True,
|
||||
master=None, masterValue=None, focus=False, historized=False,
|
||||
sync=True, mapping=None, context={}):
|
||||
# The Python method used for computing the field value
|
||||
self.method = method
|
||||
# Does field computation produce plain text or XHTML?
|
||||
|
@ -1847,8 +1874,8 @@ class Computed(Type):
|
|||
Type.__init__(self, None, multiplicity, index, default, optional,
|
||||
False, show, page, group, layouts, move, indexed, False,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, colspan, master, masterValue, focus, historized,
|
||||
sync, mapping)
|
||||
height, None, colspan, master, masterValue, focus,
|
||||
historized, sync, mapping)
|
||||
self.validable = False
|
||||
|
||||
def callMacro(self, obj, macroPath):
|
||||
|
@ -1896,9 +1923,9 @@ class Action(Type):
|
|||
page='main', group=None, layouts=None, move=0, indexed=False,
|
||||
searchable=False, specificReadPermission=False,
|
||||
specificWritePermission=False, width=None, height=None,
|
||||
colspan=1, action=None, result='computation', confirm=False,
|
||||
master=None, masterValue=None, focus=False, historized=False,
|
||||
mapping=None):
|
||||
maxChars=None, colspan=1, action=None, result='computation',
|
||||
confirm=False, master=None, masterValue=None, focus=False,
|
||||
historized=False, mapping=None):
|
||||
# Can be a single method or a list/tuple of methods
|
||||
self.action = action
|
||||
# For the 'result' param:
|
||||
|
@ -1918,8 +1945,8 @@ class Action(Type):
|
|||
Type.__init__(self, None, (0,1), index, default, optional,
|
||||
False, show, page, group, layouts, move, indexed, False,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, colspan, master, masterValue, focus, historized,
|
||||
False, mapping)
|
||||
height, None, colspan, master, masterValue, focus,
|
||||
historized, False, mapping)
|
||||
self.validable = False
|
||||
|
||||
def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'}
|
||||
|
@ -1966,13 +1993,13 @@ class Info(Type):
|
|||
page='main', group=None, layouts=None, move=0, indexed=False,
|
||||
searchable=False, specificReadPermission=False,
|
||||
specificWritePermission=False, width=None, height=None,
|
||||
colspan=1, master=None, masterValue=None, focus=False,
|
||||
historized=False, mapping=None):
|
||||
maxChars=None, colspan=1, master=None, masterValue=None,
|
||||
focus=False, historized=False, mapping=None):
|
||||
Type.__init__(self, None, (0,1), index, default, optional,
|
||||
False, show, page, group, layouts, move, indexed, False,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, colspan, master, masterValue, focus, historized,
|
||||
False, mapping)
|
||||
height, None, colspan, master, masterValue, focus,
|
||||
historized, False, mapping)
|
||||
self.validable = False
|
||||
|
||||
class Pod(Type):
|
||||
|
@ -1987,9 +2014,9 @@ class Pod(Type):
|
|||
page='main', group=None, layouts=None, move=0, indexed=False,
|
||||
searchable=False, specificReadPermission=False,
|
||||
specificWritePermission=False, width=None, height=None,
|
||||
colspan=1, master=None, masterValue=None, focus=False,
|
||||
historized=False, mapping=None, template=None, context=None,
|
||||
action=None, askAction=False, stylesMapping={},
|
||||
maxChars=None, colspan=1, master=None, masterValue=None,
|
||||
focus=False, historized=False, mapping=None, template=None,
|
||||
context=None, action=None, askAction=False, stylesMapping={},
|
||||
freezeFormat='pdf'):
|
||||
# The following param stores the path to a POD template
|
||||
self.template = template
|
||||
|
@ -2009,8 +2036,8 @@ class Pod(Type):
|
|||
Type.__init__(self, None, (0,1), index, default, optional,
|
||||
False, show, page, group, layouts, move, indexed,
|
||||
searchable, specificReadPermission,
|
||||
specificWritePermission, width, height, colspan, master,
|
||||
masterValue, focus, historized, False, mapping)
|
||||
specificWritePermission, width, height, None, colspan,
|
||||
master, masterValue, focus, historized, False, mapping)
|
||||
self.validable = False
|
||||
|
||||
def isFrozen(self, obj):
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
|
||||
<tal:comment replace="nothing">Edit macro for an Float.</tal:comment>
|
||||
<metal:edit define-macro="edit">
|
||||
<input tal:attributes="id name; name name; size widget/width;
|
||||
<input tal:define="maxChars python: test(widget['maxChars'], widget['maxChars'], '')"
|
||||
tal:attributes="id name; name name; size widget/width; maxlength maxChars;
|
||||
value python: test(inRequest, requestValue, value)" type="text"/>
|
||||
</metal:edit>
|
||||
|
||||
|
@ -16,14 +17,15 @@
|
|||
</metal:cell>
|
||||
|
||||
<tal:comment replace="nothing">Search macro for an Float.</tal:comment>
|
||||
<metal:search define-macro="search">
|
||||
<metal:search define-macro="search"
|
||||
tal:define="maxChars python: test(widget['maxChars'], widget['maxChars'], '')">
|
||||
<label tal:content="python: tool.translate(widget['labelId'])"></label><br>
|
||||
<tal:from define="fromName python: '%s*float' % widgetName">
|
||||
<label tal:attributes="for fromName" tal:content="python: tool.translate('search_from')"></label>
|
||||
<input type="text" tal:attributes="name fromName" size="4"/>
|
||||
<input type="text" tal:attributes="name fromName; maxlength maxChars" size="4"/>
|
||||
</tal:from>
|
||||
<tal:to define="toName python: '%s_to' % name">
|
||||
<label tal:attributes="for toName" tal:content="python: tool.translate('search_to')"></label>
|
||||
<input type="text" tal:attributes="name toName" size="4"/>
|
||||
<input type="text" tal:attributes="name toName; maxlength maxChars" size="4"/>
|
||||
</tal:to><br/>
|
||||
</metal:search>
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
|
||||
<tal:comment replace="nothing">Edit macro for an Integer.</tal:comment>
|
||||
<metal:edit define-macro="edit">
|
||||
<input tal:attributes="id name; name name; size widget/width;
|
||||
<input tal:define="maxChars python: test(widget['maxChars'], widget['maxChars'], '')"
|
||||
tal:attributes="id name; name name; size widget/width; maxlength maxChars;
|
||||
value python: test(inRequest, requestValue, value)" type="text"/>
|
||||
</metal:edit>
|
||||
|
||||
|
@ -15,14 +16,15 @@
|
|||
</metal:cell>
|
||||
|
||||
<tal:comment replace="nothing">Search macro for an Integer.</tal:comment>
|
||||
<metal:search define-macro="search">
|
||||
<metal:search define-macro="search"
|
||||
tal:define="maxChars python: test(widget['maxChars'], widget['maxChars'], '')">
|
||||
<label tal:content="python: tool.translate(widget['labelId'])"></label><br>
|
||||
<tal:from define="fromName python: '%s*int' % widgetName">
|
||||
<label tal:attributes="for fromName" tal:content="python: tool.translate('search_from')"></label>
|
||||
<input type="text" tal:attributes="name fromName" size="4"/>
|
||||
<input type="text" tal:attributes="name fromName; maxlength maxChars" size="4"/>
|
||||
</tal:from>
|
||||
<tal:to define="toName python: '%s_to' % name">
|
||||
<label tal:attributes="for toName" tal:content="python: tool.translate('search_to')"></label>
|
||||
<input type="text" tal:attributes="name toName" size="4"/>
|
||||
<input type="text" tal:attributes="name toName; maxlength maxChars" size="4"/>
|
||||
</tal:to><br/>
|
||||
</metal:search>
|
||||
|
|
|
@ -23,7 +23,8 @@
|
|||
tal:define="fmt widget/format;
|
||||
isSelect widget/isSelect;
|
||||
isMaster widget/slaves;
|
||||
isOneLine python: fmt in (0,3)">
|
||||
isOneLine python: fmt in (0,3);
|
||||
maxChars python: test(widget['maxChars'], widget['maxChars'], '')">
|
||||
|
||||
<tal:choice condition="isSelect">
|
||||
<select tal:define="possibleValues python:contextObj.getPossibleValues(name, withTranslations=True, withBlankValue=True)"
|
||||
|
@ -41,7 +42,7 @@
|
|||
</select>
|
||||
</tal:choice>
|
||||
<tal:line condition="python: isOneLine and not isSelect">
|
||||
<input tal:attributes="id name; name name; size widget/width;
|
||||
<input tal:attributes="id name; name name; size widget/width; maxlength maxChars;
|
||||
value python: test(inRequest, requestValue, value);
|
||||
style python: 'text-transform:%s' % widget['transform'];
|
||||
type python: (widget['format'] == 3) and 'password' or 'text'"/>
|
||||
|
@ -53,8 +54,6 @@
|
|||
style python: 'text-transform:%s' % widget['transform'];"
|
||||
tal:content="python: test(inRequest, requestValue, value)">
|
||||
</textarea>
|
||||
<input type="hidden" value="text/plain" originalvalue="text/plain"
|
||||
tal:attributes="name python: '%s_text_format' % name"/>
|
||||
</tal:textarea>
|
||||
<tal:rich condition="python: fmt == 2">
|
||||
<tal:editor define="editor python: member.getProperty('wysiwyg_editor','').lower();
|
||||
|
@ -84,7 +83,9 @@
|
|||
<label tal:attributes="for widgetName" tal:content="python: tool.translate(widget['labelId'])"></label><br>
|
||||
<tal:comment replace="nothing">Show a simple search field for most String fields.</tal:comment>
|
||||
<tal:simpleSearch condition="not: widget/isSelect">
|
||||
<input type="text" tal:attributes="name python: '%s*string-%s' % (widgetName, widget['transform']);
|
||||
<input type="text" tal:define="maxChars python: test(widget['maxChars'], widget['maxChars'], '')"
|
||||
tal:attributes="name python: '%s*string-%s' % (widgetName, widget['transform']);
|
||||
maxlength maxChars;
|
||||
style python: 'text-transform:%s' % widget['transform']"/>
|
||||
</tal:simpleSearch>
|
||||
<tal:comment replace="nothing">Show a multi-selection box for fields whose
|
||||
|
|
Loading…
Reference in a new issue