changeset 1274:8c275efc6e8c

refactored and cleaned up user module code and tests implemented a separate UserProfile class, that just deals with separately holding the profile values (also loading, saving, using defaults, tracking changes). the User object now has a .profile attribute of class UserProfile. for ease of use and compatibility, there are quite some properties defined on the User object that just access some specific value in the profile, e.g. reading u.name will read u.profile[NAME]. Most properties are read-only. commented User.exists (UserProfile.exists) code, let's see if that is used.
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sat, 03 Mar 2012 01:22:45 +0100
parents 4c4ee1f25e9f
children af630156f4ff
files MoinMoin/_tests/__init__.py MoinMoin/_tests/test_user.py MoinMoin/app.py MoinMoin/apps/frontend/_tests/test_frontend.py MoinMoin/apps/frontend/views.py MoinMoin/config/default.py MoinMoin/constants/keys.py MoinMoin/security/_tests/test_textcha.py MoinMoin/storage/middleware/indexing.py MoinMoin/storage/middleware/validation.py MoinMoin/user.py
diffstat 11 files changed, 257 insertions(+), 232 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/_tests/__init__.py	Sun Feb 26 18:33:28 2012 +0100
+++ b/MoinMoin/_tests/__init__.py	Sat Mar 03 01:22:45 2012 +0100
@@ -33,7 +33,7 @@
         Thus, for testing purposes (e.g. if you need delete rights), it is
         easier to use become_trusted().
     """
-    flaskg.user.name = username
+    flaskg.user.profile[NAME] = username
     flaskg.user.may.name = username
     flaskg.user.valid = 1
 
@@ -41,7 +41,6 @@
 def become_trusted(username=u"TrustedUser"):
     """ modify flaskg.user to make the user valid and trusted, so it is in acl group Trusted """
     become_valid(username)
-    flaskg.user.auth_trusted = True
     flaskg.user.auth_method = app.cfg.auth_methods_trusted[0]
 
 
--- a/MoinMoin/_tests/test_user.py	Sun Feb 26 18:33:28 2012 +0100
+++ b/MoinMoin/_tests/test_user.py	Sat Mar 03 01:22:45 2012 +0100
@@ -22,14 +22,20 @@
         name = u"foo"
         password = u"barbaz4711"
         email = u"foo@example.org"
-        # first create a user
+        # nonexisting user
+        u = user.User(name=name, password=password)
+        assert u.name == name
+        assert not u.valid
+        assert not u.exists()
+        # create a user
         ret = user.create_user(name, password, email, validate=False)
         assert ret is None, "create_user returned: {0}".format(ret)
-        # now try to use it
+        # existing user
         u = user.User(name=name, password=password)
         assert u.name == name
         assert u.email == email
         assert u.valid
+        assert u.exists()
 
 
 class TestLoginWithPassword(object):
@@ -202,13 +208,11 @@
         upgrades to {SSHA256}.
         """
         name = u'/no such user/'
-        #pass = MoinMoin
-        #salr = 12345
+        # pass = 'MoinMoin', salt = '12345'
         password = '{SSHA}xkDIIx1I7A4gC98Vt/+UelIkTDYxMjM0NQ=='
         self.createUser(name, password, True)
 
-        # User is not required to be valid
-        theuser = user.User(name=name, password='12345')
+        theuser = user.User(name=name, password='MoinMoin')
         assert theuser.enc_password[:9] == '{SSHA256}'
 
     def test_upgrade_password_from_sha_to_ssha256(self):
@@ -220,7 +224,6 @@
         password = '{SHA}jLIjfQZ5yojbZGTqxg2pY0VROWQ=' # 12345
         self.createUser(name, password, True)
 
-        # User is not required to be valid
         theuser = user.User(name=name, password='12345')
         assert theuser.enc_password[:9] == '{SSHA256}'
 
@@ -235,7 +238,6 @@
         password = '{APR1}$apr1$NG3VoiU5$PSpHT6tV0ZMKkSZ71E3qg.' # 12345
         self.createUser(name, password, True)
 
-        # User is not required to be valid
         theuser = user.User(name=name, password='12345')
         assert theuser.enc_password[:9] == '{SSHA256}'
 
@@ -249,7 +251,6 @@
         password = '{MD5}$1$salt$etVYf53ma13QCiRbQOuRk/' # 12345
         self.createUser(name, password, True)
 
-        # User is not required to be valid
         theuser = user.User(name=name, password='12345')
         assert theuser.enc_password[:9] == '{SSHA256}'
 
@@ -264,21 +265,9 @@
         password = '{DES}gArsfn7O5Yqfo' # 12345
         self.createUser(name, password, True)
 
-        # User is not required to be valid
         theuser = user.User(name=name, password='12345')
         assert theuser.enc_password[:9] == '{SSHA256}'
 
