# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr## This file is part of CubicWeb.## CubicWeb is free software: you can redistribute it and/or modify it under the# terms of the GNU Lesser General Public License as published by the Free# Software Foundation, either version 2.1 of the License, or (at your option)# any later version.## CubicWeb is distributed in the hope that it will be useful, but WITHOUT# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more# details.## You should have received a copy of the GNU Lesser General Public License along# with CubicWeb. If not, see <http://www.gnu.org/licenses/>."""Core hooks: check for data integrity according to the instance'schemavalidity"""__docformat__="restructuredtext en"fromthreadingimportLockfromyams.schemaimportrole_namefromcubicwebimportValidationErrorfromcubicweb.schemaimportRQLConstraint,RQLUniqueConstraintfromcubicweb.selectorsimportis_instancefromcubicweb.uilibimportsoup2xhtmlfromcubicweb.serverimporthookfromcubicweb.server.hookimportset_operation# special relations that don't have to be checked for integrity, usually# because they are handled internally by hooks (so we trust ourselves)DONT_CHECK_RTYPES_ON_ADD=set(('owned_by','created_by','is','is_instance_of','wf_info_for','from_state','to_state'))DONT_CHECK_RTYPES_ON_DEL=set(('is','is_instance_of','wf_info_for','from_state','to_state'))_UNIQUE_CONSTRAINTS_LOCK=Lock()_UNIQUE_CONSTRAINTS_HOLDER=Nonedef_acquire_unique_cstr_lock(session):"""acquire the _UNIQUE_CONSTRAINTS_LOCK for the session. This lock used to avoid potential integrity pb when checking RQLUniqueConstraint in two different transactions, as explained in http://intranet.logilab.fr/jpl/ticket/36564 """if'uniquecstrholder'insession.transaction_data:return_UNIQUE_CONSTRAINTS_LOCK.acquire()session.transaction_data['uniquecstrholder']=True# register operation responsible to release the lock on commit/rollback_ReleaseUniqueConstraintsOperation(session)def_release_unique_cstr_lock(session):if'uniquecstrholder'insession.transaction_data:delsession.transaction_data['uniquecstrholder']_UNIQUE_CONSTRAINTS_LOCK.release()class_ReleaseUniqueConstraintsOperation(hook.Operation):defcommit_event(self):passdefpostcommit_event(self):_release_unique_cstr_lock(self.session)defrollback_event(self):_release_unique_cstr_lock(self.session)class_CheckRequiredRelationOperation(hook.LateOperation):"""checking relation cardinality has to be done after commit in case the relation is being replaced """role=key=base_rql=Nonedefprecommit_event(self):session=self.sessionpendingeids=session.transaction_data.get('pendingeids',())pendingrtypes=session.transaction_data.get('pendingrtypes',())# poping key is not optional: if further operation trigger new deletion# of relation, we'll need a new operationforeid,rtypeinsession.transaction_data.pop(self.key):# recheck pending eids / relation typesifeidinpendingeids:continueifrtypeinpendingrtypes:continueifnotsession.execute(self.base_rql%rtype,{'x':eid}):etype=session.describe(eid)[0]_=session._msg=_('at least one relation %(rtype)s is required on ''%(etype)s (%(eid)s)')msg%={'rtype':_(rtype),'etype':_(etype),'eid':eid}raiseValidationError(eid,{role_name(rtype,self.role):msg})class_CheckSRelationOp(_CheckRequiredRelationOperation):"""check required subject relation"""role='subject'key='_cwisrel'base_rql='Any O WHERE S eid %%(x)s, S %s O'class_CheckORelationOp(_CheckRequiredRelationOperation):"""check required object relation"""role='object'key='_cwiorel'base_rql='Any S WHERE O eid %%(x)s, S %s O'classIntegrityHook(hook.Hook):__abstract__=Truecategory='integrity'classCheckCardinalityHook(IntegrityHook):"""check cardinalities are satisfied"""__regid__='checkcard'events=('after_add_entity','before_delete_relation')def__call__(self):getattr(self,self.event)()defafter_add_entity(self):eid=self.entity.eideschema=self.entity.e_schemaforrschema,targetschemas,roleineschema.relation_definitions():# skip automatically handled relationsifrschema.typeinDONT_CHECK_RTYPES_ON_ADD:continuerdef=rschema.role_rdef(eschema,targetschemas[0],role)ifrdef.role_cardinality(role)in'1+':ifrole=='subject':set_operation(self._cw,'_cwisrel',(eid,rschema.type),_CheckSRelationOp,list)else:set_operation(self._cw,'_cwiorel',(eid,rschema.type),_CheckORelationOp,list)defbefore_delete_relation(self):rtype=self.rtypeifrtypeinDONT_CHECK_RTYPES_ON_DEL:returnsession=self._cweidfrom,eidto=self.eidfrom,self.eidtopendingrdefs=session.transaction_data.get('pendingrdefs',())if(session.describe(eidfrom)[0],rtype,session.describe(eidto)[0])inpendingrdefs:returncard=session.schema_rproperty(rtype,eidfrom,eidto,'cardinality')ifcard[0]in'1+'andnotsession.deleted_in_transaction(eidfrom):set_operation(self._cw,'_cwisrel',(eidfrom,rtype),_CheckSRelationOp,list)ifcard[1]in'1+'andnotsession.deleted_in_transaction(eidto):set_operation(self._cw,'_cwiorel',(eidto,rtype),_CheckORelationOp,list)class_CheckConstraintsOp(hook.LateOperation):""" check a new relation satisfy its constraints """defprecommit_event(self):session=self.sessionforvaluesinsession.transaction_data.pop('check_constraints_op'):eidfrom,rtype,eidto,constraints=values# first check related entities have not been deleted in the same# transactionifsession.deleted_in_transaction(eidfrom):returnifsession.deleted_in_transaction(eidto):returnforconstraintinconstraints:# XXX# * lock RQLConstraint as well?# * use a constraint id to use per constraint lock and avoid# unnecessary commit serialization ?ifisinstance(constraint,RQLUniqueConstraint):_acquire_unique_cstr_lock(session)try:constraint.repo_check(session,eidfrom,rtype,eidto)exceptNotImplementedError:self.critical('can\'t check constraint %s, not supported',constraint)defcommit_event(self):passclassCheckConstraintHook(IntegrityHook):"""check the relation satisfy its constraints this is delayed to a precommit time operation since other relation which will make constraint satisfied (or unsatisfied) may be added later. """__regid__='checkconstraint'events=('after_add_relation',)def__call__(self):# XXX get only RQL[Unique]Constraints?constraints=self._cw.schema_rproperty(self.rtype,self.eidfrom,self.eidto,'constraints')ifconstraints:hook.set_operation(self._cw,'check_constraints_op',(self.eidfrom,self.rtype,self.eidto,tuple(constraints)),_CheckConstraintsOp,list)classCheckAttributeConstraintHook(IntegrityHook):"""check the attribute relation satisfy its constraints this is delayed to a precommit time operation since other relation which will make constraint satisfied (or unsatisfied) may be added later. """__regid__='checkattrconstraint'events=('after_add_entity','after_update_entity')def__call__(self):eschema=self.entity.e_schemaforattrinself.entity.edited_attributes:ifeschema.subjrels[attr].final:constraints=[cforcineschema.rdef(attr).constraintsifisinstance(c,(RQLUniqueConstraint,RQLConstraint))]ifconstraints:hook.set_operation(self._cw,'check_constraints_op',(self.entity.eid,attr,None,tuple(constraints)),_CheckConstraintsOp,list)classCheckUniqueHook(IntegrityHook):__regid__='checkunique'events=('before_add_entity','before_update_entity')def__call__(self):entity=self.entityeschema=entity.e_schemaforattrinentity.edited_attributes:ifeschema.subjrels[attr].finalandeschema.has_unique_values(attr):val=entity[attr]ifvalisNone:continuerql='%s X WHERE X %s%%(val)s'%(entity.e_schema,attr)rset=self._cw.execute(rql,{'val':val})ifrsetandrset[0][0]!=entity.eid:msg=self._cw._('the value "%s" is already used, use another one')qname=role_name(attr,'subject')raiseValidationError(entity.eid,{qname:msg%val})classDontRemoveOwnersGroupHook(IntegrityHook):"""delete the composed of a composite relation when this relation is deleted """__regid__='checkownersgroup'__select__=IntegrityHook.__select__&is_instance('CWGroup')events=('before_delete_entity','before_update_entity')def__call__(self):ifself.event=='before_delete_entity'andself.entity.name=='owners':msg=self._cw._('can\'t be deleted')raiseValidationError(self.entity.eid,{None:msg})elifself.event=='before_update_entity'and \'name'inself.entity.edited_attributes:newname=self.entity.pop('name')oldname=self.entity.nameifoldname=='owners'andnewname!=oldname:qname=role_name('name','subject')msg=self._cw._('can\'t be changed')raiseValidationError(self.entity.eid,{qname:msg})self.entity['name']=newnameclassTidyHtmlFields(IntegrityHook):"""tidy HTML in rich text strings"""__regid__='htmltidy'events=('before_add_entity','before_update_entity')def__call__(self):entity=self.entitymetaattrs=entity.e_schema.meta_attributes()formetaattr,(metadata,attr)inmetaattrs.iteritems():ifmetadata=='format'andattrinentity.edited_attributes:try:value=entity[attr]exceptKeyError:continue# no text to tidyifisinstance(value,unicode):# filter out None and Binaryifgetattr(entity,str(metaattr))=='text/html':entity[attr]=soup2xhtml(value,self._cw.encoding)classStripCWUserLoginHook(IntegrityHook):"""ensure user logins are stripped"""__regid__='stripuserlogin'__select__=IntegrityHook.__select__&is_instance('CWUser')events=('before_add_entity','before_update_entity',)def__call__(self):user=self.entityif'login'inuser.edited_attributesanduser.login:user.login=user.login.strip()# 'active' integrity hooks: you usually don't want to deactivate them, they are# not really integrity check, they maintain consistency on changesclass_DelayedDeleteOp(hook.Operation):"""delete the object of composite relation except if the relation has actually been redirected to another composite """key=base_rql=Nonedefprecommit_event(self):session=self.sessionpendingeids=session.transaction_data.get('pendingeids',())neweids=session.transaction_data.get('neweids',())# poping key is not optional: if further operation trigger new deletion# of composite relation, we'll need a new operationforeid,rtypeinsession.transaction_data.pop(self.key):# don't do anything if the entity is being created or deletedifnot(eidinpendingeidsoreidinneweids):etype=session.describe(eid)[0]session.execute(self.base_rql%(etype,rtype),{'x':eid})class_DelayedDeleteSEntityOp(_DelayedDeleteOp):"""delete orphan subject entity of a composite relation"""key='_cwiscomp'base_rql='DELETE %s X WHERE X eid %%(x)s, NOT X %s Y'class_DelayedDeleteOEntityOp(_DelayedDeleteOp):"""check required object relation"""key='_cwiocomp'base_rql='DELETE %s X WHERE X eid %%(x)s, NOT Y %s X'classDeleteCompositeOrphanHook(hook.Hook):"""delete the composed of a composite relation when this relation is deleted """__regid__='deletecomposite'events=('before_delete_relation',)category='activeintegrity'def__call__(self):# if the relation is being delete, don't delete composite's components# automaticallypendingrdefs=self._cw.transaction_data.get('pendingrdefs',())if(self._cw.describe(self.eidfrom)[0],self.rtype,self._cw.describe(self.eidto)[0])inpendingrdefs:returncomposite=self._cw.schema_rproperty(self.rtype,self.eidfrom,self.eidto,'composite')ifcomposite=='subject':set_operation(self._cw,'_cwiocomp',(self.eidto,self.rtype),_DelayedDeleteOEntityOp)elifcomposite=='object':set_operation(self._cw,'_cwiscomp',(self.eidfrom,self.rtype),_DelayedDeleteSEntityOp)