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:
Gaetan Delannay 2011-05-05 16:44:06 +02:00
parent 9e7ddcc771
commit bce384e2da
4 changed files with 92 additions and 60 deletions

View file

@ -360,7 +360,7 @@ class Type:
def __init__(self, validator, multiplicity, index, default, optional, def __init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission, specificWritePermission, searchable, specificReadPermission, specificWritePermission,
width, height, colspan, master, masterValue, focus, width, height, maxChars, colspan, master, masterValue, focus,
historized, sync, mapping): historized, sync, mapping):
# The validator restricts which values may be defined. It can be an # The validator restricts which values may be defined. It can be an
# interval (1,None), a list of string values ['choice1', 'choice2'], # interval (1,None), a list of string values ['choice1', 'choice2'],
@ -425,6 +425,11 @@ class Type:
# Widget width and height # Widget width and height
self.width = width self.width = width
self.height = height 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 # If the widget is in a group with multiple columns, the following
# attribute specifies on how many columns to span the widget. # attribute specifies on how many columns to span the widget.
self.colspan = colspan self.colspan = colspan
@ -807,6 +812,16 @@ class Type:
type-specific validation. p_value is never empty.''' type-specific validation. p_value is never empty.'''
return None 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): def validate(self, obj, value):
'''This method checks that p_value, coming from the request (p_obj is '''This method checks that p_value, coming from the request (p_obj is
being created or edited) and formatted through a call to being created or edited) and formatted through a call to
@ -821,6 +836,8 @@ class Type:
return obj.translate('field_required') return obj.translate('field_required')
else: else:
return None return None
# Perform security checks on p_value
self.securityCheck(obj, value)
# Triggers the sub-class-specific validation for this value # Triggers the sub-class-specific validation for this value
message = self.validateValue(obj, value) message = self.validateValue(obj, value)
if message: return message if message: return message
@ -914,13 +931,13 @@ class Integer(Type):
page='main', group=None, layouts=None, move=0, indexed=False, page='main', group=None, layouts=None, move=0, indexed=False,
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=6, height=None, specificWritePermission=False, width=6, height=None,
colspan=1, master=None, masterValue=None, focus=False, maxChars=13, colspan=1, master=None, masterValue=None,
historized=False, mapping=None): focus=False, historized=False, mapping=None):
Type.__init__(self, validator, multiplicity, index, default, optional, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission, searchable, specificReadPermission,
specificWritePermission, width, height, colspan, master, specificWritePermission, width, height, maxChars, colspan,
masterValue, focus, historized, True, mapping) master, masterValue, focus, historized, True, mapping)
self.pythonType = long self.pythonType = long
def validateValue(self, obj, value): def validateValue(self, obj, value):
@ -943,8 +960,8 @@ class Float(Type):
page='main', group=None, layouts=None, move=0, indexed=False, page='main', group=None, layouts=None, move=0, indexed=False,
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=6, height=None, specificWritePermission=False, width=6, height=None,
colspan=1, master=None, masterValue=None, focus=False, maxChars=13, colspan=1, master=None, masterValue=None,
historized=False, mapping=None, precision=None, focus=False, historized=False, mapping=None, precision=None,
sep=(',', '.')): sep=(',', '.')):
# The precision is the number of decimal digits. This number is used # The precision is the number of decimal digits. This number is used
# for rendering the float, but the internal float representation is not # 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, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
False, specificReadPermission, specificWritePermission, False, specificReadPermission, specificWritePermission,
width, height, colspan, master, masterValue, focus, width, height, maxChars, colspan, master, masterValue,
historized, True, mapping) focus, historized, True, mapping)
self.pythonType = float self.pythonType = float
def getFormattedValue(self, obj, value): def getFormattedValue(self, obj, value):
@ -1112,8 +1129,8 @@ class String(Type):
show=True, page='main', group=None, layouts=None, move=0, show=True, page='main', group=None, layouts=None, move=0,
indexed=False, searchable=False, specificReadPermission=False, indexed=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False, maxChars=None, colspan=1, master=None, masterValue=None,
historized=False, mapping=None, transform='none'): focus=False, historized=False, mapping=None, transform='none'):
self.format = format self.format = format
# The following field has a direct impact on the text entered by the # 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 # 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, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission, searchable, specificReadPermission,
specificWritePermission, width, height, colspan, master, specificWritePermission, width, height, maxChars, colspan,
masterValue, focus, historized, True, mapping) master, masterValue, focus, historized, True, mapping)
self.isSelect = self.isSelection() 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 width == None:
if format == String.TEXT: self.width = 60 if format == String.TEXT: self.width = 60
else: self.width = 30 else: self.width = 30
@ -1135,6 +1152,12 @@ class String(Type):
if format == String.TEXT: self.height = 5 if format == String.TEXT: self.height = 5
elif self.isSelect: self.height = 4 elif self.isSelect: self.height = 4
else: self.height = 1 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 \ self.filterable = self.indexed and (self.format == String.LINE) and \
not self.isSelect not self.isSelect
@ -1330,6 +1353,10 @@ class String(Type):
def store(self, obj, value): def store(self, obj, value):
if self.isMultiValued() and isinstance(value, basestring): if self.isMultiValued() and isinstance(value, basestring):
value = [value] 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 exec 'obj.%s = value' % self.name
def getIndexType(self): def getIndexType(self):
@ -1346,13 +1373,13 @@ class Boolean(Type):
page='main', group=None, layouts = None, move=0, indexed=False, page='main', group=None, layouts = None, move=0, indexed=False,
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False, maxChars=None, colspan=1, master=None, masterValue=None,
historized=False, mapping=None): focus=False, historized=False, mapping=None):
Type.__init__(self, validator, multiplicity, index, default, optional, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission, searchable, specificReadPermission,
specificWritePermission, width, height, colspan, master, specificWritePermission, width, height, None, colspan,
masterValue, focus, historized, True, mapping) master, masterValue, focus, historized, True, mapping)
self.pythonType = bool self.pythonType = bool
def getDefaultLayouts(self): def getDefaultLayouts(self):
@ -1389,8 +1416,8 @@ class Date(Type):
show=True, page='main', group=None, layouts=None, move=0, show=True, page='main', group=None, layouts=None, move=0,
indexed=False, searchable=False, specificReadPermission=False, indexed=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False, maxChars=None, colspan=1, master=None, masterValue=None,
historized=False, mapping=None): focus=False, historized=False, mapping=None):
self.format = format self.format = format
self.calendar = calendar self.calendar = calendar
self.startYear = startYear self.startYear = startYear
@ -1401,8 +1428,8 @@ class Date(Type):
Type.__init__(self, validator, multiplicity, index, default, optional, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission, searchable, specificReadPermission,
specificWritePermission, width, height, colspan, master, specificWritePermission, width, height, None, colspan,
masterValue, focus, historized, True, mapping) master, masterValue, focus, historized, True, mapping)
def getCss(self, layoutType): def getCss(self, layoutType):
if (layoutType == 'edit') and self.calendar: if (layoutType == 'edit') and self.calendar:
@ -1462,13 +1489,13 @@ class File(Type):
page='main', group=None, layouts=None, move=0, indexed=False, page='main', group=None, layouts=None, move=0, indexed=False,
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False, maxChars=None, colspan=1, master=None, masterValue=None,
historized=False, mapping=None, isImage=False): focus=False, historized=False, mapping=None, isImage=False):
self.isImage = isImage self.isImage = isImage
Type.__init__(self, validator, multiplicity, index, default, optional, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
False, specificReadPermission, specificWritePermission, False, specificReadPermission, specificWritePermission,
width, height, colspan, master, masterValue, focus, width, height, None, colspan, master, masterValue, focus,
historized, True, mapping) historized, True, mapping)
@staticmethod @staticmethod
@ -1612,8 +1639,8 @@ class Ref(Type):
select=None, maxPerPage=30, move=0, indexed=False, select=None, maxPerPage=30, move=0, indexed=False,
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=5, specificWritePermission=False, width=None, height=5,
colspan=1, master=None, masterValue=None, focus=False, maxChars=None, colspan=1, master=None, masterValue=None,
historized=False, mapping=None, queryable=False, focus=False, historized=False, mapping=None, queryable=False,
queryFields=None, queryNbCols=1): queryFields=None, queryNbCols=1):
self.klass = klass self.klass = klass
self.attribute = attribute self.attribute = attribute
@ -1663,7 +1690,7 @@ class Ref(Type):
Type.__init__(self, validator, multiplicity, index, default, optional, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
False, specificReadPermission, specificWritePermission, False, specificReadPermission, specificWritePermission,
width, height, colspan, master, masterValue, focus, width, height, None, colspan, master, masterValue, focus,
historized, sync, mapping) historized, sync, mapping)
self.validable = self.link self.validable = self.link
@ -1829,9 +1856,9 @@ class Computed(Type):
page='main', group=None, layouts=None, move=0, indexed=False, page='main', group=None, layouts=None, move=0, indexed=False,
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, method=None, plainText=True, master=None, maxChars=None, colspan=1, method=None, plainText=True,
masterValue=None, focus=False, historized=False, sync=True, master=None, masterValue=None, focus=False, historized=False,
mapping=None, context={}): sync=True, mapping=None, context={}):
# The Python method used for computing the field value # The Python method used for computing the field value
self.method = method self.method = method
# Does field computation produce plain text or XHTML? # Does field computation produce plain text or XHTML?
@ -1847,8 +1874,8 @@ class Computed(Type):
Type.__init__(self, None, multiplicity, index, default, optional, Type.__init__(self, None, multiplicity, index, default, optional,
False, show, page, group, layouts, move, indexed, False, False, show, page, group, layouts, move, indexed, False,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, colspan, master, masterValue, focus, historized, height, None, colspan, master, masterValue, focus,
sync, mapping) historized, sync, mapping)
self.validable = False self.validable = False
def callMacro(self, obj, macroPath): def callMacro(self, obj, macroPath):
@ -1896,9 +1923,9 @@ class Action(Type):
page='main', group=None, layouts=None, move=0, indexed=False, page='main', group=None, layouts=None, move=0, indexed=False,
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, action=None, result='computation', confirm=False, maxChars=None, colspan=1, action=None, result='computation',
master=None, masterValue=None, focus=False, historized=False, confirm=False, master=None, masterValue=None, focus=False,
mapping=None): historized=False, mapping=None):
# Can be a single method or a list/tuple of methods # Can be a single method or a list/tuple of methods
self.action = action self.action = action
# For the 'result' param: # For the 'result' param:
@ -1918,8 +1945,8 @@ class Action(Type):
Type.__init__(self, None, (0,1), index, default, optional, Type.__init__(self, None, (0,1), index, default, optional,
False, show, page, group, layouts, move, indexed, False, False, show, page, group, layouts, move, indexed, False,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, colspan, master, masterValue, focus, historized, height, None, colspan, master, masterValue, focus,
False, mapping) historized, False, mapping)
self.validable = False self.validable = False
def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'} 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, page='main', group=None, layouts=None, move=0, indexed=False,
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False, maxChars=None, colspan=1, master=None, masterValue=None,
historized=False, mapping=None): focus=False, historized=False, mapping=None):
Type.__init__(self, None, (0,1), index, default, optional, Type.__init__(self, None, (0,1), index, default, optional,
False, show, page, group, layouts, move, indexed, False, False, show, page, group, layouts, move, indexed, False,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, colspan, master, masterValue, focus, historized, height, None, colspan, master, masterValue, focus,
False, mapping) historized, False, mapping)
self.validable = False self.validable = False
class Pod(Type): class Pod(Type):
@ -1987,9 +2014,9 @@ class Pod(Type):
page='main', group=None, layouts=None, move=0, indexed=False, page='main', group=None, layouts=None, move=0, indexed=False,
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False, maxChars=None, colspan=1, master=None, masterValue=None,
historized=False, mapping=None, template=None, context=None, focus=False, historized=False, mapping=None, template=None,
action=None, askAction=False, stylesMapping={}, context=None, action=None, askAction=False, stylesMapping={},
freezeFormat='pdf'): freezeFormat='pdf'):
# The following param stores the path to a POD template # The following param stores the path to a POD template
self.template = template self.template = template
@ -2009,8 +2036,8 @@ class Pod(Type):
Type.__init__(self, None, (0,1), index, default, optional, Type.__init__(self, None, (0,1), index, default, optional,
False, show, page, group, layouts, move, indexed, False, show, page, group, layouts, move, indexed,
searchable, specificReadPermission, searchable, specificReadPermission,
specificWritePermission, width, height, colspan, master, specificWritePermission, width, height, None, colspan,
masterValue, focus, historized, False, mapping) master, masterValue, focus, historized, False, mapping)
self.validable = False self.validable = False
def isFrozen(self, obj): def isFrozen(self, obj):