-    def test_for_email_attribute_by_name(self):
-        """
-        checks for no access to the email attribute by getting the user object from name
-        """
-        name = u"__TestUser__"
-        password = u"ekfdweurwerh"
-        email = u"__TestUser__@moinhost"
-        self.createUser(name, password, email=email)
-        theuser = user.User(name=name)
-        assert theuser.email is None
-
     # Bookmarks -------------------------------------------------------
 
     def test_bookmark(self):
@@ -312,7 +301,6 @@
         password = name
         self.createUser(name, password)
         theUser = user.User(name=name, password=password)
-        theUser.subscribe(pagename)
 
         # no quick links exist yet
         result_before = theUser.getQuickLinks()
@@ -321,26 +309,16 @@
         result = theUser.isQuickLinkedTo([pagename])
         assert not result
 
-        # quicklinks for the user - theUser exist now
-        theUser.quicklinks = [pagename]
-        result_after = theUser.getQuickLinks()
-        expected = [u'Test_page_quicklink']
-        assert result_after == expected
-
         # test for addQuicklink()
         theUser.addQuicklink(u'Test_page_added')
         result_on_addition = theUser.getQuickLinks()
-        expected = [u'Test_page_quicklink', u'MoinTest:Test_page_added']
+        expected = [u'MoinTest:Test_page_added']
         assert result_on_addition == expected
 
-        # user should be quicklinked to [pagename]
-        result = theUser.isQuickLinkedTo([pagename])
-        assert result
-
         # previously added page u'Test_page_added' is removed
         theUser.removeQuicklink(u'Test_page_added')
         result_on_removal = theUser.getQuickLinks()
-        expected = [u'Test_page_quicklink']
+        expected = []
         assert result_on_removal == expected
 
     # Trail -----------------------------------------------------------
--- a/MoinMoin/app.py	Sun Feb 26 18:33:28 2012 +0100
+++ b/MoinMoin/app.py	Sat Mar 03 01:22:45 2012 +0100
@@ -214,10 +214,6 @@
         userobj = user.User(auth_method='invalid')
     # if we have a valid user we store it in the session
     if userobj.valid:
-        # TODO: auth_trusted should be set by the auth method (auth class
-        # could have a param where the admin could tell whether he wants to
-        # trust it)
-        userobj.auth_trusted = userobj.auth_method in app.cfg.auth_methods_trusted
         session['user.itemid'] = userobj.itemid
         session['user.auth_method'] = userobj.auth_method
         session['user.auth_attribs'] = userobj.auth_attribs
--- a/MoinMoin/apps/frontend/_tests/test_frontend.py	Sun Feb 26 18:33:28 2012 +0100
+++ b/MoinMoin/apps/frontend/_tests/test_frontend.py	Sat Mar 03 01:22:45 2012 +0100
@@ -206,6 +206,8 @@
 
 
 class TestUsersettings(object):
+    reinit_storage = True  # avoid username / email collisions
+
     def setup_method(self, method):
         # Save original user
         self.saved_user = flaskg.user
@@ -291,24 +293,7 @@
     def createUser(self, name, password, pwencoded=False, email=None):
         """ helper to create test user
         """
-        # Create user
-        self.user = user.User()
-        self.user.name = name
-        self.user.email = email
-        if not pwencoded:
-            password = crypto.crypt_password(password)
-        self.user.enc_password = password
+        if email is None:
+            email = "user@example.org"
+        user.create_user(name, password, email, is_encrypted=pwencoded)
 
-        # Validate that we are not modifying existing user data file!
-        if self.user.exists():
-            self.user = None
-            pytest.skip("Test user exists, will not override existing user data file!")
-
-        # Save test user
-        self.user.save()
-
-        # Validate user creation
-        if not self.user.exists():
-            self.user = None
-            pytest.skip("Can't create test user")
-
--- a/MoinMoin/apps/frontend/views.py	Sun Feb 26 18:33:28 2012 +0100
+++ b/MoinMoin/apps/frontend/views.py	Sat Mar 03 01:22:45 2012 +0100
@@ -50,9 +50,8 @@
 from MoinMoin.items import Item, NonExistent
 from MoinMoin.items import ROWS_META, COLS, ROWS_DATA
 from MoinMoin import config, user, util, wikiutil
-from MoinMoin.config import ACTION, COMMENT, WIKINAME, CONTENTTYPE, ITEMLINKS, ITEMTRANSCLUSIONS, NAME, NAME_EXACT, \
-                            CONTENTTYPE_GROUPS, MTIME, TAGS, ITEMID, REVID, USERID, CURRENT, CONTENT, \
-                            ALL_REVS, LATEST_REVS
+from MoinMoin.config import CONTENTTYPE_GROUPS
+from MoinMoin.constants.keys import *
 from MoinMoin.util import crypto
 from MoinMoin.util.interwiki import url_for_item
 from MoinMoin.search import SearchForm, ValidSearch
