changeset 1283:f5e17549eda3 namespaces

merge default branch into namespaces branch
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sat, 03 Mar 2012 20:47:53 +0100
parents c5432dbae81c (current diff) fcf221d8814a (diff)
children ca1c015c2e43
files MoinMoin/_tests/__init__.py MoinMoin/_tests/test_user.py MoinMoin/app.py MoinMoin/apps/frontend/views.py MoinMoin/auth/ldap_login.py MoinMoin/config/default.py MoinMoin/constants/keys.py MoinMoin/script/migration/moin19/import19.py MoinMoin/storage/middleware/indexing.py MoinMoin/storage/middleware/validation.py MoinMoin/themes/__init__.py MoinMoin/user.py
diffstat 23 files changed, 386 insertions(+), 478 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/_tests/__init__.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/_tests/__init__.py	Sat Mar 03 20:47:53 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,8 +41,7 @@
 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]
+    flaskg.user.trusted = True
 
 
 # Creating and destroying test items --------------------------------
--- a/MoinMoin/_tests/test_user.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/_tests/test_user.py	Sat Mar 03 20:47:53 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):
@@ -175,7 +181,7 @@
         assert theuser.valid
 
     def testSubscriptionSubscribedPage(self):
-        """ user: tests isSubscribedTo  """
+        """ user: tests is_subscribed_to  """
         pagename = u'HelpMiscellaneous'
         name = u'__Jürgen Herman__'
         password = name
@@ -183,10 +189,10 @@
         # Login - this should replace the old password in the user file
         theUser = user.User(name=name, password=password)
         theUser.subscribe(pagename)
-        assert theUser.isSubscribedTo([pagename]) # list(!) of pages to check
+        assert theUser.is_subscribed_to([pagename]) # list(!) of pages to check
 
     def testSubscriptionSubPage(self):
-        """ user: tests isSubscribedTo on a subpage """
+        """ user: tests is_subscribed_to on a subpage """
         pagename = u'HelpMiscellaneous'
         testPagename = u'HelpMiscellaneous/FrequentlyAskedQuestions'
         name = u'__Jürgen Herman__'
@@ -195,7 +201,7 @@
         # Login - this should replace the old password in the user file
         theUser = user.User(name=name, password=password)
         theUser.subscribe(pagename)
