[gen] Security: added missing checks at the code level, ensuring that a user can create instances of a given class (root classes, or instances created via an initiator field); bugfixes in the test system, which works again (was broken after deplonization); [shared] XmlUnmarshaller can now be ran in 'non utf-8' mode: if enabled, any marshalled string will no be Python unicode, but simple str.

This commit is contained in:
Gaetan Delannay 2012-06-02 14:36:49 +02:00
parent 0d7afb685f
commit f843d5b7d6
11 changed files with 167 additions and 79 deletions

View file

@ -1990,9 +1990,8 @@ class Ref(Type):
res.select = None # Not callable from tool. res.select = None # Not callable from tool.
return res return res
def mayAdd(self, obj, folder): def mayAdd(self, obj):
'''May the user create a new referred object to p_obj via this Ref, '''May the user create a new referred object from p_obj via this Ref?'''
in p_folder?'''
# We can't (yet) do that on back references. # We can't (yet) do that on back references.
if self.isBack: return if self.isBack: return
# Check if this Ref is addable # Check if this Ref is addable
@ -2007,13 +2006,21 @@ class Ref(Type):
if refCount >= self.multiplicity[1]: return if refCount >= self.multiplicity[1]: return
# May the user edit this Ref field? # May the user edit this Ref field?
if not obj.allows(self.writePermission): return if not obj.allows(self.writePermission): return
# Have the user the correct add permission on p_folder? # Have the user the correct add permission?
tool = obj.getTool() tool = obj.getTool()
addPermission = '%s: Add %s' % (tool.getAppName(), addPermission = '%s: Add %s' % (tool.getAppName(),
tool.getPortalType(self.klass)) tool.getPortalType(self.klass))
folder = obj.getCreateFolder()
if not obj.getUser().has_permission(addPermission, folder): return if not obj.getUser().has_permission(addPermission, folder): return
return True return True
def checkAdd(self, obj):
'''Compute m_mayAdd above, and raise an Unauthorized exception if
m_mayAdd returns False.'''
if not self.mayAdd(obj):
from AccessControl import Unauthorized
raise Unauthorized("User can't write Ref field '%s'." % self.name)
class Computed(Type): class Computed(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None, def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show='view', default=None, optional=False, editDefault=False, show='view',

View file

@ -214,9 +214,15 @@ class ZopeInstaller:
zopeContent = self.app.objectIds() zopeContent = self.app.objectIds()
from OFS.Folder import manage_addFolder from OFS.Folder import manage_addFolder
if 'config' not in zopeContent:
toolName = '%sTool' % self.productName
createObject(self.app, 'config', toolName, self.productName,
wf=False, noSecurity=True)
if 'data' not in zopeContent: if 'data' not in zopeContent:
manage_addFolder(self.app, 'data') manage_addFolder(self.app, 'data')
data = self.app.data data = self.app.data
tool = self.app.config
# Manager has been granted Add permissions for all root classes. # Manager has been granted Add permissions for all root classes.
# This may not be desired, so remove this. # This may not be desired, so remove this.
for className in self.config.rootClasses: for className in self.config.rootClasses:
@ -230,15 +236,12 @@ class ZopeInstaller:
if not klass.__dict__.has_key('root') or \ if not klass.__dict__.has_key('root') or \
not klass.__dict__['root']: not klass.__dict__['root']:
continue # It is not a root class continue # It is not a root class
creators = getattr(klass, 'creators', None)
if not creators: creators = self.config.defaultAddRoles
className = self.config.appClassNames[i] className = self.config.appClassNames[i]
wrapperClass = tool.getAppyClass(className, wrapper=True)
creators = wrapperClass.getCreators(self.config)
permission = self.getAddPermission(className) permission = self.getAddPermission(className)
updateRolesForPermission(permission, tuple(creators), data) updateRolesForPermission(permission, tuple(creators), data)
if 'config' not in zopeContent:
toolName = '%sTool' % self.productName
createObject(self.app, 'config', toolName,self.productName,wf=False)
# Remove some default objects created by Zope but not useful to Appy # Remove some default objects created by Zope but not useful to Appy
for name in ('standard_html_footer', 'standard_html_header',\ for name in ('standard_html_footer', 'standard_html_header',\
'standard_template.pt'): 'standard_template.pt'):
@ -261,15 +264,15 @@ class ZopeInstaller:
# may still be in the way for migration purposes. # may still be in the way for migration purposes.
users = ('admin',) # We suppose there is at least a user. users = ('admin',) # We suppose there is at least a user.
if not users: if not users:
appyTool.create('users', login='admin', password1='admin', appyTool.create('users', noSecurity=True, login='admin',
password2='admin', password1='admin', password2='admin',
email='admin@appyframework.org', roles=['Manager']) email='admin@appyframework.org', roles=['Manager'])
appyTool.log('Admin user "admin" created.') appyTool.log('Admin user "admin" created.')
# Create group "admins" if it does not exist # Create group "admins" if it does not exist
if not appyTool.count('Group', noSecurity=True, login='admins'): if not appyTool.count('Group', noSecurity=True, login='admins'):
appyTool.create('groups', login='admins', title='Administrators', appyTool.create('groups', noSecurity=True, login='admins',
roles=['Manager']) title='Administrators', roles=['Manager'])
appyTool.log('Group "admins" created.') appyTool.log('Group "admins" created.')
# Create a group for every global role defined in the application # Create a group for every global role defined in the application
@ -277,8 +280,8 @@ class ZopeInstaller:
relatedGroup = '%s_group' % role relatedGroup = '%s_group' % role
if appyTool.count('Group', noSecurity=True, login=relatedGroup): if appyTool.count('Group', noSecurity=True, login=relatedGroup):
continue continue
appyTool.create('groups', login=relatedGroup, title=relatedGroup, appyTool.create('groups', noSecurity=True, login=relatedGroup,
roles=[role]) title=relatedGroup, roles=[role])
appyTool.log('Group "%s", related to global role "%s", was ' \ appyTool.log('Group "%s", related to global role "%s", was ' \
'created.' % (relatedGroup, role)) 'created.' % (relatedGroup, role))
@ -320,7 +323,8 @@ class ZopeInstaller:
title = '%s (%s)' % (langEn, langNat) title = '%s (%s)' % (langEn, langNat)
else: else:
title = langEn title = langEn
appyTool.create('translations', id=language, title=title) appyTool.create('translations', noSecurity=True,
id=language, title=title)
appyTool.log('Translation object created for "%s".' % language) appyTool.log('Translation object created for "%s".' % language)
# Now, we synchronise every Translation object with the corresponding # Now, we synchronise every Translation object with the corresponding
# "po" file on disk. # "po" file on disk.

View file

@ -1,14 +1,14 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import os, os.path, sys import os, os.path, sys
try:
from AccessControl.SecurityManagement import \
newSecurityManager, noSecurityManager
except ImportError:
pass
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class TestMixin: class TestMixin:
'''This class is mixed in with any ZopeTestCase.''' '''This class is mixed in with any ZopeTestCase.'''
def createUser(self, userId, roles):
'''Creates a user with id p_userId with some p_roles.'''
self.acl_users.addMember(userId, 'password', [], [])
self.setRoles(roles, name=userId)
def changeUser(self, userId): def changeUser(self, userId):
'''Logs out currently logged user and logs in p_loginName.''' '''Logs out currently logged user and logs in p_loginName.'''
self.logout() self.logout()
@ -55,6 +55,16 @@ class TestMixin:
return arg[10:].strip(']') return arg[10:].strip(']')
return None return None
def login(self, name='admin'):
user = self.app.acl_users.getUserById(name)
newSecurityManager(None, user)
def logout(self):
'''Logs out.'''
noSecurityManager()
def _setup(self): pass
# Functions executed before and after every test ------------------------------- # Functions executed before and after every test -------------------------------
def beforeTest(test): def beforeTest(test):
'''Is executed before every test.''' '''Is executed before every test.'''
@ -64,7 +74,6 @@ def beforeTest(test):
g['appFolder'] = cfg.diskFolder g['appFolder'] = cfg.diskFolder
moduleOrClassName = g['test'].name # Not used yet. moduleOrClassName = g['test'].name # Not used yet.
# Initialize the test # Initialize the test
test.createUser('admin', ('Member','Manager'))
test.login('admin') test.login('admin')
g['t'] = g['test'] g['t'] = g['test']

View file

@ -898,8 +898,13 @@ class ToolMixin(BaseMixin):
userId = self.getUser().getId() userId = self.getUser().getId()
# Perform the logout in acl_users # Perform the logout in acl_users
rq.RESPONSE.expireCookie('__ac', path='/') rq.RESPONSE.expireCookie('__ac', path='/')
# Invalidate existing sessions. # Invalidate session.
try:
sdm = self.session_data_manager sdm = self.session_data_manager
except AttributeError, ae:
# When ran in test mode, session_data_manager is not there.
sdm = None
if sdm:
session = sdm.getSessionData(create=0) session = sdm.getSessionData(create=0)
if session is not None: if session is not None:
session.invalidate() session.invalidate()

View file

@ -25,6 +25,20 @@ class BaseMixin:
return self return self
o = property(get_o) o = property(get_o)
def getInitiatorInfo(self):
'''Gets information about a potential initiator object from the request.
Returns a 3-tuple (initiator, pageName, field):
* initiator is the initiator (Zope) object;
* pageName is the page on the initiator where the origin of the Ref
field lies;
* field is the Ref instance.
'''
if not rq.get('nav', '').startswith('ref.'): return (None,)*4
splitted = rq['nav'].split('.')
initiator = self.tool.getObject(splitted[1])
fieldName, page = splitted[2].split(':')
return (initiator, page, self.getAppyType(fieldName))
def createOrUpdate(self, created, values, def createOrUpdate(self, created, values,
initiator=None, initiatorField=None): initiator=None, initiatorField=None):
'''This method creates (if p_created is True) or updates an object. '''This method creates (if p_created is True) or updates an object.
@ -43,10 +57,9 @@ class BaseMixin:
if not initiator: if not initiator:
folder = tool.getPath('/data') folder = tool.getPath('/data')
else: else:
if initiator.isPrincipiaFolderish: folder = initiator.getCreateFolder()
folder = initiator # Check that the user can add objects through this Ref.
else: initiatorField.checkAdd(initiator)
folder = initiator.getParentNode()
obj = createObject(folder, id, obj.portal_type, tool.getAppName()) obj = createObject(folder, id, obj.portal_type, tool.getAppName())
previousData = None previousData = None
if not created: previousData = obj.rememberPreviousData() if not created: previousData = obj.rememberPreviousData()
@ -61,7 +74,7 @@ class BaseMixin:
obj.historizeData(previousData) obj.historizeData(previousData)
# Manage potential link with an initiator object # Manage potential link with an initiator object
if created and initiator: initiator.appy().link(initiatorField, obj) if created and initiator: initiator.appy().link(initiatorField.name,obj)
# Manage "add" permissions and reindex the object # Manage "add" permissions and reindex the object
obj._appy_managePermissions() obj._appy_managePermissions()
@ -112,13 +125,16 @@ class BaseMixin:
# Create the params to add to the URL we will redirect the user to # Create the params to add to the URL we will redirect the user to
# create the object. # create the object.
urlParams = {'mode':'edit', 'page':'main', 'nav':''} urlParams = {'mode':'edit', 'page':'main', 'nav':''}
if rq.get('nav', None): initiator, initiatorPage, initiatorField = self.getInitiatorInfo()
if initiator:
# The object to create will be linked to an initiator object through # The object to create will be linked to an initiator object through
# a ref field. We create here a new navigation string with one more # a Ref field. We create here a new navigation string with one more
# item, that will be the currently created item. # item, that will be the currently created item.
splitted = rq.get('nav').split('.') splitted = rq.get('nav').split('.')
splitted[-1] = splitted[-2] = str(int(splitted[-1])+1) splitted[-1] = splitted[-2] = str(int(splitted[-1])+1)
urlParams['nav'] = '.'.join(splitted) urlParams['nav'] = '.'.join(splitted)
# Check that the user can add objects through this Ref field
initiatiorField.checkAdd(initiator)
# Create a temp object in /temp_folder # Create a temp object in /temp_folder
tool = self.getTool() tool = self.getTool()
id = tool.generateUid(className) id = tool.generateUid(className)
@ -188,13 +204,7 @@ class BaseMixin:
errorMessage = 'Please correct the indicated errors.' # XXX Translate errorMessage = 'Please correct the indicated errors.' # XXX Translate
isNew = rq.get('is_new') == 'True' isNew = rq.get('is_new') == 'True'
# If this object is created from an initiator, get info about him. # If this object is created from an initiator, get info about him.
initiator = None initiator, initiatorPage, initiatorField = self.getInitiatorInfo()
initiatorPage = None
initiatorField = None
if rq.get('nav', '').startswith('ref.'):
splitted = rq['nav'].split('.')
initiator = tool.getObject(splitted[1])
initiatorField, initiatorPage = splitted[2].split(':')
# If the user clicked on 'Cancel', go back to the previous page. # If the user clicked on 'Cancel', go back to the previous page.
if rq.get('buttonCancel.x', None): if rq.get('buttonCancel.x', None):
if initiator: if initiator:
@ -434,6 +444,14 @@ class BaseMixin:
# broken on returned object. # broken on returned object.
return getattr(self, methodName, None) return getattr(self, methodName, None)
def getCreateFolder(self):
'''When an object must be created from this one through a Ref field, we
must know where to put the newly create object: within this one if it
is folderish, besides this one in its parent else.
'''
if self.isPrincipiaFolderish: return self
return self.getParentNode()
def getFieldValue(self, name, onlyIfSync=False, layoutType=None, def getFieldValue(self, name, onlyIfSync=False, layoutType=None,
outerValue=None): outerValue=None):
'''Returns the database value of field named p_name for p_self. '''Returns the database value of field named p_name for p_self.
@ -534,10 +552,10 @@ class BaseMixin:
if not refs: raise IndexError() if not refs: raise IndexError()
return refs.index(obj.UID()) return refs.index(obj.UID())
def mayAddReference(self, name, folder): def mayAddReference(self, name):
'''May the user add references via Ref field named p_name in '''May the user add references via Ref field named p_name in
p_folder?''' p_folder?'''
return self.getAppyType(name).mayAdd(self, folder) return self.getAppyType(name).mayAdd(self)
def isDebug(self): def isDebug(self):
'''Are we in debug mode ?''' '''Are we in debug mode ?'''
@ -1227,9 +1245,7 @@ class BaseMixin:
Ref fields; if it is not a folder, we must update permissions on its Ref fields; if it is not a folder, we must update permissions on its
parent folder instead.''' parent folder instead.'''
# Determine on which folder we need to set "add" permissions # Determine on which folder we need to set "add" permissions
folder = self folder = self.getCreateFolder()
if not self.isPrincipiaFolderish:
folder = self.getParentNode()
# On this folder, set "add" permissions for every content type that will # On this folder, set "add" permissions for every content type that will
# be created through reference fields # be created through reference fields
allCreators = {} # One key for every add permission allCreators = {} # One key for every add permission
@ -1238,12 +1254,12 @@ class BaseMixin:
if appyType.type != 'Ref': continue if appyType.type != 'Ref': continue
if appyType.isBack or appyType.link: continue if appyType.isBack or appyType.link: continue
# Indeed, no possibility to create objects with such Refs # Indeed, no possibility to create objects with such Refs
refType = self.getTool().getPortalType(appyType.klass) tool = self.getTool()
refType = tool.getPortalType(appyType.klass)
if refType not in addPermissions: continue if refType not in addPermissions: continue
# Get roles that may add this content type # Get roles that may add this content type
creators = getattr(appyType.klass, 'creators', None) appyWrapper = tool.getAppyClass(refType, wrapper=True)
if not creators: creators = appyWrapper.getCreators(self.getProductConfig())
creators = self.getProductConfig().defaultAddRoles
# Add those creators to the list of creators for this meta_type # Add those creators to the list of creators for this meta_type
addPermission = addPermissions[refType] addPermission = addPermissions[refType]
if addPermission in allCreators: if addPermission in allCreators:

View file

@ -9,7 +9,7 @@ from appy.gen.mixins.TestMixin import TestMixin, beforeTest, afterTest
# Initialize the Zope test system ---------------------------------------------- # Initialize the Zope test system ----------------------------------------------
ZopeTestCase.installProduct('<!applicationName!>') ZopeTestCase.installProduct('<!applicationName!>')
class Test(ZopeTestCase.ZopeTestCase, TestMixin): class Test(TestMixin, ZopeTestCase.ZopeTestCase):
'''Base test class for <!applicationName!> test cases.''' '''Base test class for <!applicationName!> test cases.'''
# Data needed for defining the tests ------------------------------------------- # Data needed for defining the tests -------------------------------------------

View file

@ -294,7 +294,8 @@ function initSlaves() {
while (i >= 0) { while (i >= 0) {
masterName = getSlaveInfo(slaves[i], 'masterName'); masterName = getSlaveInfo(slaves[i], 'masterName');
master = document.getElementById(masterName); master = document.getElementById(masterName);
updateSlaves(master, slaves[i]); // If master is not here, we can't hide its slaves when appropriate.
if (master) updateSlaves(master, slaves[i]);
i -= 1; i -= 1;
} }
} }