@@ -1010,7 +1009,7 @@
         u = user.User(auth_username=request.values['username'])
         token = request.values['token']
     if u and u.disabled and u.validate_recovery_token(token):
-        u.disabled = False
+        u.profile[DISABLED] = False
         u.save()
         flash(_("Your account has been activated, you can log in now."), "info")
     else:
@@ -1343,7 +1342,7 @@
                 # successfully modified everything
                 success = True
                 if part == 'password':
-                    flaskg.user.enc_password = crypto.crypt_password(form['password1'].value)
+                    flaskg.user.set_password(form['password1'].value)
                     flaskg.user.save()
                     response['flash'].append((_("Your password has been changed."), "info"))
                 else:
@@ -1364,10 +1363,13 @@
                             success = False
                     if success:
                         user_old_email = flaskg.user.email
-                        form.update_object(flaskg.user, omit=['submit']) # don't save submit button value :)
+                        d = dict(form.value)
+                        d.pop('submit')
+                        for k, v in d.items():
+                            flaskg.user.profile[k] = v
                         if part == 'notification' and app.cfg.user_email_verification and form['email'].value != user_old_email:
                             # disable account
-                            flaskg.user.disabled = True
+                            flaskg.user.profile[DISABLED] = True
                             # send verification mail
                             is_ok, msg = flaskg.user.mailVerificationLink()
                             if is_ok:
@@ -1377,8 +1379,8 @@
                                 response['redirect'] = url_for('.show_root')
                             else:
                                 # sending the verification email didn't work. reset email change and alert the user.
-                                flaskg.user.disabled = False
-                                flaskg.user.email = user_old_email
+                                flaskg.user.profile[DISABLED] = False
+                                flaskg.user.profile[EMAIL] = user_old_email
                                 flaskg.user.save()
                                 response['flash'].append((_('Your email address was not changed because sending the verification email failed. Please try again later.'), "error"))
                         else:
--- a/MoinMoin/config/default.py	Sun Feb 26 18:33:28 2012 +0100
+++ b/MoinMoin/config/default.py	Sat Mar 03 01:22:45 2012 +0100
@@ -448,6 +448,7 @@
         show_comments=False,
         want_trivial=False,
         disabled=False,
+        bookmarks={},
         quicklinks=[],
         subscribed_items=[],
         email_subscribed_events=[
--- a/MoinMoin/constants/keys.py	Sun Feb 26 18:33:28 2012 +0100
+++ b/MoinMoin/constants/keys.py	Sat Mar 03 01:22:45 2012 +0100
@@ -62,6 +62,20 @@
 # stuff from user profiles / for whoosh index
 EMAIL = "email"
 OPENID = "openid"
+ALIASNAME = "aliasname"
+THEME_NAME = "theme_name"
+LOCALE = "locale"
+TIMEZONE = "timezone"
+ENC_PASSWORD = "enc_password"
+SUBSCRIBED_ITEMS = "subscribed_items"
+BOOKMARKS = "bookmarks"
+QUICKLINKS = "quicklinks"
+RECOVERPASS_KEY = "recoverpass_key"
+EDIT_ON_DOUBLECLICK = "edit_on_doubleclick"
+SHOW_COMMENTS = "show_comments"
+MAILTO_AUTHOR = "mailto_author"
+RESULTS_PER_PAGE = "results_per_page"
+DISABLED = "disabled"
 
 # index names
 LATEST_REVS = 'latest_revs'
--- a/MoinMoin/security/_tests/test_textcha.py	Sun Feb 26 18:33:28 2012 +0100
+++ b/MoinMoin/security/_tests/test_textcha.py	Sat Mar 03 01:22:45 2012 +0100
@@ -8,8 +8,11 @@
 from flask import current_app as app
 from flask import g as flaskg
 
+import pytest
+
 from MoinMoin.security.textcha import TextCha, TextChaValid, TextChaizedForm
-import pytest
+from MoinMoin.constants.keys import LOCALE
+
 
 class TestTextCha(object):
     """ Test: class TextCha """
@@ -20,13 +23,13 @@
                             'What is the question?': 'Test_Answer'}
                        }
         cfg.secrets['security/textcha'] = "test_secret"
-        flaskg.user.locale = 'test_user_locale'
+        flaskg.user.profile[LOCALE] = 'test_user_locale'
 
     def teardown_method(self, method):
         cfg = app.cfg
         cfg.textchas = None
         cfg.secrets.pop('security/textcha')
-        flaskg.user.locale = None
+        flaskg.user.profile[LOCALE] = None
 
     def test_textcha(self):
         """ test for textchas and its attributes """
@@ -72,7 +75,7 @@
 
     def test_amend_form(self):
         # textchas are disabled for 'some_locale'