-        assert not theUser.isSubscribedTo([testPagename]) # list(!) of pages to check
+        assert not theUser.is_subscribed_to([testPagename]) # list(!) of pages to check
 
     def test_upgrade_password_from_ssha_to_ssha256(self):
         """
@@ -203,13 +209,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):
@@ -221,7 +225,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}'
 
@@ -236,7 +239,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}'
 
@@ -250,7 +252,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}'
 
@@ -265,42 +266,29 @@
         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):
-        name = u'Test_User_quicklink'
+        name = u'Test_User_bookmark'
         password = name
         self.createUser(name, password)
         theUser = user.User(name=name, password=password)
 
-        theUser.setBookmark(7)
-        result_added = theUser.getBookmark()
-        expected = 7
-        assert result_added == expected
+        # set / retrieve the bookmark
+        bookmark = 1234567
+        theUser.bookmark = bookmark
+        theUser = user.User(name=name, password=password)
+        result = theUser.bookmark
+        assert result == bookmark
+
         # delete the bookmark
-        result_success = theUser.delBookmark()
-        assert result_success == 0
-        result_deleted = theUser.getBookmark()
-        assert not result_deleted
-
-        # delBookmark should return 1 on failure
-        result_failure = theUser.delBookmark()
-        assert result_failure == 1
+        theUser.bookmark = None
+        theUser = user.User(name=name, password=password)
+        result = theUser.bookmark
+        assert result is None
 
     # Quicklinks ------------------------------------------------------
 
@@ -313,35 +301,24 @@
         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()
+        result_before = theUser.quicklinks
         assert result_before == []
 
-        result = theUser.isQuickLinkedTo([pagename])
+        result = theUser.is_quicklinked_to([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']
+        # add quicklink
+        theUser.quicklink(u'Test_page_added')
+        result_on_addition = theUser.quicklinks
+        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']
+        # remove quicklink
+        theUser.quickunlink(u'Test_page_added')
+        result_on_removal = theUser.quicklinks
+        expected = []
         assert result_on_removal == expected
 
     # Trail -----------------------------------------------------------
@@ -354,13 +331,14 @@
         theUser = user.User(name=name, password=password)
 
         # no item name added to trail
-        result = theUser.getTrail()
+        result = theUser.get_trail()
         expected = []
         assert result == expected
 
         # item name added to trail
-        theUser.addTrail(u'item_added')
-        result = theUser.getTrail()
+        theUser.add_trail(u'item_added')
+        theUser = user.User(name=name, password=password)
+        result = theUser.get_trail()
         expected = [u'MoinTest:item_added']
         assert result == expected
 
--- a/MoinMoin/app.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/app.py	Sat Mar 03 20:47:53 2012 +0100
@@ -214,11 +214,8 @@
         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.trusted'] = userobj.trusted
         session['user.auth_method'] = userobj.auth_method
         session['user.auth_attribs'] = userobj.auth_attribs
     return userobj
--- a/MoinMoin/apps/frontend/_tests/test_frontend.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/apps/frontend/_tests/test_frontend.py	Sat Mar 03 20:47:53 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	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/apps/frontend/views.py	Sat Mar 03 20:47:53 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
@@ -174,7 +173,7 @@
 @frontend.route('/<itemname:item_name>', defaults=dict(rev=CURRENT), methods=['GET'])
 @frontend.route('/+show/+<rev>/<itemname:item_name>', methods=['GET'])
 def show_item(item_name, rev):
-    flaskg.user.addTrail(item_name)
+    flaskg.user.add_trail(item_name)
     item_displayed.send(app._get_current_object(),
                         item_name=item_name)
     try:
@@ -255,7 +254,7 @@
 @frontend.route('/+meta/<itemname:item_name>', defaults=dict(rev=CURRENT))
 @frontend.route('/+meta/+<rev>/<itemname:item_name>')
 def show_item_meta(item_name, rev):
-    flaskg.user.addTrail(item_name)
+    flaskg.user.add_trail(item_name)
     try:
         item = Item.create(item_name, rev_id=rev)
     except AccessDenied:
@@ -750,11 +749,8 @@
 def global_history():
     all_revs = bool(request.values.get('all'))
     idx_name = ALL_REVS if all_revs else LATEST_REVS
-    if flaskg.user.valid:
-        bookmark_time = flaskg.user.getBookmark()
-    else:
-        bookmark_time = None
     query = Term(WIKINAME, app.cfg.interwikiname)
+    bookmark_time = flaskg.user.bookmark
     if bookmark_time is not None:
         query = And([query, DateRange(MTIME, start=datetime.utcfromtimestamp(bookmark_time), end=None)])
     revs = flaskg.storage.search(query, idx_name=idx_name, sortedby=[MTIME], reverse=True, limit=1000)
@@ -839,11 +835,11 @@
     msg = None
     if not u.valid:
         msg = _("You must login to use this action: %(action)s.", action="quicklink/quickunlink"), "error"
-    elif not flaskg.user.isQuickLinkedTo([item_name]):
-        if not u.addQuicklink(item_name):
+    elif not flaskg.user.is_quicklinked_to([item_name]):
+        if not u.quicklink(item_name):
             msg = _('A quicklink to this page could not be added for you.'), "error"
     else:
-        if not u.removeQuicklink(item_name):
+        if not u.quickunlink(item_name):
             msg = _('Your quicklink to this page could not be removed.'), "error"
     if msg:
         flash(*msg)
@@ -860,7 +856,7 @@
         msg = _("You must login to use this action: %(action)s.", action="subscribe/unsubscribe"), "error"
     elif not u.may.read(item_name):
         msg = _("You are not allowed to subscribe to an item you may not read."), "error"
-    elif u.isSubscribedTo([item_name]):
+    elif u.is_subscribed_to([item_name]):
         # Try to unsubscribe
         if not u.unsubscribe(item_name):
             msg = _("Can't remove regular expression subscription!") + u' ' + \
@@ -987,7 +983,7 @@
             else:
                 if app.cfg.user_email_verification:
                     u = user.User(auth_username=user_kwargs['username'])
-                    is_ok, msg = u.mailVerificationLink()
+                    is_ok, msg = u.mail_email_verification()
                     if is_ok:
                         flash(_('Account verification required, please see the email we sent to your address.'), "info")
                     else:
@@ -1010,7 +1006,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:
@@ -1065,7 +1061,7 @@
                 users = user.search_users(email=email)
                 u = users and user.User(users[0][ITEMID])
             if u and u.valid:
-                is_ok, msg = u.mailAccountData()
+                is_ok, msg = u.mail_password_recovery()
                 if not is_ok:
                     flash(msg, "error")
             flash(_("If this account exists, you will be notified."), "info")
@@ -1204,7 +1200,7 @@
 
 
 def _logout():
-    for key in ['user.itemid', 'user.auth_method', 'user.auth_attribs', ]:
+    for key in ['user.itemid', 'user.trusted', 'user.auth_method', 'user.auth_attribs', ]:
         if key in session:
             del session[key]
 
@@ -1343,7 +1339,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,12 +1360,15 @@
                             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()
+                            is_ok, msg = flaskg.user.mail_email_verification()
                             if is_ok:
                                 _logout()
                                 flaskg.user.save()
@@ -1377,8 +1376,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:
@@ -1436,11 +1435,7 @@
                     tm = int(time.time())
         else:
             tm = int(time.time())
-
-        if tm is None:
-            flaskg.user.delBookmark()
-        else:
-            flaskg.user.setBookmark(tm)
+        flaskg.user.bookmark = tm
     else:
         flash(_("You must log in to use bookmarks."), "error")
     return redirect(url_for('.global_history'))
--- a/MoinMoin/auth/__init__.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/auth/__init__.py	Sat Mar 03 20:47:53 2012 +0100
@@ -1,7 +1,7 @@
 # Copyright: 2005-2006 Bastian Blank, Florian Festi
 # Copyright: MoinMoin:AlexanderSchremmer, Nick Phillips
 # Copyright: MoinMoin:FrankieChow, MoinMoin:NirSoffer
-# Copyright: 2005-2009 MoinMoin:ThomasWaldmann
+# Copyright: 2005-2012 MoinMoin:ThomasWaldmann
 # Copyright: 2007      MoinMoin:JohannesBerg
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 
@@ -202,8 +202,10 @@
     name = None
     login_inputs = []
     logout_possible = False
-    def __init__(self):
-        pass
+    def __init__(self, trusted=False, **kw):
+        self.trusted = trusted
+        if kw:
+            raise TypeError("got unexpected arguments %r" % kw)
     def login(self, user_obj, **kw):
         return ContinueLogin(user_obj)
     def request(self, user_obj, **kw):
@@ -218,8 +220,8 @@
 
 class MoinAuth(BaseAuth):
     """ handle login from moin login form """
-    def __init__(self):
-        BaseAuth.__init__(self)
+    def __init__(self, **kw):
+        super(MoinAuth, self).__init__(**kw)
 
     login_inputs = ['username', 'password']
     name = 'moin'
@@ -241,7 +243,7 @@
         if username and not password:
             return ContinueLogin(user_obj, _('Missing password. Please enter user name and password.'))
 
-        u = user.User(name=username, password=password, auth_method=self.name)
+        u = user.User(name=username, password=password, auth_method=self.name, trusted=self.trusted)
         if u.valid:
             logging.debug("{0}: successfully authenticated user {1!r} (valid)".format(self.name, u.name))
             return ContinueLogin(u)
@@ -276,7 +278,9 @@
                  titlecase=False,  # joe doe -> Joe Doe
                  remove_blanks=False,  # Joe Doe -> JoeDoe
                  coding='utf-8',  # for decoding REMOTE_USER correctly
+                 **kw
                 ):
+        super(GivenAuth, self).__init__(**kw)
         self.env_var = env_var
         self.user_name = user_name
         self.autocreate = autocreate
@@ -285,7 +289,6 @@
         self.titlecase = titlecase
         self.remove_blanks = remove_blanks
         self.coding = coding
-        BaseAuth.__init__(self)
 
     def decode_username(self, name):
         """ decode the name we got from the environment var to unicode """
@@ -341,7 +344,7 @@
             auth_username = self.transform_username(auth_username)
             logging.debug("auth_username (after decode/transform) = {0!r}".format(auth_username))
             u = user.User(auth_username=auth_username,
-                          auth_method=self.name, auth_attribs=('name', 'password'))
+                          auth_method=self.name, auth_attribs=('name', 'password'), trusted=self.trusted)
 
         logging.debug("u: {0!r}".format(u))
         if u and self.autocreate:
@@ -427,15 +430,17 @@
 def setup_from_session():
     userobj = None
     if 'user.itemid' in session:
-        auth_userid = session['user.itemid']
+        itemid = session['user.itemid']
+        trusted = session['user.trusted']
         auth_method = session['user.auth_method']
-        auth_attrs = session['user.auth_attribs']
-        logging.debug("got from session: {0!r} {1!r}".format(auth_userid, auth_method))
+        auth_attribs = session['user.auth_attribs']
+        logging.debug("got from session: {0!r} {1!r} {2!r} {3!r}".format(itemid, trusted, auth_method, auth_attribs))
         logging.debug("current auth methods: {0!r}".format(app.cfg.auth_methods))
         if auth_method and auth_method in app.cfg.auth_methods:
-            userobj = user.User(auth_userid,
+            userobj = user.User(itemid,
                                 auth_method=auth_method,
-                                auth_attribs=auth_attrs)
+                                auth_attribs=auth_attribs,
+                                trusted=trusted)
     logging.debug("session started for user {0!r}".format(userobj))
     return userobj
 
--- a/MoinMoin/auth/http.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/auth/http.py	Sat Mar 03 20:47:53 2012 +0100
@@ -14,8 +14,6 @@
 
     from MoinMoin.auth.http import HTTPAuthMoin
     auth = [HTTPAuthMoin()]
-    # check if you want 'http' auth name in there:
-    auth_methods_trusted = ['http', ]
 """
 
 
