Two important bugfixes: one security-related, one linked to Ref fields with link=True.

This commit is contained in:
Gaetan Delannay 2010-08-27 08:59:53 +02:00
parent dbcadc506d
commit fa974239f3
12 changed files with 149 additions and 138 deletions

1
bin/new.py Executable file → Normal file
View file

@ -1,4 +1,3 @@
#!/usr/bin/python2.4.4
'''This script allows to create a brand new read-to-use Plone/Zone instance.
As prerequisite, you must have installed Plone through the Unifier installer
available at http://plone.org.'''

View file

@ -30,7 +30,7 @@ class Group:
'''Used for describing a group of widgets within a page.'''
def __init__(self, name, columns=['100%'], wide=True, style='fieldset',
hasLabel=True, hasDescr=False, hasHelp=False,
hasHeaders=False, group=None, colspan=1):
hasHeaders=False, group=None, colspan=1, valign='top'):
self.name = name
# In its simpler form, field "columns" below can hold a list or tuple
# of column widths expressed as strings, that will be given as is in
@ -65,6 +65,7 @@ class Group:
# If the group is rendered into another group, we can specify the number
# of columns that this group will span.
self.colspan = colspan
self.valign = valign
if style == 'tabs':
# Group content will be rendered as tabs. In this case, some
# param combinations have no sense.
@ -987,7 +988,8 @@ class Date(Type):
hourParts = ('hour', 'minute')
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False,
format=WITH_HOUR, startYear=time.localtime()[0]-10,
format=WITH_HOUR, calendar=True,
startYear=time.localtime()[0]-10,
endYear=time.localtime()[0]+10, show=True, page='main',
group=None, layouts=None, move=0, indexed=False,
searchable=False, specificReadPermission=False,
@ -995,6 +997,7 @@ class Date(Type):
colspan=1, master=None, masterValue=None, focus=False,
historized=False):
self.format = format
self.calendar = calendar
self.startYear = startYear
self.endYear = endYear
Type.__init__(self, validator, multiplicity, index, default, optional,
@ -1164,12 +1167,14 @@ class Ref(Type):
def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'}
def isShowable(self, obj, layoutType):
res = Type.isShowable(self, obj, layout)
if not res: return res
if (layoutType == 'edit') and self.add: return False
if self.isBack:
if layoutType == 'edit': return False
else:
return obj.getBRefs(self.relationship)
return Type.isShowable(self, obj, layout)
return True
def getValue(self, obj):
if self.isBack:

View file

@ -148,6 +148,9 @@ class Generator(AbstractGenerator):
msg('file_required', '', msg.FILE_REQUIRED),
msg('image_required', '', msg.IMAGE_REQUIRED),
]
# Create a label for every role added by this application
for role in self.getAllUsedRoles(appOnly=True):
self.labels.append(msg('role_%s' % role,'', role, niceDefault=True))
# Create basic files (config.py, Install.py, etc)
self.generateTool()
self.generateConfig()
@ -224,7 +227,7 @@ class Generator(AbstractGenerator):
if not poFile.generated:
poFile.generate()
ploneRoles = ('Manager', 'Member', 'Owner', 'Reviewer')
ploneRoles = ('Manager', 'Member', 'Owner', 'Reviewer', 'Anonymous')
def getAllUsedRoles(self, appOnly=False):
'''Produces a list of all the roles used within all workflows and
classes defined in this application. If p_appOnly is True, it

View file

@ -133,7 +133,6 @@ class PloneInstaller:
if not hasattr(site.portal_types, self.appyFolderType):
self.registerAppyFolderType()
# Create the folder
if not hasattr(site.aq_base, self.productName):
# Temporarily allow me to create Appy large plone folders
getattr(site.portal_types, self.appyFolderType).global_allow = 1
@ -144,29 +143,31 @@ class PloneInstaller:
title=self.productName)
getattr(site.portal_types, self.appyFolderType).global_allow = 0
appFolder = getattr(site, self.productName)
# All roles defined as creators should be able to create the
# corresponding root content types in this folder.
i = -1
allCreators = set()
for klass in self.appClasses:
i += 1
if klass.__dict__.has_key('root') and klass.__dict__['root']:
# It is a root class.
creators = getattr(klass, 'creators', None)
if not creators: creators = self.defaultAddRoles
allCreators = allCreators.union(creators)
className = self.appClassNames[i]
updateRolesForPermission(self.getAddPermission(className),
tuple(creators), appFolder)
if not klass.__dict__.has_key('root') or not klass.__dict__['root']:
continue # It is not a root class
creators = getattr(klass, 'creators', None)
if not creators: creators = self.defaultAddRoles
allCreators = allCreators.union(creators)
className = self.appClassNames[i]
permission = self.getAddPermission(className)
updateRolesForPermission(permission, tuple(creators), appFolder)
# Beyond content-type-specific "add" permissions, creators must also
# have the main permission "Add portal content".
updateRolesForPermission('Add portal content', tuple(allCreators),
appFolder)
permission = 'Add portal content'
updateRolesForPermission(permission, tuple(allCreators), appFolder)
# Creates the "appy" Directory view
if not hasattr(site.aq_base, 'skyn'):
addDirView = self.ploneStuff['manage_addDirectoryView']
addDirView(site, appy.getPath() + '/gen/plone25/skin',id='skyn')
if hasattr(site.aq_base, 'skyn'):
site.manage_delObjects(['skyn'])
# This way, if Appy has moved from one place to the other, the
# directory view will always refer to the correct place.
addDirView = self.ploneStuff['manage_addDirectoryView']
addDirView(site, appy.getPath() + '/gen/plone25/skin', id='skyn')
def installTypes(self):
'''Registers and configures the Plone content types that correspond to