-        flaskg.user.locale = 'some_locale'
+        flaskg.user.profile[LOCALE] = 'some_locale'
         test_form = TextChaizedForm()
         test_form['textcha_question'].value = None
         textcha_obj = TextCha(test_form)
@@ -92,13 +95,13 @@
                             {'Good Question': 'Good Answer'}
                        }
         cfg.secrets['security/textcha'] = "test_secret"
-        flaskg.user.locale = 'test_user_locale'
+        flaskg.user.profile[LOCALE] = 'test_user_locale'
 
     def teardown_method(self, method):
         cfg = app.cfg
         cfg.textchas = None
         cfg.secrets.pop('security/textcha')
-        flaskg.user.locale = None
+        flaskg.user.profile[LOCALE] = None
 
     class Element:
         def __init__(self):
--- a/MoinMoin/storage/middleware/indexing.py	Sun Feb 26 18:33:28 2012 +0100
+++ b/MoinMoin/storage/middleware/indexing.py	Sat Mar 03 01:22:45 2012 +0100
@@ -88,6 +88,7 @@
 from MoinMoin.themes import utctimestamp
 from MoinMoin.util.crypto import make_uuid
 from MoinMoin.storage.middleware.validation import ContentMetaSchema, UserMetaSchema
+from MoinMoin.storage.error import NoSuchItemError, ItemAlreadyExistsError
 
 
 INDEXES = [LATEST_REVS, ALL_REVS, ]
@@ -741,7 +742,7 @@
         item = cls(indexer, **query)
         if not item:
             return item
-        raise ItemAlreadyExists(repr(query))
+        raise ItemAlreadyExistsError(repr(query))
 
     @classmethod
     def existing(cls, indexer, **query):
@@ -751,7 +752,7 @@
         item = cls(indexer, **query)
         if item:
             return item
-        raise ItemDoesNotExist(repr(query))
+        raise NoSuchItemError(repr(query))
 
     def __nonzero__(self):
         """
--- a/MoinMoin/storage/middleware/validation.py	Sun Feb 26 18:33:28 2012 +0100
+++ b/MoinMoin/storage/middleware/validation.py	Sat Mar 03 01:22:45 2012 +0100
@@ -26,7 +26,7 @@
 
 import time
 
-from flatland import Dict, List, Unset, Integer, String
+from flatland import Dict, List, Unset, Boolean, Integer, String
 
 from MoinMoin.constants import keys
 from MoinMoin.config import CONTENTTYPE_DEFAULT, CONTENTTYPE_USER
@@ -344,13 +344,13 @@
     String.named('timezone').using(optional=True),
     String.named('locale').using(optional=True),
     String.named('css_url').using(optional=True),
-    Integer.named('disabled').using(optional=True),
-    Integer.named('want_trivial').using(optional=True),
-    Integer.named('show_comments').using(optional=True),
     Integer.named('results_per_page').using(optional=True),
     Integer.named('edit_rows').using(optional=True),
-    Integer.named('edit_on_doubleclick').using(optional=True),
-    Integer.named('mailto_author').using(optional=True),
+    Boolean.named('disabled').using(optional=True),
+    Boolean.named('want_trivial').using(optional=True),
+    Boolean.named('show_comments').using(optional=True),
+    Boolean.named('edit_on_doubleclick').using(optional=True),
+    Boolean.named('mailto_author').using(optional=True),
     List.named('quicklinks').of(String.named('quicklinks')).using(optional=True),
     List.named('subscribed_items').of(String.named('subscribed_item')).using(optional=True),
     List.named('email_subscribed_events').of(String.named('email_subscribed_event')).using(optional=True),
--- a/MoinMoin/user.py	Sun Feb 26 18:33:28 2012 +0100
+++ b/MoinMoin/user.py	Sat Mar 03 01:22:45 2012 +0100
@@ -1,5 +1,5 @@
 # Copyright: 2000-2004 Juergen Hermann <jh@web.de>
-# Copyright: 2003-2011 MoinMoin:ThomasWaldmann
+# Copyright: 2003-2012 MoinMoin:ThomasWaldmann
 # Copyright: 2007 MoinMoin:JohannesBerg
 # Copyright: 2007 MoinMoin:HeinrichWendel
 # Copyright: 2008 MoinMoin:ChristopherDenter
@@ -34,8 +34,8 @@
 from whoosh.query import Term, And, Or
 
 from MoinMoin import config, wikiutil
-from MoinMoin.config import WIKINAME, NAME, NAME_EXACT, ITEMID, ACTION, CONTENTTYPE, \
-                            EMAIL, OPENID, CURRENT, MTIME, CONTENTTYPE_USER
+from MoinMoin.config import CONTENTTYPE_USER
+from MoinMoin.constants.keys import *
 from MoinMoin.i18n import _, L_, N_
 from MoinMoin.util.interwiki import getInterwikiHome, getInterwikiName, is_local_wiki
 from MoinMoin.util.crypto import crypt_password, upgrade_password, valid_password, \
@@ -47,51 +47,50 @@
     """ create a user """
     # Create user profile
     theuser = User(auth_method="new-user")
-    theuser.name = unicode(username)
 
     # Don't allow creating users with invalid names
-    if validate and not isValidName(theuser.name):
+    if validate and not isValidName(username):
         return _("""Invalid user name '%(name)s'.
 Name may contain any Unicode alpha numeric character, with optional one