View file

@ -6,7 +6,8 @@
<tal:comment replace="nothing">Edit macro for an Float.</tal:comment> <tal:comment replace="nothing">Edit macro for an Float.</tal:comment>
<metal:edit define-macro="edit"> <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"/> value python: test(inRequest, requestValue, value)" type="text"/>
</metal:edit> </metal:edit>
@ -16,14 +17,15 @@
</metal:cell> </metal:cell>
<tal:comment replace="nothing">Search macro for an Float.</tal:comment> <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>&nbsp;&nbsp; <label tal:content="python: tool.translate(widget['labelId'])"></label><br>&nbsp;&nbsp;
<tal:from define="fromName python: '%s*float' % widgetName"> <tal:from define="fromName python: '%s*float' % widgetName">
<label tal:attributes="for fromName" tal:content="python: tool.translate('search_from')"></label> <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:from>
<tal:to define="toName python: '%s_to' % name"> <tal:to define="toName python: '%s_to' % name">
<label tal:attributes="for toName" tal:content="python: tool.translate('search_to')"></label> <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/> </tal:to><br/>
</metal:search> </metal:search>

View file

@ -5,7 +5,8 @@
<tal:comment replace="nothing">Edit macro for an Integer.</tal:comment> <tal:comment replace="nothing">Edit macro for an Integer.</tal:comment>
<metal:edit define-macro="edit"> <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"/> value python: test(inRequest, requestValue, value)" type="text"/>
</metal:edit> </metal:edit>
@ -15,14 +16,15 @@
</metal:cell> </metal:cell>
<tal:comment replace="nothing">Search macro for an Integer.</tal:comment> <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>&nbsp;&nbsp; <label tal:content="python: tool.translate(widget['labelId'])"></label><br>&nbsp;&nbsp;
<tal:from define="fromName python: '%s*int' % widgetName"> <tal:from define="fromName python: '%s*int' % widgetName">
<label tal:attributes="for fromName" tal:content="python: tool.translate('search_from')"></label> <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:from>
<tal:to define="toName python: '%s_to' % name"> <tal:to define="toName python: '%s_to' % name">
<label tal:attributes="for toName" tal:content="python: tool.translate('search_to')"></label> <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/> </tal:to><br/>
</metal:search> </metal:search>