@@ -33,11 +31,11 @@
     """ authenticate via http (basic) auth """
     name = 'http'
 
-    def __init__(self, autocreate=False, realm='MoinMoin', coding='iso-8859-1'):
+    def __init__(self, autocreate=False, realm='MoinMoin', coding='iso-8859-1', **kw):
+        super(HTTPAuthMoin, self).__init__(**kw)
         self.autocreate = autocreate
         self.realm = realm
         self.coding = coding
-        BaseAuth.__init__(self)
 
     def request(self, user_obj, **kw):
         u = None
@@ -53,7 +51,7 @@
             logging.debug("http basic auth, received username: {0!r} password: {1!r}".format(auth.username, auth.password))
             u = user.User(name=auth.username.decode(self.coding),
                           password=auth.password.decode(self.coding),
-                          auth_method=self.name, auth_attribs=[])
+                          auth_method=self.name, auth_attribs=[], trusted=self.trusted)
             logging.debug("user: {0!r}".format(u))
 
         if not u or not u.valid:
--- a/MoinMoin/auth/ldap_login.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/auth/ldap_login.py	Sat Mar 03 20:47:53 2012 +0100
@@ -86,7 +86,9 @@
         autocreate=False, # set to True if you want to autocreate user profiles
         name='ldap', # use e.g. 'ldap_pdc' and 'ldap_bdc' (or 'ldap1' and 'ldap2') if you auth against 2 ldap servers
         report_invalid_credentials=True, # whether to emit "invalid username or password" msg at login time or not