View file

@ -124,10 +124,10 @@
objs refObjects/objects; objs refObjects/objects;
totalNumber refObjects/totalNumber; totalNumber refObjects/totalNumber;
batchSize refObjects/batchSize; batchSize refObjects/batchSize;
folder python: contextObj.isPrincipiaFolderish and contextObj or contextObj.getParentNode(); folder contextObj/getCreateFolder;
linkedPortalType python: tool.getPortalType(appyType['klass']); linkedPortalType python: tool.getPortalType(appyType['klass']);
canWrite python: not appyType['isBack'] and contextObj.allows(appyType['writePermission']); canWrite python: not appyType['isBack'] and contextObj.allows(appyType['writePermission']);
showPlusIcon python: contextObj.mayAddReference(fieldName, folder); showPlusIcon python: contextObj.mayAddReference(fieldName);
atMostOneRef python: (appyType['multiplicity'][1] == 1) and (len(objs)&lt;=1); atMostOneRef python: (appyType['multiplicity'][1] == 1) and (len(objs)&lt;=1);
addConfirmMsg python: appyType['addConfirm'] and _('%s_addConfirm' % appyType['labelId']) or ''; addConfirmMsg python: appyType['addConfirm'] and _('%s_addConfirm' % appyType['labelId']) or '';
navBaseCall python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url(), fieldName, innerRef)"> navBaseCall python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url(), fieldName, innerRef)">

