# HG changeset patch # User Sylvain Thénault # Date 1266316272 -3600 # Node ID 437867dde2363b3a11a77c30b01f0bcf6b3adfaf # Parent 440e340c61fe95f954c95ac6cd84a6a935a69498# Parent e872097f228731e5a35203a39eaae912337debed backport fixes done accidentaly in default diff -r 440e340c61fe -r 437867dde236 crypto.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/crypto.py Tue Feb 16 11:31:12 2010 +0100 @@ -0,0 +1,35 @@ +"""Simple cryptographic routines, based on python-crypto. + +:organization: Logilab +:copyright: 2009-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +""" +__docformat__ = "restructuredtext en" + +from pickle import dumps, loads +from base64 import b64encode, b64decode + +from Crypto.Cipher import Blowfish + + +_CYPHERERS = {} +def _cypherer(seed): + try: + return _CYPHERERS[seed] + except KeyError: + _CYPHERERS[seed] = Blowfish.new(seed, Blowfish.MODE_ECB) + return _CYPHERERS[seed] + + +def encrypt(data, seed): + string = dumps(data) + string = string + '*' * (8 - len(string) % 8) + string = b64encode(_cypherer(seed).encrypt(string)) + return unicode(string) + + +def decrypt(string, seed): + # pickle ignores trailing characters so we do not need to strip them off + string = _cypherer(seed).decrypt(b64decode(string)) + return loads(string) diff -r 440e340c61fe -r 437867dde236 dbapi.py --- a/dbapi.py Mon Feb 15 18:36:34 2010 +0100 +++ b/dbapi.py Tue Feb 16 11:31:12 2010 +0100 @@ -263,6 +263,8 @@ def get_session_data(self, key, default=None, pop=False): """return value associated to `key` in session data""" + if self.cnx is None: + return None # before the connection has been established return self.cnx.get_session_data(key, default, pop) def set_session_data(self, key, value): diff -r 440e340c61fe -r 437867dde236 debian/control --- a/debian/control Mon Feb 15 18:36:34 2010 +0100 +++ b/debian/control Tue Feb 16 11:31:12 2010 +0100 @@ -63,7 +63,7 @@ Architecture: all XB-Python-Version: ${python:Versions} Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), python-simplejson (>= 1.3), python-elementtree -Recommends: python-docutils, python-vobject, fckeditor, python-fyzz, python-pysixt, fop +Recommends: python-docutils, python-vobject, fckeditor, python-fyzz, python-pysixt, fop, python-imaging Description: web interface library for the CubicWeb framework CubicWeb is a semantic web application framework. . @@ -78,7 +78,7 @@ Architecture: all XB-Python-Version: ${python:Versions} Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.6.0), python-logilab-common (>= 0.47.0), python-yams (>= 0.27.0), python-rql (>= 0.24.0), python-lxml -Recommends: python-simpletal (>= 4.0) +Recommends: python-simpletal (>= 4.0), python-crypto Conflicts: cubicweb-core Replaces: cubicweb-core Description: common library for the CubicWeb framework diff -r 440e340c61fe -r 437867dde236 hooks/syncschema.py --- a/hooks/syncschema.py Mon Feb 15 18:36:34 2010 +0100 +++ b/hooks/syncschema.py Tue Feb 16 11:31:12 2010 +0100 @@ -161,11 +161,16 @@ def commit_event(self): rebuildinfered = self.session.data.get('rebuild-infered', True) repo = self.session.repo - repo.set_schema(repo.schema, rebuildinfered=rebuildinfered) - # CWUser class might have changed, update current session users - cwuser_cls = self.session.vreg['etypes'].etype_class('CWUser') - for session in repo._sessions.values(): - session.user.__class__ = cwuser_cls + # commit event should not raise error, while set_schema has chances to + # do so because it triggers full vreg reloading + try: + repo.set_schema(repo.schema, rebuildinfered=rebuildinfered) + # CWUser class might have changed, update current session users + cwuser_cls = self.session.vreg['etypes'].etype_class('CWUser') + for session in repo._sessions.values(): + session.user.__class__ = cwuser_cls + except: + self.critical('error while setting schmea', exc_info=True) def rollback_event(self): self.precommit_event() diff -r 440e340c61fe -r 437867dde236 schemas/base.py --- a/schemas/base.py Mon Feb 15 18:36:34 2010 +0100 +++ b/schemas/base.py Tue Feb 16 11:31:12 2010 +0100 @@ -170,19 +170,11 @@ """link a permission to the entity. This permission should be used in the security definition of the entity's type to be useful. """ - __permissions__ = { - 'read': ('managers', 'users', 'guests'), - 'add': ('managers',), - 'delete': ('managers',), - } + __permissions__ = META_RTYPE_PERMS class require_group(RelationType): """used to grant a permission to a group""" - __permissions__ = { - 'read': ('managers', 'users', 'guests'), - 'add': ('managers',), - 'delete': ('managers',), - } + __permissions__ = META_RTYPE_PERMS class ExternalUri(EntityType): diff -r 440e340c61fe -r 437867dde236 schemas/bootstrap.py --- a/schemas/bootstrap.py Mon Feb 15 18:36:34 2010 +0100 +++ b/schemas/bootstrap.py Tue Feb 16 11:31:12 2010 +0100 @@ -8,8 +8,8 @@ __docformat__ = "restructuredtext en" _ = unicode -from yams.buildobjs import (EntityType, RelationType, SubjectRelation, - RichString, String, Boolean, Int) +from yams.buildobjs import (EntityType, RelationType, RelationDefinition, + SubjectRelation, RichString, String, Boolean, Int) from cubicweb.schema import RQLConstraint from cubicweb.schemas import META_ETYPE_PERMS, META_RTYPE_PERMS @@ -195,43 +195,72 @@ __permissions__ = META_RTYPE_PERMS inlined = True -class read_permission(RelationType): - """grant permission to read entity or relation through a group or rql - expression - """ + +class read_permission_cwgroup(RelationDefinition): + """groups allowed to read entities/relations of this type""" + __permissions__ = META_RTYPE_PERMS + name = 'read_permission' + subject = ('CWEType', 'CWAttribute', 'CWRelation') + object = 'CWGroup' + cardinality = '**' + +class add_permission_cwgroup(RelationDefinition): + """groups allowed to add entities/relations of this type""" + __permissions__ = META_RTYPE_PERMS + name = 'add_permission' + subject = ('CWEType', 'CWRelation') + object = 'CWGroup' + cardinality = '**' + +class delete_permission_cwgroup(RelationDefinition): + """groups allowed to delete entities/relations of this type""" __permissions__ = META_RTYPE_PERMS + name = 'delete_permission' + subject = ('CWEType', 'CWRelation') + object = 'CWGroup' + cardinality = '**' + +class update_permission_cwgroup(RelationDefinition): + """groups allowed to update entities/relations of this type""" + __permissions__ = META_RTYPE_PERMS + name = 'update_permission' + subject = ('CWEType', 'CWAttribute') + object = 'CWGroup' + cardinality = '**' + +class read_permission_rqlexpr(RelationDefinition): + """rql expression allowing to read entities/relations of this type""" + __permissions__ = META_RTYPE_PERMS + name = 'read_permission' subject = ('CWEType', 'CWAttribute', 'CWRelation') - object = ('CWGroup', 'RQLExpression') + object = 'RQLExpression' cardinality = '*?' composite = 'subject' -class add_permission(RelationType): - """grant permission to add entity or relation through a group or rql - expression - """ +class add_permission_rqlexpr(RelationDefinition): + """rql expression allowing to add entities/relations of this type""" __permissions__ = META_RTYPE_PERMS + name = 'add_permission' subject = ('CWEType', 'CWRelation') - object = ('CWGroup', 'RQLExpression') + object = 'RQLExpression' cardinality = '*?' composite = 'subject' -class delete_permission(RelationType): - """grant permission to delete entity or relation through a group or rql - expression - """ +class delete_permission_rqlexpr(RelationDefinition): + """rql expression allowing to delete entities/relations of this type""" __permissions__ = META_RTYPE_PERMS + name = 'delete_permission' subject = ('CWEType', 'CWRelation') - object = ('CWGroup', 'RQLExpression') + object = 'RQLExpression' cardinality = '*?' composite = 'subject' -class update_permission(RelationType): - """grant permission to update entity or attribute through a group or rql - expression - """ +class update_permission_rqlexpr(RelationDefinition): + """rql expression allowing to update entities/relations of this type""" __permissions__ = META_RTYPE_PERMS + name = 'update_permission' subject = ('CWEType', 'CWAttribute') - object = ('CWGroup', 'RQLExpression') + object = 'RQLExpression' cardinality = '*?' composite = 'subject' diff -r 440e340c61fe -r 437867dde236 schemas/workflow.py --- a/schemas/workflow.py Mon Feb 15 18:36:34 2010 +0100 +++ b/schemas/workflow.py Tue Feb 16 11:31:12 2010 +0100 @@ -172,6 +172,7 @@ } inlined = True + class workflow_of(RelationType): """link a workflow to one or more entity type""" __permissions__ = META_RTYPE_PERMS @@ -186,20 +187,15 @@ __permissions__ = META_RTYPE_PERMS inlined = True -class subworkflow(RelationType): - """link a transition to one or more workflow""" +class destination_state(RelationType): + """destination state of a transition""" __permissions__ = META_RTYPE_PERMS inlined = True -class exit_point(RelationType): - """link a transition to one or more workflow""" +class allowed_transition(RelationType): + """allowed transitions from this state""" __permissions__ = META_RTYPE_PERMS -class subworkflow_state(RelationType): - """link a transition to one or more workflow""" - __permissions__ = META_RTYPE_PERMS - inlined = True - class initial_state(RelationType): """indicate which state should be used by default when an entity using states is created @@ -207,14 +203,25 @@ __permissions__ = META_RTYPE_PERMS inlined = True -class destination_state(RelationType): - """destination state of a transition""" + +class subworkflow(RelationType): __permissions__ = META_RTYPE_PERMS inlined = True -class allowed_transition(RelationType): - """allowed transitions from this state""" +class exit_point(RelationType): + __permissions__ = META_RTYPE_PERMS + +class subworkflow_state(RelationType): __permissions__ = META_RTYPE_PERMS + inlined = True + + +class condition(RelationType): + __permissions__ = META_RTYPE_PERMS + +# already defined in base.py +# class require_group(RelationType): +# __permissions__ = META_RTYPE_PERMS # "abstract" relations, set by WorkflowableEntityType ########################## diff -r 440e340c61fe -r 437867dde236 web/captcha.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/captcha.py Tue Feb 16 11:31:12 2010 +0100 @@ -0,0 +1,85 @@ +"""Simple captcha library, based on PIL. Monkey patch functions in this module +if you want something better... + +:organization: Logilab +:copyright: 2009-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +""" +__docformat__ = "restructuredtext en" + +from random import randint, choice +from cStringIO import StringIO + +import Image, ImageFont, ImageDraw, ImageFilter + + +from time import time + +from cubicweb import tags +from cubicweb.view import StartupView +from cubicweb.web import httpcache, formwidgets as fw, formfields as ff + + +def pil_captcha(text, fontfile, fontsize): + """Generate a captcha image. Return a PIL image object. + + adapted from http://code.activestate.com/recipes/440588/ + """ + # randomly select the foreground color + fgcolor = randint(0, 0xffff00) + # make the background color the opposite of fgcolor + bgcolor = fgcolor ^ 0xffffff + # create a font object + font = ImageFont.truetype(fontfile, fontsize) + # determine dimensions of the text + dim = font.getsize(text) + # create a new image slightly larger that the text + img = Image.new('RGB', (dim[0]+5, dim[1]+5), bgcolor) + draw = ImageDraw.Draw(img) + # draw 100 random colored boxes on the background + x, y = img.size + for num in range(100): + draw.rectangle((randint(0, x), randint(0, y), + randint(0, x), randint(0, y)), + fill=randint(0, 0xffffff)) + # add the text to the image + draw.text((3, 3), text, font=font, fill=fgcolor) + img = img.filter(ImageFilter.EDGE_ENHANCE_MORE) + return img + + +def captcha(fontfile, fontsize, size=5, format='JPEG'): + """Generate an arbitrary text, return it together with a buffer containing + the captcha image for the text + """ + text = u''.join(choice('QWERTYUOPASDFGHJKLZXCVBNM') for i in range(size)) + img = pil_captcha(text, fontfile, fontsize) + out = StringIO() + img.save(out, format) + out.seek(0) + return text, out + + +class CaptchaView(StartupView): + __regid__ = 'captcha' + + http_cache_manager = httpcache.NoHTTPCacheManager + binary = True + templatable = False + content_type = 'image/jpg' + + def call(self): + text, data = captcha.captcha(self._cw.vreg.config['captcha-font-file'], + self._cw.vreg.config['captcha-font-size']) + self._cw.set_session_data('captcha', text) + self.w(data.read()) + + +class CaptchaWidget(fw.TextInput): + def render(self, form, field, renderer=None): + # t=int(time()*100) to make sure img is not cached + src = form._cw.build_url('view', vid='captcha', t=int(time()*100)) + img = tags.img(src=src, alt=u'captcha') + img = u'
%s
' % img + return img + super(CaptchaWidget, self).render(form, field, renderer) diff -r 440e340c61fe -r 437867dde236 web/data/cubicweb.login.css --- a/web/data/cubicweb.login.css Mon Feb 15 18:36:34 2010 +0100 +++ b/web/data/cubicweb.login.css Tue Feb 16 11:31:12 2010 +0100 @@ -75,7 +75,7 @@ width:12em; } -input.loginButton { +.loginButton { border: 1px solid #edecd2; border-color:#edecd2 #cfceb7 #cfceb7 #edecd2; margin: 2px 0px 0px; diff -r 440e340c61fe -r 437867dde236 web/data/porkys.ttf Binary file web/data/porkys.ttf has changed diff -r 440e340c61fe -r 437867dde236 web/formwidgets.py --- a/web/formwidgets.py Mon Feb 15 18:36:34 2010 +0100 +++ b/web/formwidgets.py Tue Feb 16 11:31:12 2010 +0100 @@ -318,7 +318,7 @@ iattrs['checked'] = u'checked' tag = tags.input(name=field.input_name(form, self.suffix), type=self.type, value=value, **iattrs) - options.append(tag + label) + options.append(u'%s %s' % (tag, label)) return sep.join(options) @@ -521,9 +521,9 @@ def init_ajax_attributes(attrs, wdgtype, loadtype=u'auto'): try: - attrs['klass'] += u' widget' + attrs['class'] += u' widget' except KeyError: - attrs['klass'] = u'widget' + attrs['class'] = u'widget' attrs.setdefault('cubicweb:wdgtype', wdgtype) attrs.setdefault('cubicweb:loadtype', loadtype) @@ -639,7 +639,7 @@ self.value = '' self.onclick = onclick self.cwaction = cwaction - self.attrs.setdefault('klass', 'validateButton') + self.attrs.setdefault('class', 'validateButton') def render(self, form, field=None, renderer=None): label = form._cw._(self.label) diff -r 440e340c61fe -r 437867dde236 web/views/autoform.py --- a/web/views/autoform.py Mon Feb 15 18:36:34 2010 +0100 +++ b/web/views/autoform.py Tue Feb 16 11:31:12 2010 +0100 @@ -427,9 +427,6 @@ % (pendingid, entity.eid) rset = form._cw.eid_rset(reid) eview = form._cw.view('text', rset, row=0) - # XXX find a clean way to handle baskets - if rset.description[0][0] == 'Basket': - eview = '%s (%s)' % (eview, display_name(form._cw, 'Basket')) yield rtype, pendingid, jscall, label, reid, eview @@ -461,8 +458,6 @@ options.append('' % (self._cw._('select a'), etypes)) options += self._get_select_options(entity, rschema, role) options += self._get_search_options(entity, rschema, role, targettypes) - if 'Basket' in self._cw.vreg.schema: # XXX - options += self._get_basket_options(entity, rschema, role, targettypes) relname, role = self._cw.form.get('relation').rsplit('_', 1) return u"""\
@@ -509,37 +504,6 @@ xml_escape(url), _('Search for'), eschema.display_name(self._cw)))) return [o for l, o in sorted(options)] - # XXX move this out - def _get_basket_options(self, entity, rschema, role, targettypes): - options = [] - rtype = rschema.type - _ = self._cw._ - for basketeid, basketname in self._get_basket_links(self._cw.user.eid, - role, targettypes): - optionid = relation_id(entity.eid, rtype, role, basketeid) - options.append('' % ( - optionid, basketeid, _('link to each item in'), xml_escape(basketname))) - return options - - def _get_basket_links(self, ueid, role, targettypes): - targettypes = set(targettypes) - for basketeid, basketname, elements in self._get_basket_info(ueid): - baskettypes = elements.column_types(0) - # if every elements in the basket can be attached to the - # edited entity - if baskettypes & targettypes: - yield basketeid, basketname - - def _get_basket_info(self, ueid): - basketref = [] - basketrql = 'Any B,N WHERE B is Basket, B owned_by U, U eid %(x)s, B name N' - basketresultset = self._cw.execute(basketrql, {'x': ueid}, 'x') - for result in basketresultset: - basketitemsrql = 'Any X WHERE X in_basket B, B eid %(x)s' - rset = self._cw.execute(basketitemsrql, {'x': result[0]}, 'x') - basketref.append((result[0], result[1], rset)) - return basketref - # The automatic entity form #################################################### diff -r 440e340c61fe -r 437867dde236 web/views/basetemplates.py --- a/web/views/basetemplates.py Mon Feb 15 18:36:34 2010 +0100 +++ b/web/views/basetemplates.py Tue Feb 16 11:31:12 2010 +0100 @@ -15,6 +15,8 @@ from cubicweb.view import View, MainTemplate, NOINDEX, NOFOLLOW from cubicweb.utils import UStringIO, can_do_pdf_conversion from cubicweb.schema import display_name +from cubicweb.web import formfields as ff, formwidgets as fw +from cubicweb.web.views import forms # main templates ############################################################## @@ -389,7 +391,7 @@ self.w(u'\n') self.w(u'\n') self.wview('logform', rset=self.cw_rset, id='popupLoginBox', klass='hidden', - title=False, message=False) + title=False, showmessage=False) def state_header(self): state = self._cw.search_state @@ -460,13 +462,28 @@ self.w(u'
') -class LogFormTemplate(View): +class LogForm(forms.FieldsForm): + __regid__ = 'logform' + domid = 'loginForm' + # XXX have to recall fields name since python is mangling __login/__password + __login = ff.StringField('__login', widget=fw.TextInput({'class': 'data'})) + __password = ff.StringField('__password', label=_('password'), + widget=fw.PasswordSingleInput({'class': 'data'})) + form_buttons = [fw.SubmitButton(label=_('log in'), + attrs={'class': 'loginButton right'})] + + @property + def action(self): + return xml_escape(login_form_url(self._cw)) + + +class LogFormView(View): __regid__ = 'logform' __select__ = match_kwargs('id', 'klass') title = 'log in' - def call(self, id, klass, title=True, message=True): + def call(self, id, klass, title=True, showmessage=True): self._cw.add_css('cubicweb.login.css') self.w(u'
' % (id, klass)) if title: @@ -477,52 +494,28 @@ stitle = u' ' self.w(u'
%s
' % stitle) self.w(u'
\n') - - if message: - self.display_message() - if self._cw.vreg.config['auth-mode'] == 'http': - # HTTP authentication - pass - else: + if showmessage and self._cw.message: + self.w(u'
%s
\n' % self._cw.message) + if self._cw.vreg.config['auth-mode'] != 'http': # Cookie authentication self.login_form(id) self.w(u'
\n') - def display_message(self): - message = self._cw.message - if message: - self.w(u'
%s
\n' % message) + def login_form(self, id): + cw = self._cw + form = cw.vreg['forms'].select('logform', cw) + if cw.vreg.config['allow-email-login']: + label = cw._('login or email') + else: + label = cw._('login') + form.field_by_name('__login').label = label + self.w(form.render(table_class='', display_progress_div=False)) + cw.html_headers.add_onload('jQuery("#__login:visible").focus()') - def login_form(self, id): - _ = self._cw._ - # XXX turn into a form - self.w(u'
\n' - % xml_escape(login_form_url(self._cw.vreg.config, self._cw))) - self.w(u'\n') - self.add_fields() - self.w(u'\n') - self.w(u'' % _('log in')) - self.w(u'\n') - self.w(u'
 \n