+        **kw
         ):
+        super(LDAPAuth, self).__init__(**kw)
         self.server_uri = server_uri
         self.bind_dn = bind_dn
         self.bind_pw = bind_pw
@@ -232,10 +234,12 @@
                     username = self.name_callback(ldap_dict)
 
                 if email:
-                    u = user.User(auth_username=username, auth_method=self.name, auth_attribs=('name', 'password', 'email', 'mailto_author', ))
+                    u = user.User(auth_username=username, auth_method=self.name, auth_attribs=('name', 'password', 'email', 'mailto_author', ),
+                                  trusted=self.trusted)
                     u.email = email
                 else:
-                    u = user.User(auth_username=username, auth_method=self.name, auth_attribs=('name', 'password', 'mailto_author', ))
+                    u = user.User(auth_username=username, auth_method=self.name, auth_attribs=('name', 'password', 'mailto_author', ),
+                                  trusted=self.trusted)
                 u.name = username
                 u.display_name = display_name
                 logging.debug("creating user object with name {0!r} email {1!r} display name {2!r}".format(username, email, display_name))
--- a/MoinMoin/auth/log.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/auth/log.py	Sat Mar 03 20:47:53 2012 +0100
@@ -18,6 +18,9 @@
     """ just log the call, do nothing else """
     name = "log"
 
+    def __init__(self, **kw):
+        super(AuthLog, self).__init__(**kw)
+
     def log(self, action, user_obj, kw):
         logging.info('{0}: user_obj={1!r} kw={2!r}'.format(action, user_obj, kw))
 
--- a/MoinMoin/auth/openidrp.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/auth/openidrp.py	Sat Mar 03 20:47:53 2012 +0100
@@ -26,7 +26,8 @@
 
 
 class OpenIDAuth(BaseAuth):
-    def __init__(self, trusted_providers=[]):
+    def __init__(self, trusted_providers=[], **kw):
+        super(OpenIDAuth, self).__init__(**kw)
         # the name
         self.name = 'openid'
         # we only need openid
@@ -37,7 +38,6 @@
         self.store = MemoryStore()
 
         self._trusted_providers = list(trusted_providers)
-        BaseAuth.__init__(self)
 
     def _handleContinuationVerify(self):
         """
@@ -83,7 +83,7 @@
                 # we get the user with this openid associated to him
                 identity = oid_info.identity_url
                 users = user.search_users(openid=identity)
-                user_obj = users and user.User(users[0][ITEMID])
+                user_obj = users and user.User(users[0][ITEMID], trusted=self.trusted)
 
                 # if the user actually exists
                 if user_obj:
--- a/MoinMoin/auth/smb_mount.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/auth/smb_mount.py	Sat Mar 03 20:47:53 2012 +0100
@@ -34,8 +34,9 @@
         iocharset='utf-8', # mount.cifs -o iocharset=... (try 'iso8859-1' if default does not work)
         coding='utf-8', # encoding used for username/password/cmdline (try 'iso8859-1' if default does not work)
         log='/dev/null', # logfile for mount.cifs output
+        **kw
         ):
-        BaseAuth.__init__(self)
+        super(SMBMount, self).__init__(**kw)
         self.server = server
         self.share = share
         self.mountpoint_fn = mountpoint_fn
--- a/MoinMoin/config/default.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/config/default.py	Sat Mar 03 20:47:53 2012 +0100
@@ -305,8 +305,6 @@
   'auth': ('Authentication / Authorization / Security settings', None, (
     ('auth', DefaultExpression('[MoinAuth()]'),
      "list of auth objects, to be called in this order (see HelpOnAuthentication)"),
-    ('auth_methods_trusted', ['http', 'given', ], # Note: 'http' auth method is currently just a redirect to 'given'
-     'authentication methods for which users should be included in the special "Trusted" ACL group.'),
     ('secrets', None, """Either a long shared secret string used for multiple purposes or a dict {"purpose": "longsecretstring", ...} for setting up different shared secrets for different purposes."""),
     ('SecurityPolicy',
      None,
@@ -455,6 +453,7 @@
         show_comments=False,
         want_trivial=False,
         disabled=False,
+        bookmarks={},
         quicklinks=[],
         subscribed_items=[],
         email_subscribed_events=[
--- a/MoinMoin/constants/keys.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/constants/keys.py	Sat Mar 03 20:47:53 2012 +0100
@@ -63,6 +63,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"
 
 # in which backend is some revision stored?
 BACKENDNAME = "backendname"
--- a/MoinMoin/error.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/error.py	Sat Mar 03 20:47:53 2012 +0100
@@ -1,9 +1,8 @@
 # Copyright: 2004-2005 Nir Soffer <nirs@freeshell.org>
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 
-""" MoinMoin errors
-
-    Supply Error class and sub classes used to raise various errors
+"""
+MoinMoin errors / exception classes
 """
 
 