View file

@ -263,39 +263,6 @@ class ToolMixin(AbstractMixin):
return value[:maxWidth] + '...'
return value
xhtmlToText = re.compile('<.*?>', re.S)
def getReferenceLabel(self, brain, appyType):
'''p_appyType is a Ref with link=True. I need to display, on an edit
view, the referenced object p_brain in the listbox that will allow
the user to choose which object(s) to link through the Ref.
According to p_appyType, the label may only be the object title,
or more if parameter appyType.shownInfo is used.'''
res = brain.Title
if 'title' in appyType['shownInfo']:
# We may place it at another place
res = ''
appyObj = brain.getObject().appy()
for fieldName in appyType['shownInfo']:
value = getattr(appyObj, fieldName)
if isinstance(value, AbstractWrapper):
value = value.title.decode('utf-8')
elif isinstance(value, basestring):
value = value.decode('utf-8')
refAppyType = appyObj.o.getAppyType(fieldName)
if refAppyType and (refAppyType.type == 'String') and \
(refAppyType.format == 2):
value = self.xhtmlToText.sub(' ', value)
else:
value = str(value)
prefix = ''
if res:
prefix = ' | '
res += prefix + value.encode('utf-8')
maxWidth = self.getListBoxesMaximumWidth()
if len(res) > maxWidth:
res = res[:maxWidth-2] + '...'
return res
translationMapping = {'portal_path': ''}
def translateWithMapping(self, label):
'''Translates p_label in the application domain, with a default

View file

@ -351,17 +351,61 @@ class AbstractMixin:
res.objects = res.objects[0]
return res
def getAppyRefs(self, appyType, startNumber=None):
'''Gets the objects linked to me through Ref p_appyType.
def getAppyRefs(self, name, startNumber=None):
'''Gets the objects linked to me through Ref field named p_name.
If p_startNumber is None, this method returns all referred objects.
If p_startNumber is a number, this method will return x objects,
starting at p_startNumber, x being appyType.maxPerPage.'''
if not appyType['isBack']:
return self._appy_getRefs(appyType['name'], ploneObjects=True,
startNumber=startNumber).__dict__
appyType = self.getAppyType(name)
if not appyType.isBack:
return self._appy_getRefs(name, ploneObjects=True,
startNumber=startNumber).__dict__
else:
# Note Pagination is not yet implemented for backward refs.
return SomeObjects(self.getBRefs(appyType['relationship'])).__dict__
# Note that pagination is not yet implemented for backward refs.
return SomeObjects(self.getBRefs(appyType.relationship)).__dict__
def getSelectableAppyRefs(self, name):
'''p_name is the name of a Ref field. This method returns the list of
all objects that can be selected to be linked as references to p_self
through field p_name.'''
appyType = self.getAppyType(name)
if not appyType.select:
# No select method has been defined: we must retrieve all objects
# of the referred type that the user is allowed to access.
return self.appy().search(appyType.klass)
else:
return appyType.select(self.appy())
xhtmlToText = re.compile('<.*?>', re.S)
def getReferenceLabel(self, name, refObject):
'''p_name is the name of a Ref field with link=True. I need to display,
on an edit view, the p_refObject in the listbox that will allow
the user to choose which object(s) to link through the Ref.
The information to display may only be the object title or more if
field.shownInfo is used.'''
appyType = self.getAppyType(name)
res = refObject.title
if 'title' in appyType.shownInfo:
# We may place it at another place
res = ''
for fieldName in appyType.shownInfo:
refType = refObject.o.getAppyType(fieldName)
value = getattr(refObject, fieldName)
value = refType.getFormattedValue(refObject.o, value)
if (refType.type == 'String') and (refType.format == 2):
value = self.xhtmlToText.sub(' ', value)
prefix = ''
if res:
prefix = ' | '
res += prefix + value
maxWidth = appyType.width or 30
if len(res) > maxWidth:
res = res[:maxWidth-2] + '...'
return res
def getReferenceUid(self, refObject):
'''Returns the UID of referred object p_refObject.'''
return refObject.o.UID()
def getAppyRefIndex(self, fieldName, obj):
'''Gets the position of p_obj within Ref field named p_fieldName.'''
@ -726,20 +770,9 @@ class AbstractMixin:
self.reindexObject()
return self.goto(urlBack)
def callAppySelect(self, selectMethod, brains):
'''Selects objects from a Reference field.'''
if selectMethod:
obj = self.appy()
allObjects = [b.getObject().appy() for b in brains]
filteredObjects = selectMethod(obj, allObjects)
filteredUids = [o.o.UID() for o in filteredObjects]
res = []
for b in brains:
if b.UID in filteredUids:
res.append(b)
else:
res = brains
return res
def getFlavour(self):
'''Returns the flavour corresponding to this object.'''
return self.getTool().getFlavour(self.portal_type)
def fieldValueSelected(self, fieldName, vocabValue, dbValue):
'''When displaying a selection box (ie a String with a validator being a
@ -888,31 +921,38 @@ class AbstractMixin:
folder = self.getParentNode()
# On this folder, set "add" permissions for every content type that will
# be created through reference fields
allCreators = set()
allCreators = {} # One key for every add permission
addPermissions = self.getProductConfig().ADD_CONTENT_PERMISSIONS
for appyType in self.getAllAppyTypes():
if appyType.type == 'Ref':
refContentTypeName = self.getAppyRefPortalType(appyType.name)
refContentType = getattr(self.portal_types, refContentTypeName)
refMetaType = refContentType.content_meta_type
if refMetaType in self.getProductConfig(\
).ADD_CONTENT_PERMISSIONS:
# No specific "add" permission is defined for tool and
# flavour, for example.
appyClass = refContentType.wrapperClass.__bases__[-1]
# Get roles that may add this content type
creators = getattr(appyClass, 'creators', None)
if not creators:
creators = self.getProductConfig().defaultAddRoles
allCreators = allCreators.union(creators)
# Grant this "add" permission to those roles
updateRolesForPermission(
self.getProductConfig().ADD_CONTENT_PERMISSIONS[\
refMetaType], creators, folder)
if appyType.type != 'Ref': continue
if appyType.isBack or appyType.link: continue
# Indeed, no possibility to create objects with such Ref
refContentTypeName = self.getAppyRefPortalType(appyType.name)
refContentType = getattr(self.portal_types, refContentTypeName)
refMetaType = refContentType.content_meta_type
if refMetaType not in addPermissions: continue
# Indeed, there is no specific "add" permission is defined for tool
# and flavour, for example.
appyClass = refContentType.wrapperClass.__bases__[-1]
# Get roles that may add this content type
creators = getattr(appyClass, 'creators', None)
if not creators:
creators = self.getProductConfig().defaultAddRoles
# Add those creators to the list of creators for this meta_type
addPermission = addPermissions[refMetaType]
if addPermission in allCreators:
allCreators[addPermission] = allCreators[\
addPermission].union(creators)
else:
allCreators[addPermission] = set(creators)
# Update the permissions
for permission, creators in allCreators.iteritems():
updateRolesForPermission(permission, tuple(creators), folder)
# Beyond content-type-specific "add" permissions, creators must also
# have the main permission "Add portal content".
if allCreators:
updateRolesForPermission('Add portal content', tuple(allCreators),
folder)
permission = 'Add portal content'
for creators in allCreators.itervalues():
updateRolesForPermission(permission, tuple(creators), folder)
def _appy_getPortalType(self, request):
'''Guess the portal_type of p_self from info about p_self and