View file

@ -2,7 +2,7 @@
import re, os, os.path import re, os, os.path
# Function for creating a Zope object ------------------------------------------ # Function for creating a Zope object ------------------------------------------
def createObject(folder, id, className, appName, wf=True): def createObject(folder, id, className, appName, wf=True, noSecurity=False):
'''Creates, in p_folder, object with some p_id. Object will be an instance '''Creates, in p_folder, object with some p_id. Object will be an instance
of p_className from application p_appName. In a very special case (the of p_className from application p_appName. In a very special case (the
creation of the config object), computing workflow-related info is not creation of the config object), computing workflow-related info is not
@ -10,6 +10,24 @@ def createObject(folder, id, className, appName, wf=True):
p_wf=False.''' p_wf=False.'''
exec 'from Products.%s.%s import %s as ZopeClass' % (appName, className, exec 'from Products.%s.%s import %s as ZopeClass' % (appName, className,
className) className)
if not noSecurity:
# Check that the user can create objects of className
if folder.meta_type.endswith('Folder'): # Folder or temp folder.
tool = folder.config
else:
tool = folder.getTool()
user = tool.getUser()
userRoles = user.getRoles()
allowedRoles=ZopeClass.wrapperClass.getCreators(tool.getProductConfig())
allowed = False
for role in userRoles:
if role in allowedRoles:
allowed = True
break
if not allowed:
from AccessControl import Unauthorized
raise Unauthorized("User can't create instances of %s" % \
ZopeClass.__name__)
obj = ZopeClass(id) obj = ZopeClass(id)
folder._objects = folder._objects + \ folder._objects = folder._objects + \
({'id':id, 'meta_type':className},) ({'id':id, 'meta_type':className},)