\n') - self.w(u'
\n') - self._cw.html_headers.add_onload('jQuery("#__login:visible").focus()') - - def add_fields(self): - msg = (self._cw.vreg.config['allow-email-login'] and _('login or email')) or _('login') - self.add_field('__login', msg, 'text') - self.add_field('__password', self._cw._('password'), 'password') - - def add_field(self, name, label, inputtype): - self.w(u'\n') - self.w(u'' % (name, label)) - self.w(u'\n' % - (name, name, inputtype)) - self.w(u'\n') - - -def login_form_url(config, req): +def login_form_url(req): if req.https: return req.url() - if config.get('https-url'): - return req.url().replace(req.base_url(), config['https-url']) + httpsurl = req.vreg.config.get('https-url') + if httpsurl: + return req.url().replace(req.base_url(), httpsurl) return req.url() diff -r 440e340c61fe -r 437867dde236 web/webconfig.py --- a/web/webconfig.py Mon Feb 15 18:36:34 2010 +0100 +++ b/web/webconfig.py Tue Feb 16 11:31:12 2010 +0100 @@ -176,6 +176,22 @@ 'help': 'print the traceback on the error page when an error occured', 'group': 'web', 'inputlevel': 2, }), + + ('captcha-font-file', + {'type' : 'string', + 'default': join(CubicWebConfiguration.shared_dir(), 'data', 'porkys.ttf'), + 'help': 'True type font to use for captcha image generation (you \ +must have the python imaging library installed to use captcha)', + 'group': 'web', 'inputlevel': 2, + }), + ('captcha-font-size', + {'type' : 'int', + 'default': 25, + 'help': 'Font size to use for captcha image generation (you must \ +have the python imaging library installed to use captcha)', + 'group': 'web', 'inputlevel': 2, + }), + )) def fckeditor_installed(self):