--- /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)
--- 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):
--- 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
--- 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()
--- 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):
--- 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'
--- 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 ##########################
--- /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'<div class="captcha">%s</div>' % img
+ return img + super(CaptchaWidget, self).render(form, field, renderer)
--- 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;
Binary file web/data/porkys.ttf has changed
--- 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)
--- 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('<option>%s %s</option>' % (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"""\
<div class="%s" id="%s">
@@ -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('<option id="%s" value="%s">%s %s</option>' % (
- 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 ####################################################
--- 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'</td>\n')
self.w(u'</tr></table>\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'</div>')
-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'<div id="%s" class="%s">' % (id, klass))
if title:
@@ -477,52 +494,28 @@
stitle = u' '
self.w(u'<div id="loginTitle">%s</div>' % stitle)
self.w(u'<div id="loginContent">\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'<div class="simpleMessage">%s</div>\n' % self._cw.message)
+ if self._cw.vreg.config['auth-mode'] != 'http':
# Cookie authentication
self.login_form(id)
self.w(u'</div></div>\n')
- def display_message(self):
- message = self._cw.message
- if message:
- self.w(u'<div class="simpleMessage">%s</div>\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'<form method="post" action="%s" id="login_form">\n'
- % xml_escape(login_form_url(self._cw.vreg.config, self._cw)))
- self.w(u'<table>\n')
- self.add_fields()
- self.w(u'<tr>\n')
- self.w(u'<td> </td><td><input type="submit" class="loginButton right" value="%s" />\n</td>' % _('log in'))
- self.w(u'</tr>\n')
- self.w(u'</table>\n')
- self.w(u'</form>\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'<tr>\n')
- self.w(u'<td><label for="%s" >%s</label></td>' % (name, label))
- self.w(u'<td><input name="%s" id="%s" class="data" type="%s" /></td>\n' %
- (name, name, inputtype))
- self.w(u'</tr>\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()
--- 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):