View file

@ -17,7 +17,7 @@
tool python: portal.get('portal_%s' % appName.lower());
contentType python:context.REQUEST.get('type_name');
flavour python: tool.getFlavour(contentType);
flavourNumber python:int(context.REQUEST.get('flavourNumber'));
flavourNumber python:int(context.REQUEST.get('flavourNumber', 1));
searchName python:context.REQUEST.get('search', '')">
<div metal:use-macro="here/skyn/page/macros/prologue"/>

View file

@ -42,8 +42,9 @@
selected python:contextObj.dateValueSelected(name, 'year', year, rawValue)"
tal:content="year"></option>
</select>
<tal:comment replace="nothing">The icon for displaying the date chooser</tal:comment>
<a tal:attributes="onclick python: 'return showJsCalendar(\'%s_month\', \'%s\', \'%s_year\', \'%s_month\', \'%s_day\', null, null, %d, %d)' % (name, dummyName, name, name, name, years[0], years[-1])"><img tal:attributes="src string: $portal_url/popup_calendar.gif"/></a>
<tal:comment replace="nothing">The icon for displaying the calendar (=date chooser)</tal:comment>
<a tal:condition="widget/calendar"
tal:attributes="onclick python: 'return showJsCalendar(\'%s_month\', \'%s\', \'%s_year\', \'%s_month\', \'%s_day\', null, null, %d, %d)' % (name, dummyName, name, name, name, years[0], years[-1])"><img tal:attributes="src string: $portal_url/popup_calendar.gif"/></a>
<tal:hour condition="python: widget['format'] == 0">
<select tal:define="hours python:range(0,24);"

