[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:
parent
0d7afb685f
commit
f843d5b7d6
|
@ -1990,9 +1990,8 @@ class Ref(Type):
|
|||
res.select = None # Not callable from tool.
|
||||
return res
|
||||
|
||||
def mayAdd(self, obj, folder):
|
||||
'''May the user create a new referred object to p_obj via this Ref,
|
||||
in p_folder?'''
|
||||
def mayAdd(self, obj):
|
||||
'''May the user create a new referred object from p_obj via this Ref?'''
|
||||
# We can't (yet) do that on back references.
|
||||
if self.isBack: return
|
||||
# Check if this Ref is addable
|
||||
|
@ -2007,13 +2006,21 @@ class Ref(Type):
|
|||
if refCount >= self.multiplicity[1]: return
|
||||
# May the user edit this Ref field?
|
||||
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()
|
||||
addPermission = '%s: Add %s' % (tool.getAppName(),
|
||||
tool.getPortalType(self.klass))
|
||||
folder = obj.getCreateFolder()
|
||||
if not obj.getUser().has_permission(addPermission, folder): return
|
||||
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):
|
||||
def __init__(self, validator=None, multiplicity=(0,1), index=None,
|
||||
default=None, optional=False, editDefault=False, show='view',
|
||||
|
|
|
@ -214,9 +214,15 @@ class ZopeInstaller:
|
|||
zopeContent = self.app.objectIds()
|
||||
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:
|
||||
manage_addFolder(self.app, 'data')
|
||||
data = self.app.data
|
||||
tool = self.app.config
|
||||
# Manager has been granted Add permissions for all root classes.
|
||||
# This may not be desired, so remove this.
|
||||
for className in self.config.rootClasses:
|
||||
|
@ -230,15 +236,12 @@ class ZopeInstaller:
|
|||
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.config.defaultAddRoles
|
||||
className = self.config.appClassNames[i]
|
||||
wrapperClass = tool.getAppyClass(className, wrapper=True)
|
||||
creators = wrapperClass.getCreators(self.config)
|
||||
permission = self.getAddPermission(className)
|
||||
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
|
||||
for name in ('standard_html_footer', 'standard_html_header',\
|
||||
'standard_template.pt'):
|
||||
|
@ -261,15 +264,15 @@ class ZopeInstaller:
|
|||
# may still be in the way for migration purposes.
|
||||
users = ('admin',) # We suppose there is at least a user.
|
||||
if not users:
|
||||
appyTool.create('users', login='admin', password1='admin',
|
||||
password2='admin',
|
||||
appyTool.create('users', noSecurity=True, login='admin',
|
||||
password1='admin', password2='admin',
|
||||
email='admin@appyframework.org', roles=['Manager'])
|
||||
appyTool.log('Admin user "admin" created.')
|
||||
|
||||
# Create group "admins" if it does not exist
|
||||
if not appyTool.count('Group', noSecurity=True, login='admins'):
|
||||
appyTool.create('groups', login='admins', title='Administrators',
|
||||
roles=['Manager'])
|
||||
appyTool.create('groups', noSecurity=True, login='admins',
|
||||
title='Administrators', roles=['Manager'])
|
||||
appyTool.log('Group "admins" created.')
|
||||
|
||||
# Create a group for every global role defined in the application
|
||||
|
@ -277,8 +280,8 @@ class ZopeInstaller:
|
|||
relatedGroup = '%s_group' % role
|
||||
if appyTool.count('Group', noSecurity=True, login=relatedGroup):
|
||||
continue
|
||||
appyTool.create('groups', login=relatedGroup, title=relatedGroup,
|
||||
roles=[role])
|
||||
appyTool.create('groups', noSecurity=True, login=relatedGroup,
|
||||
title=relatedGroup, roles=[role])
|
||||
appyTool.log('Group "%s", related to global role "%s", was ' \
|
||||
'created.' % (relatedGroup, role))
|
||||
|
||||
|
@ -320,7 +323,8 @@ class ZopeInstaller:
|
|||
title = '%s (%s)' % (langEn, langNat)
|
||||
else:
|
||||
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)
|
||||
# Now, we synchronise every Translation object with the corresponding
|
||||
# "po" file on disk.
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import os, os.path, sys
|
||||
try:
|
||||
from AccessControl.SecurityManagement import \
|
||||
newSecurityManager, noSecurityManager
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class TestMixin:
|
||||
'''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):
|
||||
'''Logs out currently logged user and logs in p_loginName.'''
|
||||
self.logout()
|
||||
|
@ -55,6 +55,16 @@ class TestMixin:
|
|||
return arg[10:].strip(']')
|
||||
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 -------------------------------
|
||||
def beforeTest(test):
|
||||
'''Is executed before every test.'''
|
||||
|
@ -64,7 +74,6 @@ def beforeTest(test):
|
|||
g['appFolder'] = cfg.diskFolder
|
||||
moduleOrClassName = g['test'].name # Not used yet.
|
||||
# Initialize the test
|
||||
test.createUser('admin', ('Member','Manager'))
|
||||
test.login('admin')
|
||||
g['t'] = g['test']
|
||||
|
||||
|
|
|
@ -898,11 +898,16 @@ class ToolMixin(BaseMixin):
|
|||
userId = self.getUser().getId()
|
||||
# Perform the logout in acl_users
|
||||
rq.RESPONSE.expireCookie('__ac', path='/')
|
||||
# Invalidate existing sessions.
|
||||
sdm = self.session_data_manager
|
||||
session = sdm.getSessionData(create=0)
|
||||
if session is not None:
|
||||
session.invalidate()
|
||||
# Invalidate session.
|
||||
try:
|
||||
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)
|
||||
if session is not None:
|
||||
session.invalidate()
|
||||
self.log('User "%s" has been logged out.' % userId)
|
||||
# Remove user from variable "loggedUsers"
|
||||
from appy.gen.installer import loggedUsers
|
||||
|
|
|
@ -25,6 +25,20 @@ class BaseMixin:
|
|||
return self
|
||||
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,
|
||||
initiator=None, initiatorField=None):
|
||||
'''This method creates (if p_created is True) or updates an object.
|
||||
|
@ -43,10 +57,9 @@ class BaseMixin:
|
|||
if not initiator:
|
||||
folder = tool.getPath('/data')
|
||||
else:
|
||||
if initiator.isPrincipiaFolderish:
|
||||
folder = initiator
|
||||
else:
|
||||
folder = initiator.getParentNode()
|
||||
folder = initiator.getCreateFolder()
|
||||
# Check that the user can add objects through this Ref.
|
||||
initiatorField.checkAdd(initiator)
|
||||
obj = createObject(folder, id, obj.portal_type, tool.getAppName())
|
||||
previousData = None
|
||||
if not created: previousData = obj.rememberPreviousData()
|
||||
|
@ -61,7 +74,7 @@ class BaseMixin:
|
|||
obj.historizeData(previousData)
|
||||
|
||||
# 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
|
||||
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 object.
|
||||
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
|
||||
# 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.
|
||||
splitted = rq.get('nav').split('.')
|
||||
splitted[-1] = splitted[-2] = str(int(splitted[-1])+1)
|
||||
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
|
||||
tool = self.getTool()
|
||||
id = tool.generateUid(className)
|
||||
|
@ -188,13 +204,7 @@ class BaseMixin:
|
|||
errorMessage = 'Please correct the indicated errors.' # XXX Translate
|
||||
isNew = rq.get('is_new') == 'True'
|
||||
# If this object is created from an initiator, get info about him.
|
||||
initiator = None
|
||||
initiatorPage = None
|
||||
initiatorField = None
|
||||
if rq.get('nav', '').startswith('ref.'):
|
||||
splitted = rq['nav'].split('.')
|
||||
initiator = tool.getObject(splitted[1])
|
||||
initiatorField, initiatorPage = splitted[2].split(':')
|
||||
initiator, initiatorPage, initiatorField = self.getInitiatorInfo()
|
||||
# If the user clicked on 'Cancel', go back to the previous page.
|
||||
if rq.get('buttonCancel.x', None):
|
||||
if initiator:
|
||||
|
@ -434,6 +444,14 @@ class BaseMixin:
|
|||
# broken on returned object.
|
||||
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,
|
||||
outerValue=None):
|
||||
'''Returns the database value of field named p_name for p_self.
|
||||
|
@ -534,10 +552,10 @@ class BaseMixin:
|
|||
if not refs: raise IndexError()
|
||||
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
|
||||
p_folder?'''
|
||||
return self.getAppyType(name).mayAdd(self, folder)
|
||||
return self.getAppyType(name).mayAdd(self)
|
||||
|
||||
def isDebug(self):
|
||||
'''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
|
||||
parent folder instead.'''
|
||||
# Determine on which folder we need to set "add" permissions
|
||||
folder = self
|
||||
if not self.isPrincipiaFolderish:
|
||||
folder = self.getParentNode()
|
||||
folder = self.getCreateFolder()
|
||||
# On this folder, set "add" permissions for every content type that will
|
||||
# be created through reference fields
|
||||
allCreators = {} # One key for every add permission
|
||||
|
@ -1238,12 +1254,12 @@ class BaseMixin:
|
|||
if appyType.type != 'Ref': continue
|
||||
if appyType.isBack or appyType.link: continue
|
||||
# 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
|
||||
# Get roles that may add this content type
|
||||
creators = getattr(appyType.klass, 'creators', None)
|
||||
if not creators:
|
||||
creators = self.getProductConfig().defaultAddRoles
|
||||
appyWrapper = tool.getAppyClass(refType, wrapper=True)
|
||||
creators = appyWrapper.getCreators(self.getProductConfig())
|
||||
# Add those creators to the list of creators for this meta_type
|
||||
addPermission = addPermissions[refType]
|
||||
if addPermission in allCreators:
|
||||
|
|
|
@ -9,7 +9,7 @@ from appy.gen.mixins.TestMixin import TestMixin, beforeTest, afterTest
|
|||
# Initialize the Zope test system ----------------------------------------------
|
||||
ZopeTestCase.installProduct('<!applicationName!>')
|
||||
|
||||
class Test(ZopeTestCase.ZopeTestCase, TestMixin):
|
||||
class Test(TestMixin, ZopeTestCase.ZopeTestCase):
|
||||
'''Base test class for <!applicationName!> test cases.'''
|
||||
|
||||
# Data needed for defining the tests -------------------------------------------
|
||||
|
|
|
@ -294,7 +294,8 @@ function initSlaves() {
|
|||
while (i >= 0) {
|
||||
masterName = getSlaveInfo(slaves[i], '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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -124,10 +124,10 @@
|
|||
objs refObjects/objects;
|
||||
totalNumber refObjects/totalNumber;
|
||||
batchSize refObjects/batchSize;
|
||||
folder python: contextObj.isPrincipiaFolderish and contextObj or contextObj.getParentNode();
|
||||
folder contextObj/getCreateFolder;
|
||||
linkedPortalType python: tool.getPortalType(appyType['klass']);
|
||||
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)<=1);
|
||||
addConfirmMsg python: appyType['addConfirm'] and _('%s_addConfirm' % appyType['labelId']) or '';
|
||||
navBaseCall python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url(), fieldName, innerRef)">
|
||||
|
|
20
gen/utils.py
20
gen/utils.py
|
@ -2,7 +2,7 @@
|
|||
import re, os, os.path
|
||||
|
||||
# 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
|
||||
of p_className from application p_appName. In a very special case (the
|
||||
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.'''
|
||||
exec 'from Products.%s.%s import %s as ZopeClass' % (appName, 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)
|
||||
folder._objects = folder._objects + \
|
||||
({'id':id, 'meta_type':className},)
|
||||
|
|
|
@ -22,8 +22,46 @@ FREEZE_FATAL_ERROR = 'A server error occurred. Please contact the system ' \
|
|||
|
||||
# ------------------------------------------------------------------------------
|
||||
class AbstractWrapper(object):
|
||||
'''Any real Zope object has a companion object that is an instance of this
|
||||
class.'''
|
||||
'''Any real Appy-managed Zope object has a companion object that is an
|
||||
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 appy(self): return self
|
||||
|
||||
|
@ -87,21 +125,6 @@ class AbstractWrapper(object):
|
|||
return customUser.__dict__[methodName](self, *args, **kwargs)
|
||||
|
||||
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):
|
||||
'''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)))
|
||||
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
|
||||
create an object and link it to the current one (self) through
|
||||
reference field named p_fieldName.
|
||||
|
@ -156,12 +179,13 @@ class AbstractWrapper(object):
|
|||
if not isField:
|
||||
folder = tool.getPath('/data')
|
||||
else:
|
||||
if hasattr(self, 'folder') and self.folder:
|
||||
folder = self.o
|
||||
else:
|
||||
folder = self.o.getParentNode()
|
||||
folder = self.o.getCreateFolder()
|
||||
if not noSecurity:
|
||||
# Check that the user can edit this field.
|
||||
appyType.checkAdd(self.o)
|
||||
# Create the object
|
||||
zopeObj = createObject(folder, objId,portalType, tool.getAppName())
|
||||
zopeObj = createObject(folder, objId, portalType, tool.getAppName(),
|
||||
noSecurity=noSecurity)
|
||||
appyObj = zopeObj.appy()
|
||||
# Set object attributes
|
||||
for attrName, attrValue in kwargs.iteritems():
|
||||
|
|
|
@ -216,7 +216,8 @@ class XmlUnmarshaller(XmlParser):
|
|||
If "object" is specified, it means that the tag contains sub-tags, each
|
||||
one corresponding to the value of an attribute for this object.
|
||||
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)
|
||||
# self.classes below is a dict whose keys are tag names and values are
|
||||
# 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,
|
||||
# knowing that the value is a 'string' is not sufficient).
|
||||
self.conversionFunctions = conversionFunctions
|
||||
self.utf8 = utf8
|
||||
|
||||
def convertAttrs(self, attrs):
|
||||
'''Converts XML attrs to a dict.'''
|
||||
|
@ -360,6 +362,8 @@ class XmlUnmarshaller(XmlParser):
|
|||
setattr(currentContainer, name, attrValue)
|
||||
|
||||
def characters(self, content):
|
||||
if not self.utf8:
|
||||
content = content.encode('utf-8')
|
||||
e = XmlParser.characters(self, content)
|
||||
if e.currentBasicType:
|
||||
e.currentContent += content
|
||||
|
|
Loading…
Reference in a new issue