@@ -107,12 +106,3 @@
 class InternalError(FatalError):
     """ Raise when internal fatal error is found """
 
-class NoConfigMatchedError(Exception):
-    """ we didn't find a configuration for this URL """
-    pass
-
-class ConvertError(FatalError):
-    """ Raise when html to storage format (e.g. 'wiki') conversion fails """
-    name = "MoinMoin Convert Error"
-
-
--- a/MoinMoin/script/migration/moin19/import19.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/script/migration/moin19/import19.py	Sat Mar 03 20:47:53 2012 +0100
@@ -570,6 +570,8 @@
             if key in metadata and metadata[key] in [u'', tuple(), {}, [], ]:
                 del metadata[key]
 
+        # TODO quicklinks and subscribed_items - check for non-interwiki elements and convert them to interwiki
+
         return metadata
 
 
--- a/MoinMoin/security/__init__.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/security/__init__.py	Sat Mar 03 20:47:53 2012 +0100
@@ -262,12 +262,12 @@
         return None
 
     def _special_Trusted(self, name, dowhat, rightsdict):
-        """ check if user <name> is known AND has logged in using a trusted
-            authentication method.
+        """ check if user <name> is the current user AND is has logged in using
+            an authentication method that set the trusted attribute.
             Does not work for subsription emails that should be sent to <user>,
-            as he is not logged in in that case.
+            as the user is not logged in in that case.
         """
-        if flaskg.user.name == name and flaskg.user.auth_trusted:
+        if flaskg.user.name == name and flaskg.user.trusted:
             return rightsdict.get(dowhat)
         return None
 
--- a/MoinMoin/security/_tests/test_textcha.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/security/_tests/test_textcha.py	Sat Mar 03 20:47:53 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/error.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/storage/error.py	Sat Mar 03 20:47:53 2012 +0100
@@ -4,11 +4,10 @@
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 
 """
-    MoinMoin storage errors
+MoinMoin storage errors
 """
 
 
-from MoinMoin.i18n import _, L_, N_
 from MoinMoin.error import CompositeError
 
 
@@ -16,66 +15,29 @@
     """
     General class for exceptions on the storage layer.
     """
-    pass
-
-class AccessError(StorageError):
-    """
-    Raised if the action could not be commited because of access problems.
-    """
-    pass
-
-class CouldNotDestroyError(AccessError):
-    """
-    Raised if the item/revision in question could not be destroyed due to
-    an internal backend problem. NOT raised if the user does not have the
-    'destroy' privilege. This exception describes a technical deletion
-    problem, not missing ACLs.
-    """
-    pass
-
-class LockingError(AccessError):
-    """
-    Raised if the action could not be commited because the Item is locked
-    or the if the item could not be locked.
-    """
-    pass
 
 class BackendError(StorageError):
     """
     Raised if the backend couldn't commit the action.
     """
-    pass
 
 class NoSuchItemError(BackendError):
     """
     Raised if the requested item does not exist.
     """
-    pass
 
 class ItemAlreadyExistsError(BackendError):
     """
     Raised if the Item you are trying to create already exists.
     """
-    pass
 
 class NoSuchRevisionError(BackendError):
     """
     Raised if the requested revision of an item does not exist.
     """
-    pass
 
 class RevisionAlreadyExistsError(BackendError):
     """
     Raised if the Revision you are trying to create already exists.
     """
-    pass
 
-class RevisionNumberMismatchError(BackendError):
-    """
-    Raised if a revision number that is not greater than the most recent revision
-    number was passed or if the backend does not yet support non-contiguous or
-    non-zero-based revision numbers and the operation violated these
-    requirements.
-    """
-    pass
-
--- a/MoinMoin/storage/middleware/indexing.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/storage/middleware/indexing.py	Sat Mar 03 20:47:53 2012 +0100
@@ -89,6 +89,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, ]
@@ -807,7 +808,7 @@
         item = cls(indexer, **query)
         if not item:
             return item
-        raise ItemAlreadyExists(repr(query))
+        raise ItemAlreadyExistsError(repr(query))
 
     @classmethod
     def existing(cls, indexer, **query):
@@ -817,7 +818,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	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/storage/middleware/validation.py	Sat Mar 03 20:47:53 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
@@ -353,13 +353,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/templates/itemviews.html	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/templates/itemviews.html	Sat Mar 03 20:47:53 2012 +0100
@@ -39,7 +39,7 @@
             {%- if endpoint == 'frontend.quicklink_item' and user.valid %}
                 <li>
                     <a href="{{ url_for(endpoint, item_name=item_name) }}" title="{{ title }}" rel="nofollow">
-                        {%- if user.isQuickLinkedTo([item_name]) %}
+                        {%- if user.is_quicklinked_to([item_name]) %}
                             {{ _('Remove Link') }}
                         {%- else %}
                             {{ _('Add Link') }}
@@ -51,7 +51,7 @@
             {%- if endpoint == 'frontend.subscribe_item' and user.valid %}
                 <li>
                     <a href="{{ url_for(endpoint, item_name=item_name) }}" title="{{ title }}" rel="nofollow">
-                        {%- if user.isSubscribedTo([item_name]) %}
+                        {%- if user.is_subscribed_to([item_name]) %}
                             {{ _('Unsubscribe') }}
                         {%- else %}
                             {{ _('Subscribe') }}
--- a/MoinMoin/themes/__init__.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/themes/__init__.py	Sat Mar 03 20:47:53 2012 +0100
@@ -104,7 +104,7 @@
         """
         user = self.user
         breadcrumbs = []