View file

@ -9,9 +9,10 @@
on a forward reference, the "nav" parameter is added to the URL for allowing to navigate
from one object to the next/previous on skyn/view.</tal:comment>
<a tal:define="viewUrl obj/getUrl;
includeShownInfo includeShownInfo | python:False;
navInfo python:'nav=ref.%s.%s.%d.%d' % (contextObj.UID(), fieldName, repeat['obj'].number()+startNumber, totalNumber);
fullUrl python: test(appyType['isBack'], viewUrl + '/?page=%s' % appyType['page'], viewUrl + '/?' + navInfo)"
tal:attributes="href fullUrl" tal:content="obj/Title"></a>
fullUrl python: appyType['isBack'] and (viewUrl + '/?page=%s' % appyType['backd']['page']) or (viewUrl + '/?' + navInfo)"
tal:attributes="href fullUrl" tal:content="python: (not includeShownInfo) and obj.Title() or contextObj.getReferenceLabel(fieldName, obj.appy())"></a>
</metal:objectTitle>
<metal:objectActions define-macro="objectActions">
@ -99,7 +100,7 @@
ajaxHookId python: contextObj.UID()+fieldName;
startNumber python: int(request.get('%s_startNumber' % ajaxHookId, 0));
tool contextObj/getTool;
refObjects python:contextObj.getAppyRefs(appyType, startNumber);
refObjects python:contextObj.getAppyRefs(fieldName, startNumber);
objs refObjects/objects;
totalNumber refObjects/totalNumber;
batchSize refObjects/batchSize;
@ -126,7 +127,8 @@
<tal:comment replace="nothing">Display a simplified widget if maximum number of
referenced objects is 1.</tal:comment>
<table class="no-style-table" cellpadding="0" cellspacing="0"><tr valign="top">
<td><span class="appyLabel" tal:condition="not: innerRef" tal:content="structure label"></span></td>
<td><span class="appyLabel" tal:condition="python: not innerRef and not appyType['link']"
tal:content="structure label"></span></td>
<tal:comment replace="nothing">If there is no object...</tal:comment>
<tal:noObject condition="not:objs">
@ -135,9 +137,9 @@
</tal:noObject>
<tal:comment replace="nothing">If there is an object...</tal:comment>
<tal:objectIsPresent condition="python: len(objs) == 1">
<tal:objectIsPresent condition="objs">
<tal:obj repeat="obj objs">
<td><metal:showObjectTitle use-macro="portal/skyn/widgets/ref/macros/objectTitle" /></td>
<td tal:define="includeShownInfo python:True"><metal:showObjectTitle use-macro="portal/skyn/widgets/ref/macros/objectTitle" /></td>
<td tal:condition="not: appyType/isBack">
<metal:showObjectActions use-macro="portal/skyn/widgets/ref/macros/objectActions" />
</td>
@ -240,31 +242,25 @@
</div>
<tal:comment replace="nothing">Edit macro for an Ref.</tal:comment>
<div define-macro="edit"
<div metal:define-macro="edit"
tal:condition="widget/link"
tal:define="refPortalType python: contextObj.getAppyRefPortalType(name);
allBrains python:here.uid_catalog(portal_type=refPortalType);
brains python:contextObj.callAppySelect(widget['select'], allBrains);
refUids python: [o.UID() for o in here.getAppyRefs(name)['objects']];
isMultiple python:test(widget['multiplicity'][1]!=1, 'multiple', '');
appyFieldName python: 'appy_ref_%s' % name;
inError python:test(errors.has_key(name), True, False);
isBeingCreated python: contextObj.isTemporary() or ('/portal_factory/' in contextObj.absolute_url())"
tal:attributes="class python:'appyRefEdit field' + test(inError, ' error', '')">
tal:define="rname python: 'appy_ref_%s' % name;
requestValue python: request.get(rname, []);
inRequest python: request.has_key(rname);
allObjects python: contextObj.getSelectableAppyRefs(name);
refUids python: [o.UID() for o in here.getAppyRefs(name)['objects']];
isBeingCreated python: contextObj.isTemporary() or ('/portal_factory/' in contextObj.absolute_url())">
<tal:comment replace="nothing">This macro displays the Reference widget on an "edit" page</tal:comment>
<label tal:attributes="for python:appyFieldName" tal:content="label"></label>&nbsp;
<span class="fieldRequired" tal:condition="python: appyType['multiplicity'][0]&gt;0"></span><br/>
<div tal:condition="inError" tal:content="python: errors[field.getName()]"></div>
<select tal:define="valueIsInReq python:test(request.get(appyFieldName, None) != None, True, False)"
tal:attributes="name python:'appy_ref_%s' % field.getName();
multiple isMultiple">
<option tal:condition="not: isMultiple" value="" i18n:translate="choose_a_value"/>
<option tal:repeat="brain brains"
tal:content="python: tool.getReferenceLabel(brain, appyType)"
tal:attributes="value brain/UID;
selected python:test((valueIsInReq and (brain.UID in request.get(appyFieldName, []))) or (not valueIsInReq and ((brain.UID in refUids) or (isBeingCreated and (brain.UID==defaultValueUID)))), True, False)"/>
<select tal:attributes="name rname;
multiple python: isMultiple and 'multiple' or ''">
<option tal:condition="not: isMultiple" i18n:translate="choose_a_value"></option>
<tal:ref repeat="refObj allObjects">
<option tal:define="uid python: contextObj.getReferenceUid(refObj)"
tal:content="python: contextObj.getReferenceLabel(name, refObj)"
tal:attributes="value uid;
selected python:(inRequest and (uid in requestValue) or (not inRequest and ((uid in refUids)))) and True or False">
</option>
</tal:ref>
</select>
</div>