-space between words. Group page name is not allowed.""", name=theuser.name)
+space between words. Group page name is not allowed.""", name=username)
 
     # Name required to be unique. Check if name belong to another user.
-    if validate and search_users(name_exact=theuser.name):
+    if validate and search_users(name_exact=username):
         return _("This user name already belongs to somebody else.")
 
+    theuser.profile[NAME] = unicode(username)
+
     pw_checker = app.cfg.password_checker
     if validate and pw_checker:
-        pw_error = pw_checker(theuser.name, password)
+        pw_error = pw_checker(username, password)
         if pw_error:
             return _("Password not acceptable: %(msg)s", msg=pw_error)
 
-    # Encode password
     try:
-        if is_encrypted:
-            theuser.enc_password = password
-        else:
-            theuser.enc_password = crypt_password(password)
+        theuser.set_password(password, is_encrypted)
     except UnicodeError as err:
         # Should never happen
         return "Can't encode password: %(msg)s" % dict(msg=str(err))
 
     # try to get the email, for new users it is required
-    theuser.email = email
-    if validate and not theuser.email:
+    if validate and not email:
         return _("Please provide your email address. If you lose your"
                  " login information, you can get it by email.")
 
     # Email should be unique - see also MoinMoin/script/accounts/moin_usercheck.py
-    if validate and theuser.email and app.cfg.user_email_unique:
-        if search_users(email=theuser.email):
+    if validate and email and app.cfg.user_email_unique:
+        if search_users(email=email):
             return _("This email already belongs to somebody else.")
 
+    theuser.profile[EMAIL] = email
+
     # Openid should be unique
-    theuser.openid = openid
-    if validate and theuser.openid and search_users(openid=theuser.openid):
+    if validate and openid and search_users(openid=openid):
         return _('This OpenID already belongs to somebody else.')
 
-    theuser.disabled = is_disabled
+    theuser.profile[OPENID] = openid
+
+    theuser.profile[DISABLED] = is_disabled
 
     # save data
     theuser.save()
@@ -101,14 +100,20 @@
     return flaskg.unprotected_storage
 
 
+def update_user_query(**q):
+    USER_QUERY_STDARGS = {
+        CONTENTTYPE: CONTENTTYPE_USER,
+        WIKINAME: app.cfg.interwikiname,  # XXX for now, search only users of THIS wiki
+                                          # maybe add option to not index wiki users
+                                          # separately, but share them in the index also.
+    }
+    q.update(USER_QUERY_STDARGS)
+    return q
+
+
 def search_users(**q):
     """ Searches for a users with given query keys/values """
-    q.update({
-        WIKINAME: app.cfg.interwikiname, # XXX for now, search only users of THIS wiki
-                                         # maybe add option to not index wiki users
-                                         # separately, but share them in the index also.
-        CONTENTTYPE: CONTENTTYPE_USER,
-    })
+    q = update_user_query(**q)
     backend = get_user_backend()
     docs = backend.documents(**q)
     return list(docs)
@@ -173,6 +178,65 @@
     return (name == normalized) and not wikiutil.isGroupItem(name)
 
 
+class UserProfile(object):
+    """ A User Profile"""
+
+    def __init__(self, **q):
+        self._defaults = copy.deepcopy(app.cfg.user_defaults)
+        self._meta = {}
+        self._stored = False
+        self._changed = False
+        if q:
+            self.load(**q)
+
+    @property
+    def stored(self):
+        return self._stored
+
+    def __getitem__(self, name):
+        """
+        get a value from the profile or,
+        if not present, from the configured defaults
+        """
+        try:
+            return self._meta[name]
+        except KeyError:
+            return self._defaults[name]
+
+    def __setitem__(self, name, value):
+        """
+        set a value, update changed status
+        """
+        prev_value = self._meta.get(name)
+        self._meta[name] = value
+        if value != prev_value:
+            self._changed = True
+
+    def load(self, **q):
+        """
+        load a user profile, the query q can use any indexed (unique) field
+        """
+        q = update_user_query(**q)
+        item = get_user_backend().existing_item(**q)
+        rev = item[CURRENT]
+        self._meta = dict(rev.meta)
+        self._stored = True
+        self._changed = False
+
+    def save(self):
+        """
+        save a user profile (if it was changed since loading it)
+        """
+        if self._changed:
+            self[CONTENTTYPE] = CONTENTTYPE_USER
+            q = {ITEMID: self[ITEMID]}
+            q = update_user_query(**q)
+            item = get_user_backend().get_item(**q)
+            item.store_revision(self._meta, StringIO(''), overwrite=True)
+            self._stored = True
+            self._changed = False
+
+
 class User(object):
     """ A MoinMoin User """
 