View file

@ -22,8 +22,46 @@ FREEZE_FATAL_ERROR = 'A server error occurred. Please contact the system ' \
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class AbstractWrapper(object): class AbstractWrapper(object):
'''Any real Zope object has a companion object that is an instance of this '''Any real Appy-managed Zope object has a companion object that is an
class.''' instance of this class.'''
# --------------------------------------------------------------------------
# Class methods
# --------------------------------------------------------------------------
@classmethod
def _getParentAttr(klass, attr):
'''Gets value of p_attr on p_klass base classes (if this attr exists).
Scan base classes in the reverse order as Python does. Used by
classmethods m_getWorkflow and m_getCreators below. Scanning base
classes in reverse order allows user-defined elements to override
default Appy elements.'''
i = len(klass.__bases__) - 1
res = None
while i >= 0:
res = getattr(klass.__bases__[i], attr, None)
if res: return res
i -= 1
@classmethod
def getWorkflow(klass):
'''Returns the workflow tied to p_klass.'''
res = klass._getParentAttr('workflow')
# Return a default workflow if no workflow was found.
if not res: res = WorkflowAnonymous
return res
@classmethod
def getCreators(klass, cfg):
'''Returns the roles that are allowed to create instances of p_klass.
p_cfg is the product config that holds the default value.'''
res = klass._getParentAttr('creators')
# Return default creators if no creators was found.
if not res: res = cfg.defaultAddRoles
return res
# --------------------------------------------------------------------------
# Instance methods
# --------------------------------------------------------------------------
def __init__(self, o): self.__dict__['o'] = o def __init__(self, o): self.__dict__['o'] = o
def appy(self): return self def appy(self): return self
@ -87,21 +125,6 @@ class AbstractWrapper(object):
return customUser.__dict__[methodName](self, *args, **kwargs) return customUser.__dict__[methodName](self, *args, **kwargs)
def getField(self, name): return self.o.getAppyType(name) def getField(self, name): return self.o.getAppyType(name)
@classmethod
def getWorkflow(klass):
'''Returns the workflow tied to p_klass.'''
# Browse parent classes of p_klass in reverse order. This way, a
# user-defined workflow will override a Appy default workflow.
i = len(klass.__bases__)-1
res = None
while i >= 0:
res = getattr(klass.__bases__[i], 'workflow', None)
if res: break
i -= 1
# Return a default workflow if no workflow was found.
if not res:
res = WorkflowAnonymous
return res
def link(self, fieldName, obj): def link(self, fieldName, obj):
'''This method links p_obj (which can be a list of objects) to this one '''This method links p_obj (which can be a list of objects) to this one
@ -124,7 +147,7 @@ class AbstractWrapper(object):
getattr(tool.getObject(y), sortKey))) getattr(tool.getObject(y), sortKey)))
if reverse: refs.reverse() if reverse: refs.reverse()
def create(self, fieldNameOrClass, **kwargs): def create(self, fieldNameOrClass, noSecurity=False, **kwargs):
'''If p_fieldNameOrClass is the name of a field, this method allows to '''If p_fieldNameOrClass is the name of a field, this method allows to
create an object and link it to the current one (self) through create an object and link it to the current one (self) through
reference field named p_fieldName. reference field named p_fieldName.
@ -156,12 +179,13 @@ class AbstractWrapper(object):
if not isField: if not isField:
folder = tool.getPath('/data') folder = tool.getPath('/data')
else: else:
if hasattr(self, 'folder') and self.folder: folder = self.o.getCreateFolder()
folder = self.o if not noSecurity:
else: # Check that the user can edit this field.
folder = self.o.getParentNode() appyType.checkAdd(self.o)
# Create the object # Create the object
zopeObj = createObject(folder, objId,portalType, tool.getAppName()) zopeObj = createObject(folder, objId, portalType, tool.getAppName(),
noSecurity=noSecurity)
appyObj = zopeObj.appy() appyObj = zopeObj.appy()
# Set object attributes # Set object attributes
for attrName, attrValue in kwargs.iteritems(): for attrName, attrValue in kwargs.iteritems():