View file

@ -59,7 +59,9 @@
requestValue python: request.get(name, None);
inRequest python: request.has_key(name);
errors errors | python: ();
inError python: test(widget['name'] in errors, True, False)">
inError python: (widget['name'] in errors) and True or False;
isMultiple python: (widget['multiplicity'][1] == None) or (widget['multiplicity'][1] &gt; 1)">
<metal:layout use-macro="here/skyn/widgets/show/macros/layout"/>
</metal:field>
@ -89,7 +91,7 @@
<tal:asTabs condition="python: widget['style'] == 'tabs'">
<table cellpadding="0" cellspacing="0" tal:attributes="width python: test(widget['wide'], '100%', '')">
<tal:comment replace="nothing">First row: the tabs.</tal:comment>
<tr><td style="border-bottom: 1px solid #ff8040">
<tr valign="middle"><td style="border-bottom: 1px solid #ff8040">
<table cellpadding="0" cellspacing="0" style="position:relative; bottom:-1px;">
<tr valign="bottom">
<tal:tab repeat="widgetRow widget/widgets">
@ -157,7 +159,7 @@
</th>
</tr>
<tal:comment replace="nothing">The rows of widgets</tal:comment>
<tr valign="top" tal:repeat="widgetRow widget/widgets">
<tr tal:attributes="valign widget/valign" tal:repeat="widgetRow widget/widgets">
<td tal:repeat="widget widgetRow"
tal:attributes="colspan widget/colspan|python:1;
style python: test(repeat['widget'].number() != len(widgetRow), 'padding-right: 0.6em', '')">
@ -176,7 +178,7 @@
<tal:comment replace="nothing">Displays a field label.</tal:comment>
<tal:label metal:define-macro="label" condition="widget/hasLabel">
<label tal:attributes="for widget/name"
tal:condition="python: widget['type'] not in ('Action', 'Ref')"
tal:condition="python: not ((widget['type'] == 'Action') or ((widget['type'] == 'Ref') and (widget['add'])))"
tal:content="structure python: contextObj.translate(widget['labelId'])"></label>
</tal:label>