@@ -191,54 +255,31 @@
                                changeable by preferences, default: ().
                                First tuple element was used for authentication.
         """
-        self._user_backend = get_user_backend()
-
+        self.profile = UserProfile()
         self._cfg = app.cfg
-        self.valid = 0
-        self.itemid = uid
-        self.auth_username = auth_username
+        self.valid = False
         self.auth_method = kw.get('auth_method', 'internal')
         self.auth_attribs = kw.get('auth_attribs', ())
-        self.bookmarks = {} # interwikiname: bookmark
-
-        self.__dict__.update(copy.deepcopy(self._cfg.user_defaults))
-
-        if name:
-            self.name = name
-        elif auth_username: # this is needed for user autocreate
-            self.name = auth_username
-
-        self.recoverpass_key = None
-
-        if password:
-            self.enc_password = crypt_password(password)
-
-        self._stored = False
-
-        # attrs not saved to profile
 
-        # we got an already authenticated username:
-        check_password = None
-        if not self.itemid and self.auth_username:
-            users = search_users(name_exact=self.auth_username)
+        _name = name or auth_username
+
+        itemid = uid
+        if not itemid and auth_username:
+            users = search_users(name_exact=auth_username)
             if users:
-                self.itemid = users[0].meta[ITEMID]
-            if not password is None:
-                check_password = password
-        if self.itemid:
-            self.load_from_id(check_password)
-        elif self.name and self.name != 'anonymous':
-            users = search_users(name_exact=self.name)
+                itemid = users[0].meta[ITEMID]
+        if not itemid and _name and _name != 'anonymous':
+            users = search_users(name_exact=_name)
             if users:
-                self.itemid = users[0].meta[ITEMID]
-            if self.itemid:
-                # no password given should fail
-                self.load_from_id(password or u'')
-        # Still no ID - make new user
-        if not self.itemid:
-            self.itemid = make_uuid()
+                itemid = users[0].meta[ITEMID]
+        if itemid:
+            self.load_from_id(itemid, password)
+        else:
+            self.profile[ITEMID] = make_uuid()
+            if _name:
+                self.profile[NAME] = _name
             if password is not None:
-                self.enc_password = crypt_password(password)
+                self.set_password(password)
 
         # "may" so we can say "if user.may.read(pagename):"
         if self._cfg.SecurityPolicy:
@@ -252,13 +293,32 @@
             self.__class__.__module__, self.__class__.__name__, id(self),
             self.name, self.itemid, self.valid)
 
+    def __getattr__(self, name):
+        """
+        delegate some lookups into the .profile
+        """
+        if name in [NAME, DISABLED, ITEMID, ALIASNAME, ENC_PASSWORD, EMAIL, OPENID,
+                    MAILTO_AUTHOR, SHOW_COMMENTS, RESULTS_PER_PAGE, EDIT_ON_DOUBLECLICK,
+                    THEME_NAME, LOCALE, TIMEZONE,
+                   ]:
+            return self.profile[name]
+        else:
+            return object.__getattr__(self, name)
+
+    @property
+    def auth_trusted(self):
+        # TODO: auth_trusted should be set by the auth method (auth class
+        # could have a param where the admin could tell whether he wants to
+        # trust it)
+        return self.auth_method in app.cfg.auth_methods_trusted
+
     @property
     def language(self):
         l = self._cfg.language_default
-        # .locale is either None or something like 'en_US'
-        if self.locale is not None:
+        locale = self.locale  # is either None or something like 'en_US'
+        if locale is not None:
             try:
-                l = parse_locale(self.locale)[0]
+                l = parse_locale(locale)[0]
             except ValueError:
                 pass
         return l
@@ -272,11 +332,12 @@
 
         theme = get_current_theme()
 
-        if not self.email:
+        email = self.email
+        if not email:
             return static_file_url(theme, theme.info.get('default_avatar', 'img/default_avatar.png'))
 
         param = {}
-        param['gravatar_id'] = hashlib.md5(self.email.lower()).hexdigest()
+        param['gravatar_id'] = hashlib.md5(email.lower()).hexdigest()
 
         param['default'] = static_file_url(theme,
                                            theme.info.get('default_avatar', 'img/default_avatar.png'),
@@ -298,58 +359,40 @@
             self.save() # yes, create/update user profile
 
     def exists(self):
-        """ Do we have a user account for this user?
+        """ Do we have a user profile for this user?
 
         :rtype: bool
         :returns: true, if we have a user account
         """
-        return self._user_backend.has_item(self.name)
+        return self.profile.stored
 
