# HG changeset patch # User Aurelien Campeas # Date 1286211365 -7200 # Node ID 72ba82a26e050ae589ede41b6edde0e31bcb328e # Parent 34317f39561989b9cd0dcd1375881282be37bcd1 refactor login box & form to enable easy pluggability * vregistry.selectable: get all selectable object of fixed oid with given context * template headeri, logbox, logform: reorganize a bit the structure diff -r 34317f395619 -r 72ba82a26e05 selectors.py --- a/selectors.py Mon Oct 04 18:55:57 2010 +0200 +++ b/selectors.py Mon Oct 04 18:56:05 2010 +0200 @@ -1182,7 +1182,6 @@ """ return ~ authenticated_user() - class match_user_groups(ExpectedValueSelector): """Return a non-zero score if request's user is in at least one of the groups given as initializer argument. Returned score is the number of groups @@ -1213,6 +1212,24 @@ return score +class configuration_values(Selector): + """Return 1 if the instance is configured according to + the given value(s)""" + + def __init__(self, key, values): + self._key = key + if isinstance(values, basestring): + values = (values,) + self._values = frozenset(values) + + @lltrace + def __call__(self, cls, req, **kwargs): + try: + return self._score + except AttributeError: + self._score = req.vreg.config[self._key] in self._values + return self._score + # Web request selectors ######################################################## # XXX deprecate diff -r 34317f395619 -r 72ba82a26e05 vregistry.py --- a/vregistry.py Mon Oct 04 18:55:57 2010 +0200 +++ b/vregistry.py Mon Oct 04 18:56:05 2010 +0200 @@ -195,6 +195,18 @@ select_object = deprecated('[3.6] use select_or_none instead of select_object' )(select_or_none) + def selectable(self, oid, *args, **kwargs): + """return all appobjects having the given oid that are + selectable against the given context, in score order + """ + objects = [] + for appobject in self[oid]: + score = appobject.__select__(appobject, *args, **kwargs) + if score > 0: + objects.append((score, appobject)) + return [obj(*args, **kwargs) + for _score, obj in sorted(objects)] + def possible_objects(self, *args, **kwargs): """return an iterator on possible objects in this registry for the given context diff -r 34317f395619 -r 72ba82a26e05 web/data/cubicweb.htmlhelpers.js --- a/web/data/cubicweb.htmlhelpers.js Mon Oct 04 18:55:57 2010 +0200 +++ b/web/data/cubicweb.htmlhelpers.js Mon Oct 04 18:56:05 2010 +0200 @@ -1,3 +1,14 @@ +/* in CW 3.10, we should move these functions in this namespace */ +cw.htmlhelpers = new Namespace('cw.htmlhelpers'); + +jQuery.extend(cw.htmlhelpers, { + popupLoginBox: function(loginboxid, focusid) { + $('#'+loginboxid).toggleClass('hidden'); + jQuery('#' + focusid +':visible').focus(); + } +}); + + /** * .. function:: baseuri() * @@ -97,10 +108,11 @@ * toggles visibility of login popup div */ // XXX used exactly ONCE in basecomponents -function popupLoginBox() { - $('#popupLoginBox').toggleClass('hidden'); - jQuery('#__login:visible').focus(); -} +popupLoginBox = cw.utils.deprecatedFunction( + function() { + $('#popupLoginBox').toggleClass('hidden'); + jQuery('#__login:visible').focus(); +}); /** * .. function getElementsMatching(tagName, properties, \/* optional \*\/ parent) diff -r 34317f395619 -r 72ba82a26e05 web/data/cubicweb.login.css --- a/web/data/cubicweb.login.css Mon Oct 04 18:55:57 2010 +0200 +++ b/web/data/cubicweb.login.css Mon Oct 04 18:56:05 2010 +0200 @@ -5,25 +5,25 @@ * :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr */ -div#popupLoginBox { +div.popupLoginBox { position: absolute; z-index: 400; right: 0px; width: 26em; padding: 0px 1px 1px; - background: %(listingBorderColor)s; + background: %(listingBorderColor)s; } -div#popupLoginBox label{ +div.popupLoginBox label{ font-weight: bold; } -div#popupLoginBox div#loginContent { +div.popupLoginBox div.loginContent { background: #e6e4ce; padding: 5px 3px 4px; } -div#loginBox { +div.loginBox { position : absolute; top: 15%; left : 50%; @@ -40,7 +40,7 @@ font-size: 140%; } -div#loginTitle { +div.loginTitle { color: #fff; font-weight: bold; font-size: 140%; @@ -49,32 +49,32 @@ background: %(headerBgColor)s url("banner.png") repeat-x top left; } -div#loginBox div#loginContent form { +div.loginBox div.loginContent form { padding-top: 1em; width: 90%; margin: auto; } -#popupLoginBox table td { +.popupLoginBox table td { padding: 0px 3px; white-space: nowrap; } -#loginContent table { +.loginContent table { padding: 0px 0.5em; margin: auto; } -#loginBox table td { +.loginBox table td { padding: 0px 3px 0.6em; white-space: nowrap; } -#loginBox .loginButton { +.loginBox .loginButton { margin-top: 0.6em; } -#loginContent input.data { +.loginContent input.data { width: 12em; } @@ -85,6 +85,6 @@ background: #f0eff0 url("gradient-grey-up.png") left top repeat-x; } -#loginContent .formButtonBar { +.loginContent .formButtonBar { float: right; } diff -r 34317f395619 -r 72ba82a26e05 web/views/authentication.py --- a/web/views/authentication.py Mon Oct 04 18:55:57 2010 +0200 +++ b/web/views/authentication.py Mon Oct 04 18:56:05 2010 +0200 @@ -37,6 +37,7 @@ class WebAuthInfoRetreiver(Component): __registry__ = 'webauth' order = None + __abstract__ = True def authentication_information(self, req): """retreive authentication information from the given request, raise @@ -51,6 +52,18 @@ """ pass + def request_has_auth_info(self, req): + """tells from the request if it has enough information + to proceed to authentication, would the current session + be invalidated + """ + raise NotImplementedError() + + def revalidate_login(self, req): + """returns a login string or None, for repository session + validation purposes + """ + return None class LoginPasswordRetreiver(WebAuthInfoRetreiver): __regid__ = 'loginpwdauth' @@ -65,6 +78,11 @@ raise NoAuthInfo() return login, {'password': password} + def request_has_auth_info(self, req): + return '__login' in req.form + + def revalidate_login(self, req): + return req.get_authorization()[0] class RepositoryAuthenticationManager(AbstractAuthenticationManager): """authenticate user associated to a request and check session validity""" @@ -73,7 +91,7 @@ super(RepositoryAuthenticationManager, self).__init__(vreg) self.repo = vreg.config.repository(vreg) self.log_queries = vreg.config['query-log-file'] - self.authinforetreivers = sorted(vreg['webauth'].possible_objects(vreg), + self.authinforetrievers = sorted(vreg['webauth'].possible_objects(vreg), key=lambda x: x.order) # 2-uple login / password, login is None when no anonymous access # configured @@ -88,10 +106,19 @@ raise :exc:`InvalidSession` if session is corrupted for a reason or another and should be closed + + also invoked while going from anonymous to logged in """ # with this authentication manager, session is actually a dbapi # connection - login = req.get_authorization()[0] + for retriever in self.authinforetrievers: + if retriever.request_has_auth_info(req): + login = retriever.revalidate_login(req) + return self._validate_session(req, session, login) + # let's try with the current session + return self._validate_session(req, session, None) + + def _validate_session(self, req, session, login): # check session.login and not user.login, since in case of login by # email, login and cnx.login are the email while user.login is the # actual user login @@ -114,18 +141,19 @@ raise :exc:`cubicweb.AuthenticationError` if authentication failed (no authentication info found or wrong user/password) """ - for retreiver in self.authinforetreivers: + for retriever in self.authinforetrievers: try: - login, authinfo = retreiver.authentication_information(req) + login, authinfo = retriever.authentication_information(req) except NoAuthInfo: continue try: cnx = self._authenticate(login, authinfo) except AuthenticationError: continue # the next one may succeed - for retreiver_ in self.authinforetreivers: - retreiver_.authenticated(retreiver, req, cnx, login, authinfo) + for retriever_ in self.authinforetrievers: + retriever_.authenticated(retriever, req, cnx, login, authinfo) return cnx, login, authinfo + # false if no authentication info found, eg this is not an # authentication failure if 'login' in locals(): diff -r 34317f395619 -r 72ba82a26e05 web/views/basecomponents.py --- a/web/views/basecomponents.py Mon Oct 04 18:55:57 2010 +0200 +++ b/web/views/basecomponents.py Mon Oct 04 18:56:05 2010 +0200 @@ -28,8 +28,8 @@ from logilab.mtconverter import xml_escape from rql import parse -from cubicweb.selectors import (yes, multi_etypes_rset, - match_form_params, match_context, +from cubicweb.selectors import (yes, multi_etypes_rset, match_form_params, + match_context, configuration_values, anonymous_user, authenticated_user) from cubicweb.schema import display_name from cubicweb.utils import wrap_on_write @@ -97,24 +97,36 @@ """if the user is the anonymous user, build a link to login else display a menu with user'action (preference, logout, etc...) """ + __abstract__ = True cw_property_defs = VISIBLE_PROP_DEF # don't want user to hide this component using an cwproperty site_wide = True __regid__ = 'loggeduserlink' -class AnonUserLink(_UserLink): - __select__ = _UserLink.__select__ & anonymous_user() +class CookieAnonUserLink(_UserLink): + __select__ = _UserLink.__select__ & configuration_values('auth-mode', 'cookie') & anonymous_user() + loginboxid = 'popupLoginBox' + def call(self): - if self._cw.vreg.config['auth-mode'] == 'cookie': - self.w(self._cw._('anonymous')) - self.w(u''' [%s]''' - % (self._cw._('i18n_login_popup'))) - else: - self.w(self._cw._('anonymous')) - self.w(u' [%s]' - % (self._cw.build_url('login'), self._cw._('login'))) + w = self.w + w(self._cw._('anonymous')) + w(u"""[%s]""" + % (self.loginboxid, self._cw._('i18n_login_popup'))) + self.wview('logform', rset=self.cw_rset, id=self.loginboxid, + klass='hidden', title=False, showmessage=False) +class HTTPAnonUserLink(_UserLink): + __select__ = _UserLink.__select__ & configuration_values('auth-mode', 'http') & anonymous_user() + loginboxid = 'popupLoginBox' + + def call(self): + w = self.w + w(self._cw._('anonymous')) + # this redirects to the 'login' controller which in turn + # will raise a 401/Unauthorized + w(u' [%s]' + % (self._cw.build_url('login'), self._cw._('login'))) class UserLink(_UserLink): __select__ = _UserLink.__select__ & authenticated_user() diff -r 34317f395619 -r 72ba82a26e05 web/views/basetemplates.py --- a/web/views/basetemplates.py Mon Oct 04 18:55:57 2010 +0200 +++ b/web/views/basetemplates.py Mon Oct 04 18:56:05 2010 +0200 @@ -276,11 +276,12 @@ logo = self._cw.vreg['components'].select_or_none('logo', self._cw, rset=self.cw_rset) if logo and logo.cw_propval('visible'): - self.w(u'\n') - self.w(u'\n') - self.w(u'\n') + w = self.w + w(u'\n') + w(u'\n') + w(u'\n') # page parts templates ######################################################## @@ -335,35 +336,31 @@ def main_header(self, view): """build the top menu with authentification info and the rql box""" - self.w(u'\n') - self.w(u'') + w(u'\n') def state_header(self): state = self._cw.search_state @@ -380,7 +377,6 @@ return self.w(u'
%s
' % msg) - class HTMLPageFooter(View): """default html page footer: include footer actions """ @@ -438,12 +434,16 @@ __regid__ = 'logform' domid = 'loginForm' needs_css = ('cubicweb.login.css',) + onclick = "javascript: cw.htmlhelpers.popupLoginBox('%s', '%s');" # 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'})] + attrs={'class': 'loginButton'}), + fw.ResetButton(label=_('cancel'), + attrs={'class': 'loginButton', + 'onclick': onclick % ('popupLoginBox', '__login')}),] def form_action(self): if self.action is None: @@ -452,32 +452,35 @@ class LogFormView(View): + # XXX an awfull lot of hardcoded assumptions there + # makes it unobvious to reuse/specialize __regid__ = 'logform' __select__ = match_kwargs('id', 'klass') title = 'log in' def call(self, id, klass, title=True, showmessage=True): - self.w(u'
' % (id, klass)) + w = self.w + w(u'
' % (id, klass)) if title: stitle = self._cw.property_value('ui.site-title') if stitle: stitle = xml_escape(stitle) else: stitle = u' ' - self.w(u'
%s
' % stitle) - self.w(u'
\n') + w(u'
%s
' % stitle) + w(u'
\n') if showmessage and self._cw.message: - self.w(u'
%s
\n' % self._cw.message) + w(u'
%s
\n' % self._cw.message) config = self._cw.vreg.config if config['auth-mode'] != 'http': self.login_form(id) # Cookie authentication - self.w(u'
') + w(u'
') if self._cw.https and config.anonymous_user()[0]: path = xml_escape(config['base-url'] + self._cw.relative_path()) - self.w(u'\n' - % (path, self._cw._('No account? Try public access at %s') % path)) - self.w(u'
\n') + w(u'\n' + % (path, self._cw._('No account? Try public access at %s') % path)) + w(u'
\n') def login_form(self, id): cw = self._cw