-        trail = user.getTrail()
+        trail = user.get_trail()
         for interwiki_item_name in trail:
             wiki_name, item_name = split_interwiki(interwiki_item_name)
             err = not is_known_wiki(wiki_name)
@@ -217,8 +217,7 @@
                  for cls, endpoint, args, link_text, title in self.cfg.navi_bar]
 
         # Add user links to wiki links.
-        userlinks = self.user.getQuickLinks()
-        for text in userlinks:
+        for text in self.user.quicklinks:
             url, link_text, title = self.split_navilink(text)
             items.append(('userlink', url, link_text, title))
 
--- a/MoinMoin/user.py	Mon Feb 27 00:35:43 2012 +0100
+++ b/MoinMoin/user.py	Sat Mar 03 20:47:53 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
@@ -19,7 +19,7 @@
 
 from __future__ import absolute_import, division
 
-import time
+import re
 import copy
 import hashlib
 import werkzeug
@@ -33,10 +33,11 @@
 
 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 import wikiutil
+from MoinMoin.config import CONTENTTYPE_USER
+from MoinMoin.constants.keys import *
 from MoinMoin.i18n import _, L_, N_
+from MoinMoin.mail import sendmail
 from MoinMoin.util.interwiki import getInterwikiHome, getInterwikiName, is_local_wiki
 from MoinMoin.util.crypto import crypt_password, upgrade_password, valid_password, \
                                  generate_token, valid_token, make_uuid
@@ -47,51 +48,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,20 +101,24 @@
     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 """
-
     # Since item name is a list, it's possible a list have been passed as parameter.
     # No problem, since user always have just one name (TODO: validate single name for user)
     if q.get('name_exact') and isinstance(q.get('name_exact'), list):
         q['name_exact'] = q['name_exact'][0]
-
-    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)
@@ -179,17 +183,84 @@
     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:
+            v = self._defaults[name]
+            if isinstance(v, (list, dict, set)): # mutable
+                self._meta[name] = v
+            return v
+
+    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, force=False):
+        """
+        save a user profile (if it was changed since loading it)
+
+        Note: if mutable profile values were modified, you need to use
+              force=True because these changes are not detected!
+        """
+        if self._changed or force:
+            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 """
 
-    def __init__(self, uid=None, name="", password=None, auth_username="", **kw):
+    def __init__(self, uid=None, name="", password=None, auth_username="", trusted=False, **kw):
         """ Initialize User object
 
-        :param uid: (optional) user ID
+        :param uid: (optional) user ID (user itemid)
         :param name: (optional) user name
         :param password: (optional) user password (unicode)
         :param auth_username: (optional) already authenticated user name
                               (e.g. when using http basic auth) (unicode)
+        :param trusted: (optional) whether user instance is created by a
+                        trusted auth method / session
         :keyword auth_method: method that was used for authentication,
                               default: 'internal'
         :keyword auth_attribs: tuple of user object attribute names that are
@@ -197,54 +268,32 @@
                                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.trusted = trusted
         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:
@@ -254,17 +303,29 @@
             self.may = Default(self)
 
     def __repr__(self):
-        return "<{0}.{1} at {2:#x} name:{3!r} itemid:{4!r} valid:{5!r}>".format(
+        return "<{0}.{1} at {2:#x} name:{3!r} itemid:{4!r} valid:{5!r} trusted:{6!r}>".format(
             self.__class__.__module__, self.__class__.__name__, id(self),
-            self.name, self.itemid, self.valid)
+            self.name, self.itemid, self.valid, self.trusted)
+
+    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, SUBSCRIBED_ITEMS, QUICKLINKS,
+                   ]:
+            return self.profile[name]
+        else:
+            return object.__getattr__(self, name)
 
     @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
@@ -278,18 +339,19 @@
 
         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'),
                                            True)
 
         param['size'] = str(size)
-        #TODO: use same protocol of Moin site (might be https instead of http)]
+        # TODO: use same protocol of Moin site (might be https instead of http)]
         gravatar_url = "http://www.gravatar.com/avatar.php?"
         gravatar_url += werkzeug.url_encode(param)
 
@@ -304,60 +366,42 @@
             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._validate_password(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):
+    def _validate_password(self, data, password):
         """
         Check user password.
 