-    def load_from_id(self, password=None):
+    def load_from_id(self, itemid, password=None):
         """ Load user account data from disk.
 
-        Can only load user data if the id number is already known.
-
-        This loads all member variables, except "id" and "valid" and
-        those starting with an underscore.
-
         :param password: If not None, then the given password must match the
                          password in the user account file.
         """
         try:
-            item = self._user_backend.get_item(itemid=self.itemid)
-            rev = item[CURRENT]
-        except KeyError: # was: (NoSuchItemError, NoSuchRevisionError):
+            self.profile.load(itemid=itemid)
+        except (NoSuchItemError, NoSuchRevisionError):
             return
 
-        user_data = dict(rev.meta)
-
         # Validate data from user file. In case we need to change some
         # values, we set 'changed' flag, and later save the user data.
-        changed = 0
+        changed = False
 
         if password is not None:
             # Check for a valid password, possibly changing storage
-            valid, changed = self._validatePassword(user_data, password)
+            valid, changed = self._validatePassword(self.profile, password)
             if not valid:
                 return
 
-        # Copy user data into user object
-        for key, val in user_data.items():
-            if isinstance(val, tuple):
-                val = list(val)
-            vars(self)[key] = val
-
         if not self.disabled:
-            self.valid = 1
-
-        # Mark this user as stored so saves don't send
-        # the "user created" event
-        self._stored = True
+            self.valid = True
 
         # If user data has been changed, save fixed user data.
         if changed:
-            self.save()
+            self.profile.save()
 
     def _validatePassword(self, data, password):
         """
@@ -362,7 +405,7 @@
         :rtype: 2 tuple (bool, bool)
         :returns: password is valid, enc_password changed
         """
-        pw_hash = data['enc_password']
+        pw_hash = data[ENC_PASSWORD]
 
         # If we have no password set, we don't accept login with username.
         # Require non-empty password.
@@ -377,38 +420,26 @@
         if not new_pw_hash:
             return True, False
 
-        data['enc_password'] = new_pw_hash
+        data[ENC_PASSWORD] = new_pw_hash
         return True, True
 
-    def persistent_items(self):
-        """ items we want to store into the user profile """
-        nonpersistent_keys = ['valid', 'may', 'auth_username',
-                              'password', 'password2',
-                              'auth_method', 'auth_attribs', 'auth_trusted',
-                             ]
-        return [(key, value) for key, value in vars(self).items()
-                    if key not in nonpersistent_keys and key[0] != '_' and value is not None]
+    def set_password(self, password, is_encrypted=False):
+        if not is_encrypted:
+            password = crypt_password(password)
+        self.profile[ENC_PASSWORD] = password
 
     def save(self):
         """
         Save user account data to user account file on disk.
         """
-        backend_name = self.name # XXX maybe UserProfile/<name> later
-        item = self._user_backend[backend_name]
-        meta = {}
-        for key, value in self.persistent_items():
-            if isinstance(value, list):
-                value = tuple(value)
-            meta[key] = value
-        meta[CONTENTTYPE] = CONTENTTYPE_USER
-        item.store_revision(meta, StringIO(''), overwrite=True)
+        exists = self.exists
+        self.profile.save()
 
         if not self.disabled:
-            self.valid = 1
+            self.valid = True
 
-        if not self._stored:
-            self._stored = True
-            # XXX UserCreatedEvent
+        if not exists:
+            pass # XXX UserCreatedEvent
         else:
             pass #  XXX UserChangedEvent
 
@@ -426,7 +457,7 @@
         :param tm: timestamp
         """
         if self.valid:
-            self.bookmarks[self._cfg.interwikiname] = int(tm)
+            self.profile[BOOKMARKS][self._cfg.interwikiname] = int(tm)
             self.save()
 
     def getBookmark(self):
@@ -438,7 +469,7 @@
         bm = None
         if self.valid:
             try:
-                bm = self.bookmarks[self._cfg.interwikiname]
+                bm = self.profile[BOOKMARKS][self._cfg.interwikiname]
             except (ValueError, KeyError):
                 pass
         return bm
@@ -451,7 +482,7 @@
         """
         if self.valid:
             try:
-                del self.bookmarks[self._cfg.interwikiname]
+                del self.profile[BOOKMARKS][self._cfg.interwikiname]
             except KeyError:
                 return 1
             self.save()
@@ -467,7 +498,7 @@
         :rtype: list
         :returns: pages this user has subscribed to
         """
-        return self.subscribed_items
+        return self.profile[SUBSCRIBED_ITEMS]
 
     def isSubscribedTo(self, pagelist):
         """ Check if user subscription matches any page in pagelist.