View file

@ -23,7 +23,8 @@
tal:define="fmt widget/format; tal:define="fmt widget/format;
isSelect widget/isSelect; isSelect widget/isSelect;
isMaster widget/slaves; 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"> <tal:choice condition="isSelect">
<select tal:define="possibleValues python:contextObj.getPossibleValues(name, withTranslations=True, withBlankValue=True)" <select tal:define="possibleValues python:contextObj.getPossibleValues(name, withTranslations=True, withBlankValue=True)"
@ -41,7 +42,7 @@
</select> </select>
</tal:choice> </tal:choice>
<tal:line condition="python: isOneLine and not isSelect"> <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); value python: test(inRequest, requestValue, value);
style python: 'text-transform:%s' % widget['transform']; style python: 'text-transform:%s' % widget['transform'];
type python: (widget['format'] == 3) and 'password' or 'text'"/> type python: (widget['format'] == 3) and 'password' or 'text'"/>
@ -53,8 +54,6 @@
style python: 'text-transform:%s' % widget['transform'];" style python: 'text-transform:%s' % widget['transform'];"
tal:content="python: test(inRequest, requestValue, value)"> tal:content="python: test(inRequest, requestValue, value)">
</textarea> </textarea>
<input type="hidden" value="text/plain" originalvalue="text/plain"
tal:attributes="name python: '%s_text_format' % name"/>
</tal:textarea> </tal:textarea>
<tal:rich condition="python: fmt == 2"> <tal:rich condition="python: fmt == 2">
<tal:editor define="editor python: member.getProperty('wysiwyg_editor','').lower(); <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>&nbsp;&nbsp; <label tal:attributes="for widgetName" tal:content="python: tool.translate(widget['labelId'])"></label><br>&nbsp;&nbsp;
<tal:comment replace="nothing">Show a simple search field for most String fields.</tal:comment> <tal:comment replace="nothing">Show a simple search field for most String fields.</tal:comment>
<tal:simpleSearch condition="not: widget/isSelect"> <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']"/> style python: 'text-transform:%s' % widget['transform']"/>
</tal:simpleSearch> </tal:simpleSearch>
<tal:comment replace="nothing">Show a multi-selection box for fields whose <tal:comment replace="nothing">Show a multi-selection box for fields whose