View file

@ -1,14 +1,12 @@
<tal:comment replace="nothing">View macro for a String.</tal:comment>
<metal:view define-macro="view"
tal:define="fmt widget/format;
maxMult python: widget['multiplicity'][1];
severalValues python: (maxMult == None) or (maxMult &gt; 1)">
tal:define="fmt widget/format">
<span tal:condition="python: fmt in (0, 3)"
tal:attributes="class widget/master_css; id rawValue">
<ul class="appyList" tal:condition="python: value and severalValues">
<ul class="appyList" tal:condition="python: value and isMultiple">
<li class="appyBullet" tal:repeat="sv value"><i tal:content="structure sv"></i></li>
</ul>
<tal:singleValue condition="python: value and not severalValues">
<tal:singleValue condition="python: value and not isMultiple">
<span tal:condition="python: fmt != 3" tal:replace="structure value"/>
<span tal:condition="python: fmt == 3">********</span>
</tal:singleValue>

View file

@ -14,7 +14,6 @@ label { font-weight: bold; font-style: italic; }
.appyNav { padding: 0.4em 0 0.4em 0; }
.appyFocus { color: #900101; }
.appyTitle { padding-top: 0.5em; font-size: 110%; }
.appyRefEdit { line-height: 1.5em; }
.appyWorkflow { text-align: center; background-color: &dtml-globalBackgroundColor;;}
.appyPlusImg {