@@ -518,8 +549,9 @@
         :returns: if page was subscribed
         """
         pagename = getInterwikiName(pagename)
-        if pagename not in self.subscribed_items:
-            self.subscribed_items.append(pagename)
+        subscribed_items = self.profile[SUBSCRIBED_ITEMS]
+        if pagename not in subscribed_items:
+            subscribed_items.append(pagename)
             self.save()
             # XXX SubscribedToPageEvent
             return True
@@ -538,6 +570,8 @@
         must check if the user is still subscribed to the page after we
         try to remove names from the list.
 
+        TODO: remove the non-interwiki kind of subscriptions
+
         :param pagename: name of the page to subscribe
         :type pagename: unicode
         :rtype: bool
@@ -545,13 +579,14 @@
             regular expression that match, it will always fail.
         """
         changed = False
-        if pagename in self.subscribed_items:
-            self.subscribed_items.remove(pagename)
+        subscribed_items = self.profile[SUBSCRIBED_ITEMS]
+        if pagename in subscribed_items:
+            subscribed_items.remove(pagename)
             changed = True
 
         interWikiName = getInterwikiName(pagename)
-        if interWikiName and interWikiName in self.subscribed_items:
-            self.subscribed_items.remove(interWikiName)
+        if interWikiName and interWikiName in subscribed_items:
+            subscribed_items.remove(interWikiName)
             changed = True
 
         if changed:
@@ -564,14 +599,18 @@
     def getQuickLinks(self):
         """ Get list of pages this user wants in the navibar
 
+        TODO: implement as a property
+
         :rtype: list
         :returns: quicklinks from user account
         """
-        return self.quicklinks
+        return self.profile[QUICKLINKS]
 
     def isQuickLinkedTo(self, pagelist):
         """ Check if user quicklink matches any page in pagelist.
 
+        TODO: remove the non-interwiki kind of subscriptions
+
         :param pagelist: list of pages to check for quicklinks
         :rtype: bool
         :returns: if user has quicklinked any page in pagelist
@@ -579,11 +618,12 @@
         if not self.valid:
             return False
 
+        quicklinks = self.getQuickLinks()
         for pagename in pagelist:
-            if pagename in self.quicklinks:
+            if pagename in quicklinks:
                 return True
             interWikiName = getInterwikiName(pagename)
-            if interWikiName and interWikiName in self.quicklinks:
+            if interWikiName and interWikiName in quicklinks:
                 return True
 
         return False
@@ -594,23 +634,26 @@
         If the wiki has an interwiki name, all links are saved as
         interwiki names. If not, as simple page name.
 
+        TODO: remove the non-interwiki kind of subscriptions
+
         :param pagename: page name
         :type pagename: unicode
         :rtype: bool
         :returns: if pagename was added
         """
         changed = False
+        quicklinks = self.getQuickLinks()
         interWikiName = getInterwikiName(pagename)
         if interWikiName:
-            if pagename in self.quicklinks:
-                self.quicklinks.remove(pagename)
+            if pagename in quicklinks:
+                quicklinks.remove(pagename)
                 changed = True
-            if interWikiName not in self.quicklinks:
-                self.quicklinks.append(interWikiName)
+            if interWikiName not in quicklinks:
+                quicklinks.append(interWikiName)
                 changed = True
         else:
-            if pagename not in self.quicklinks:
-                self.quicklinks.append(pagename)
+            if pagename not in quicklinks:
+                quicklinks.append(pagename)
                 changed = True
 
         if changed:
@@ -622,18 +665,21 @@
 
         Remove both interwiki and simple name from quicklinks.
 
+        TODO: remove the non-interwiki kind of subscriptions
+
         :param pagename: page name
         :type pagename: unicode
         :rtype: bool
         :returns: if pagename was removed
         """
         changed = False
+        quicklinks = self.getQuickLinks()
         interWikiName = getInterwikiName(pagename)
-        if interWikiName and interWikiName in self.quicklinks:
-            self.quicklinks.remove(interWikiName)
+        if interWikiName and interWikiName in quicklinks:
+            quicklinks.remove(interWikiName)
             changed = True
-        if pagename in self.quicklinks:
-            self.quicklinks.remove(pagename)
+        if pagename in quicklinks:
+            quicklinks.remove(pagename)
             changed = True
 
         if changed:
@@ -674,18 +720,18 @@
 
     def generate_recovery_token(self):
         key, token = generate_token()
-        self.recoverpass_key = key
+        self.profile[RECOVERPASS_KEY] = key
         self.save()
         return token
 
     def validate_recovery_token(self, token):
-        return valid_token(self.recoverpass_key, token)
+        return valid_token(self.profile[RECOVERPASS_KEY], token)
 
     def apply_recovery_token(self, token, newpass):
         if not self.validate_recovery_token(token):
             return False
-        self.recoverpass_key = None
-        self.enc_password = crypt_password(newpass)
+        self.profile[RECOVERPASS_KEY] = None
+        self.set_password(newpass)
         self.save()
         return True