View file

@ -216,7 +216,8 @@ class XmlUnmarshaller(XmlParser):
If "object" is specified, it means that the tag contains sub-tags, each If "object" is specified, it means that the tag contains sub-tags, each
one corresponding to the value of an attribute for this object. one corresponding to the value of an attribute for this object.
if "tuple" is specified, it will be converted to a list.''' if "tuple" is specified, it will be converted to a list.'''
def __init__(self, classes={}, tagTypes={}, conversionFunctions={}): def __init__(self, classes={}, tagTypes={}, conversionFunctions={},
utf8=True):
XmlParser.__init__(self) XmlParser.__init__(self)
# self.classes below is a dict whose keys are tag names and values are # self.classes below is a dict whose keys are tag names and values are
# Python classes. During the unmarshalling process, when an object is # Python classes. During the unmarshalling process, when an object is
@ -253,6 +254,7 @@ class XmlUnmarshaller(XmlParser):
# for example convert strings that have specific values (in this case, # for example convert strings that have specific values (in this case,
# knowing that the value is a 'string' is not sufficient). # knowing that the value is a 'string' is not sufficient).
self.conversionFunctions = conversionFunctions self.conversionFunctions = conversionFunctions
self.utf8 = utf8
def convertAttrs(self, attrs): def convertAttrs(self, attrs):
'''Converts XML attrs to a dict.''' '''Converts XML attrs to a dict.'''
@ -360,6 +362,8 @@ class XmlUnmarshaller(XmlParser):
setattr(currentContainer, name, attrValue) setattr(currentContainer, name, attrValue)
def characters(self, content): def characters(self, content):
if not self.utf8:
content = content.encode('utf-8')
e = XmlParser.characters(self, content) e = XmlParser.characters(self, content)
if e.currentBasicType: if e.currentBasicType:
e.currentContent += content e.currentContent += content