@@ -368,7 +412,7 @@
         :rtype: 2 tuple (bool, bool)
         :returns: password is valid, enc_password changed
         """
-        pw_hash = data.get('enc_password')
+        pw_hash = data[ENC_PASSWORD]
 
         # If we have no password set, we don't accept login with username.
         # Require non-empty password.
@@ -383,39 +427,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):
+    def save(self, force=False):
         """
         Save user account data to user account file on disk.
         """
-        # XXX maybe UserProfile/<name> later
-        backend_name = self.name[0] if isinstance(self.name, list) else self.name
-        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(force=force)
 
         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
 
@@ -424,63 +455,44 @@
         return text # FIXME, was: self._request.getText(text, lang=self.language)
 
 
-    # -----------------------------------------------------------------
-    # Bookmark
+    # Bookmarks --------------------------------------------------------------
 
-    def setBookmark(self, tm):
+    def _set_bookmark(self, tm):
         """ Set bookmark timestamp.
 
-        :param tm: timestamp
+        :param tm: timestamp (int or None)
         """
         if self.valid:
-            self.bookmarks[self._cfg.interwikiname] = int(tm)
-            self.save()
+            if not (tm is None or isinstance(tm, int)):
+                raise ValueError('tm should be int or None')
+            if tm is None:
+                self.profile[BOOKMARKS].pop(self._cfg.interwikiname)
+            else:
+                self.profile[BOOKMARKS][self._cfg.interwikiname] = tm
+            self.save(force=True)
 
-    def getBookmark(self):
+    def _get_bookmark(self):
         """ Get bookmark timestamp.
 
-        :rtype: int
+        :rtype: int / None
         :returns: bookmark timestamp or None
         """
         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
 
-    def delBookmark(self):
-        """ Removes bookmark timestamp.
+    bookmark = property(_get_bookmark, _set_bookmark)
 
-        :rtype: int
-        :returns: 0 on success, 1 on failure
-        """
-        if self.valid:
-            try:
-                del self.bookmarks[self._cfg.interwikiname]
-            except KeyError:
-                return 1
-            self.save()
-            return 0
-        return 1
+    # Subscribed Items -------------------------------------------------------
 
-    # -----------------------------------------------------------------
-    # Subscribe
-
-    def getSubscriptionList(self):
-        """ Get list of pages this user has subscribed to
-
-        :rtype: list
-        :returns: pages this user has subscribed to
-        """
-        return self.subscribed_items
-
-    def isSubscribedTo(self, pagelist):
+    def is_subscribed_to(self, pagelist):
         """ Check if user subscription matches any page in pagelist.
 
-        The subscription list may contain page names or interwiki page
-        names. e.g 'Page Name' or 'WikiName:Page_Name'
+        The subscription contains interwiki page names. e.g 'WikiName:Page_Name'
 
         TODO: check if it's fast enough when getting called for many
               users from page.getSubscribersList()
@@ -492,14 +504,12 @@
         if not self.valid:
             return False
 
-        import re
-        # Create a new list with both names and interwiki names.
-        pages = pagelist[:] # TODO: get rid of non-interwiki subscriptions?
-        pages += [getInterwikiName(pagename) for pagename in pagelist]
+        # Create a new list with interwiki names.
+        pages = [getInterwikiName(pagename) for pagename in pagelist]
         # Create text for regular expression search
         text = '\n'.join(pages)
 
-        for pattern in self.getSubscriptionList():
+        for pattern in self.subscribed_items:
             # Try simple match first
             if pattern in pages:
                 return True
@@ -516,8 +526,7 @@
     def subscribe(self, pagename):
         """ Subscribe to a wiki page.
 
-        To enable shared farm users, if the wiki has an interwiki name,
-        page names are saved as interwiki names.
+        Page names are saved as interwiki names.
 
         :param pagename: name of the page to subscribe
         :type pagename: unicode
@@ -525,9 +534,10 @@
         :returns: if page was subscribed
         """
         pagename = getInterwikiName(pagename)
-        if pagename not in self.subscribed_items:
-            self.subscribed_items.append(pagename)
-            self.save()
+        subscribed_items = self.subscribed_items
+        if pagename not in subscribed_items:
+            subscribed_items.append(pagename)
+            self.save(force=True)
             # XXX SubscribedToPageEvent
             return True
         return False
@@ -535,48 +545,30 @@
     def unsubscribe(self, pagename):
         """ Unsubscribe a wiki page.
 
-        Try to unsubscribe by removing non-interwiki name (leftover
-        from old use files) and interwiki name from the subscription
+        Try to unsubscribe by removing interwiki name from the subscription
         list.
 
         Its possible that the user will be subscribed to a page by more
-        then one pattern. It can be both pagename and interwiki name,
-        or few patterns that all of them match the page. Therefore, we
-        must check if the user is still subscribed to the page after we
-        try to remove names from the list.
+        than one pattern. It can be both interwiki name and a regex pattern that
+        both match the page. Therefore, we must check if the user is
+        still subscribed to the page after we try to remove names from the list.
 
         :param pagename: name of the page to subscribe
         :type pagename: unicode
         :rtype: bool
-        :returns: if unsubscrieb was successful. If the user has a
-            regular expression that match, it will always fail.
+        :returns: if unsubscribe was successful. If the user has a
+            regular expression that matches, unsubscribe will always fail.
         """
-        changed = False
-        if pagename in self.subscribed_items:
-            self.subscribed_items.remove(pagename)
-            changed = True
-
         interWikiName = getInterwikiName(pagename)
-        if interWikiName and interWikiName in self.subscribed_items:
-            self.subscribed_items.remove(interWikiName)
-            changed = True
+        subscribed_items = self.profile[SUBSCRIBED_ITEMS]
+        if interWikiName and interWikiName in subscribed_items:
+            subscribed_items.remove(interWikiName)
+            self.save(force=True)
+        return not self.is_subscribed_to([pagename])
 
-        if changed:
-            self.save()
-        return not self.isSubscribedTo([pagename])
-
-    # -----------------------------------------------------------------
-    # Quicklinks
+    # Quicklinks -------------------------------------------------------------
 
-    def getQuickLinks(self):
-        """ Get list of pages this user wants in the navibar
-
-        :rtype: list
-        :returns: quicklinks from user account
-        """
-        return self.quicklinks
-
-    def isQuickLinkedTo(self, pagelist):
+    def is_quicklinked_to(self, pagelist):
         """ Check if user quicklink matches any page in pagelist.
 
         :param pagelist: list of pages to check for quicklinks
@@ -586,71 +578,53 @@
         if not self.valid:
             return False
 
+        quicklinks = self.quicklinks
         for pagename in pagelist:
-            if pagename in self.quicklinks:
-                return True
             interWikiName = getInterwikiName(pagename)
-            if interWikiName and interWikiName in self.quicklinks:
+            if interWikiName and interWikiName in quicklinks:
                 return True
 
         return False
 
-    def addQuicklink(self, pagename):
+    def quicklink(self, pagename):
         """ Adds a page to the user quicklinks
 
-        If the wiki has an interwiki name, all links are saved as
-        interwiki names. If not, as simple page name.
+        Add links as interwiki names.
 
         :param pagename: page name
         :type pagename: unicode
         :rtype: bool
         :returns: if pagename was added
         """
-        changed = False
+        quicklinks = self.quicklinks
         interWikiName = getInterwikiName(pagename)
-        if interWikiName:
-            if pagename in self.quicklinks:
-                self.quicklinks.remove(pagename)
-                changed = True
-            if interWikiName not in self.quicklinks:
-                self.quicklinks.append(interWikiName)
-                changed = True
-        else:
-            if pagename not in self.quicklinks:
-                self.quicklinks.append(pagename)
-                changed = True
+        if interWikiName and interWikiName not in quicklinks:
+            quicklinks.append(interWikiName)
+            self.save(force=True)
+            return True
+        return False
 
-        if changed:
-            self.save()
-        return changed
-
-    def removeQuicklink(self, pagename):
+    def quickunlink(self, pagename):
         """ Remove a page from user quicklinks
 
-        Remove both interwiki and simple name from quicklinks.
+        Remove interwiki name from quicklinks.
 
         :param pagename: page name
         :type pagename: unicode
         :rtype: bool
         :returns: if pagename was removed
         """
-        changed = False
+        quicklinks = self.quicklinks
         interWikiName = getInterwikiName(pagename)
-        if interWikiName and interWikiName in self.quicklinks:
-            self.quicklinks.remove(interWikiName)
-            changed = True
-        if pagename in self.quicklinks:
-            self.quicklinks.remove(pagename)
-            changed = True
+        if interWikiName and interWikiName in quicklinks:
+            quicklinks.remove(interWikiName)
+            self.save(force=True)
+            return True
+        return False
 
-        if changed:
-            self.save()
-        return changed
+    # Trail ------------------------------------------------------------------
 
-    # -----------------------------------------------------------------
-    # Trail
-
-    def addTrail(self, item_name):
+    def add_trail(self, item_name):
         """ Add item name to trail.
 
         :param item_name: the item name (unicode) to add to the trail
@@ -664,7 +638,7 @@
         if trail != trail_in_session:
             session['trail'] = trail
 
-    def getTrail(self):
+    def get_trail(self):
         """ Return list of recently visited item names.
 
         :rtype: list
@@ -672,35 +646,35 @@
         """
         return session.get('trail', [])
 
-    # -----------------------------------------------------------------
-    # Other
+    # Other ------------------------------------------------------------------
 
-    def isCurrentUser(self):
+    def is_current_user(self):
         """ Check if this user object is the user doing the current request """
         return flaskg.user.name == self.name
 
+    # Account verification / Password recovery -------------------------------
+
     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
 
-    def mailAccountData(self, cleartext_passwd=None):
+    def mail_password_recovery(self, cleartext_passwd=None):
         """ Mail a user who forgot his password a message enabling
             him to login again.
         """
-        from MoinMoin.mail import sendmail
         token = self.generate_recovery_token()
 
         text = _("""\
@@ -720,9 +694,8 @@
         mailok, msg = sendmail.sendmail(subject, text, to=[self.email], mail_from=self._cfg.mail_from)
         return mailok, msg
 
-    def mailVerificationLink(self):
+    def mail_email_verification(self):
         """ Mail a user a link to verify his email address. """
-        from MoinMoin.mail import sendmail
         token = self.generate_recovery_token()
 
         text = _("""\