diff --git a/wiki-lenny/share/Page.py b/wiki-lenny/share/Page.py
index ed47432c..a37752e6 100644
--- a/wiki-lenny/share/Page.py
+++ b/wiki-lenny/share/Page.py
@@ -1108,7 +1108,7 @@ class Page(object):
request.write(self.formatter.startDocument(self.page_name))
### HACK SAUVAGE
- request.theme.add_msg(u"Le wiki vient d'être migré, merci de nous signaler tout bug consécutif à cette migration sur WikiBugs")
+ request.theme.add_msg(u"Le wiki vient d'être migré, merci de nous signaler tout bug consécutif à cette migration sur WikiBugs")
### FIN HACK
# send the page header
if self.default_formatter:
diff --git a/wiki-lenny/share/user.orig.py b/wiki-lenny/share/user.orig.py
new file mode 100644
index 00000000..8d9918b2
--- /dev/null
+++ b/wiki-lenny/share/user.orig.py
@@ -0,0 +1,1094 @@
+# -*- coding: iso-8859-1 -*-
+"""
+ MoinMoin - User Accounts
+
+ This module contains functions to access user accounts (list all users, get
+ some specific user). User instances are used to access the user profile of
+ some specific user (name, password, email, bookmark, trail, settings, ...).
+
+ Some related code is in the userform and userprefs modules.
+
+ TODO:
+ * code is a mixture of highlevel user stuff and lowlevel storage functions,
+ this has to get separated into:
+ * user object highlevel stuff
+ * storage code
+
+ @copyright: 2000-2004 Juergen Hermann ,
+ 2003-2007 MoinMoin:ThomasWaldmann
+ @license: GNU GPL, see COPYING for details.
+"""
+
+# add names here to hide them in the cgitb traceback
+unsafe_names = ("id", "key", "val", "user_data", "enc_password", "recoverpass_key")
+
+import os, time, sha, codecs, hmac
+
+from MoinMoin import config, caching, wikiutil, i18n, events
+from MoinMoin.util import timefuncs, filesys, random_string
+from MoinMoin.wikiutil import url_quote_plus
+
+
+def getUserList(request):
+ """ Get a list of all (numerical) user IDs.
+
+ @param request: current request
+ @rtype: list
+ @return: all user IDs
+ """
+ import re
+ user_re = re.compile(r'^\d+\.\d+(\.\d+)?$')
+ files = filesys.dclistdir(request.cfg.user_dir)
+ userlist = [f for f in files if user_re.match(f)]
+ return userlist
+
+def get_by_filter(request, filter_func):
+ """ Searches for an user with a given filter function """
+ for uid in getUserList(request):
+ theuser = User(request, uid)
+ if filter_func(theuser):
+ return theuser
+
+def get_by_email_address(request, email_address):
+ """ Searches for an user with a particular e-mail address and returns it. """
+ filter_func = lambda user: user.valid and user.email.lower() == email_address.lower()
+ return get_by_filter(request, filter_func)
+
+def get_by_jabber_id(request, jabber_id):
+ """ Searches for an user with a perticular jabber id and returns it. """
+ filter_func = lambda user: user.valid and user.jid.lower() == jabber_id.lower()
+ return get_by_filter(request, filter_func)
+
+def _getUserIdByKey(request, key, search):
+ """ Get the user ID for a specified key/value pair.
+
+ This method must only be called for keys that are
+ guaranteed to be unique.
+
+ @param key: the key to look in
+ @param search: the value to look for
+ @return the corresponding user ID or None
+ """
+ if not search or not key:
+ return None
+ cfg = request.cfg
+ cachekey = '%s2id' % key
+ try:
+ _key2id = getattr(cfg.cache, cachekey)
+ except AttributeError:
+ arena = 'user'
+ cache = caching.CacheEntry(request, arena, cachekey, scope='wiki', use_pickle=True)
+ try:
+ _key2id = cache.content()
+ except caching.CacheError:
+ _key2id = {}
+ setattr(cfg.cache, cachekey, _key2id)
+ uid = _key2id.get(search, None)
+ if uid is None:
+ for userid in getUserList(request):
+ u = User(request, id=userid)
+ if hasattr(u, key):
+ value = getattr(u, key)
+ if isinstance(value, list):
+ for val in value:
+ _key2id[val] = userid
+ else:
+ _key2id[value] = userid
+ arena = 'user'
+ cache = caching.CacheEntry(request, arena, cachekey, scope='wiki', use_pickle=True)
+ try:
+ cache.update(_key2id)
+ except caching.CacheError:
+ pass
+ uid = _key2id.get(search, None)
+ return uid
+
+
+def getUserId(request, searchName):
+ """ Get the user ID for a specific user NAME.
+
+ @param searchName: the user name to look up
+ @rtype: string
+ @return: the corresponding user ID or None
+ """
+ return _getUserIdByKey(request, 'name', searchName)
+
+
+def getUserIdByOpenId(request, openid):
+ """ Get the user ID for a specific OpenID.
+
+ @param openid: the openid to look up
+ @rtype: string
+ @return: the corresponding user ID or None
+ """
+ return _getUserIdByKey(request, 'openids', openid)
+
+
+def getUserIdentification(request, username=None):
+ """ Return user name or IP or '' indicator.
+
+ @param request: the request object
+ @param username: (optional) user name
+ @rtype: string
+ @return: user name or IP or unknown indicator
+ """
+ _ = request.getText
+
+ if username is None:
+ username = request.user.name
+
+ return username or (request.cfg.show_hosts and request.remote_addr) or _("")
+
+
+def encodePassword(pwd, charset='utf-8'):
+ """ Encode a cleartext password
+
+ Compatible to Apache htpasswd SHA encoding.
+
+ When using different encoding than 'utf-8', the encoding might fail
+ and raise UnicodeError.
+
+ @param pwd: the cleartext password, (unicode)
+ @param charset: charset used to encode password, used only for
+ compatibility with old passwords generated on moin-1.2.
+ @rtype: string
+ @return: the password in apache htpasswd compatible SHA-encoding,
+ or None
+ """
+ import base64
+
+ # Might raise UnicodeError, but we can't do anything about it here,
+ # so let the caller handle it.
+ pwd = pwd.encode(charset)
+
+ pwd = sha.new(pwd).digest()
+ pwd = '{SHA}' + base64.encodestring(pwd).rstrip()
+ return pwd
+
+
+def normalizeName(name):
+ """ Make normalized user name
+
+ Prevent impersonating another user with names containing leading,
+ trailing or multiple whitespace, or using invisible unicode
+ characters.
+
+ Prevent creating user page as sub page, because '/' is not allowed
+ in user names.
+
+ Prevent using ':' and ',' which are reserved by acl.
+
+ @param name: user name, unicode
+ @rtype: unicode
+ @return: user name that can be used in acl lines
+ """
+ username_allowedchars = "'@.-_" # ' for names like O'Brian or email addresses.
+ # "," and ":" must not be allowed (ACL delimiters).
+ # We also allow _ in usernames for nicer URLs.
+ # Strip non alpha numeric characters (except username_allowedchars), keep white space
+ name = ''.join([c for c in name if c.isalnum() or c.isspace() or c in username_allowedchars])
+
+ # Normalize white space. Each name can contain multiple
+ # words separated with only one space.
+ name = ' '.join(name.split())
+
+ return name
+
+
+def isValidName(request, name):
+ """ Validate user name
+
+ @param name: user name, unicode
+ """
+ normalized = normalizeName(name)
+ return (name == normalized) and not wikiutil.isGroupPage(request, name)
+
+
+def encodeList(items):
+ """ Encode list of items in user data file
+
+ Items are separated by '\t' characters.
+
+ @param items: list unicode strings
+ @rtype: unicode
+ @return: list encoded as unicode
+ """
+ line = []
+ for item in items:
+ item = item.strip()
+ if not item:
+ continue
+ line.append(item)
+
+ line = '\t'.join(line)
+ return line
+
+def decodeList(line):
+ """ Decode list of items from user data file
+
+ @param line: line containing list of items, encoded with encodeList
+ @rtype: list of unicode strings
+ @return: list of items in encoded in line
+ """
+ items = []
+ for item in line.split('\t'):
+ item = item.strip()
+ if not item:
+ continue
+ items.append(item)
+ return items
+
+def encodeDict(items):
+ """ Encode dict of items in user data file
+
+ Items are separated by '\t' characters.
+ Each item is key:value.
+
+ @param items: dict of unicode:unicode
+ @rtype: unicode
+ @return: dict encoded as unicode
+ """
+ line = []
+ for key, value in items.items():
+ item = u'%s:%s' % (key, value)
+ line.append(item)
+ line = '\t'.join(line)
+ return line
+
+def decodeDict(line):
+ """ Decode dict of key:value pairs from user data file
+
+ @param line: line containing a dict, encoded with encodeDict
+ @rtype: dict
+ @return: dict unicode:unicode items
+ """
+ items = {}
+ for item in line.split('\t'):
+ item = item.strip()
+ if not item:
+ continue
+ key, value = item.split(':', 1)
+ items[key] = value
+ return items
+
+
+class User:
+ """ A MoinMoin User """
+
+ def __init__(self, request, id=None, name="", password=None, auth_username="", **kw):
+ """ Initialize User object
+
+ TODO: when this gets refactored, use "uid" not builtin "id"
+
+ @param request: the request object
+ @param id: (optional) user ID
+ @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)
+ @keyword auth_method: method that was used for authentication,
+ default: 'internal'
+ @keyword auth_attribs: tuple of user object attribute names that are
+ determined by auth method and should not be
+ changeable by preferences, default: ().
+ First tuple element was used for authentication.
+ """
+ self._cfg = request.cfg
+ self.valid = 0
+ self.id = id
+ self.auth_username = auth_username
+ self.auth_method = kw.get('auth_method', 'internal')
+ self.auth_attribs = kw.get('auth_attribs', ())
+ self.bookmarks = {} # interwikiname: bookmark
+
+ # create some vars automatically
+ self.__dict__.update(self._cfg.user_form_defaults)
+
+ if name:
+ self.name = name
+ elif auth_username: # this is needed for user_autocreate
+ self.name = auth_username
+
+ # create checkbox fields (with default 0)
+ for key, label in self._cfg.user_checkbox_fields:
+ setattr(self, key, self._cfg.user_checkbox_defaults.get(key, 0))
+
+ self.recoverpass_key = ""
+
+ self.enc_password = ""
+ if password:
+ try:
+ self.enc_password = encodePassword(password)
+ except UnicodeError:
+ pass # Should never happen
+
+ #self.edit_cols = 80
+ self.tz_offset = int(float(self._cfg.tz_offset) * 3600)
+ self.language = ""
+ self.real_language = "" # In case user uses "Browser setting". For language-statistics
+ self._stored = False
+ self.date_fmt = ""
+ self.datetime_fmt = ""
+ self.quicklinks = self._cfg.quicklinks_default
+ self.subscribed_pages = self._cfg.subscribed_pages_default
+ self.email_subscribed_events = self._cfg.email_subscribed_events_default
+ self.jabber_subscribed_events = self._cfg.jabber_subscribed_events_default
+ self.theme_name = self._cfg.theme_default
+ self.editor_default = self._cfg.editor_default
+ self.editor_ui = self._cfg.editor_ui
+ self.last_saved = str(time.time())
+
+ # attrs not saved to profile
+ self._request = request
+ self._trail = []
+
+ # we got an already authenticated username:
+ check_pass = 0
+ if not self.id and self.auth_username:
+ self.id = getUserId(request, self.auth_username)
+ if not password is None:
+ check_pass = 1
+ if self.id:
+ self.load_from_id(check_pass)
+ elif self.name:
+ self.id = getUserId(self._request, self.name)
+ if self.id:
+ self.load_from_id(1)
+ else:
+ self.id = self.make_id()
+ else:
+ self.id = self.make_id()
+
+ # "may" so we can say "if user.may.read(pagename):"
+ if self._cfg.SecurityPolicy:
+ self.may = self._cfg.SecurityPolicy(self)
+ else:
+ from MoinMoin.security import Default
+ self.may = Default(self)
+
+ if self.language and not self.language in i18n.wikiLanguages():
+ self.language = 'en'
+
+ def __repr__(self):
+ return "<%s.%s at 0x%x name:%r valid:%r>" % (
+ self.__class__.__module__, self.__class__.__name__,
+ id(self), self.name, self.valid)
+
+ def make_id(self):
+ """ make a new unique user id """
+ #!!! this should probably be a hash of REMOTE_ADDR, HTTP_USER_AGENT
+ # and some other things identifying remote users, then we could also
+ # use it reliably in edit locking
+ from random import randint
+ return "%s.%d" % (str(time.time()), randint(0, 65535))
+
+ def create_or_update(self, changed=False):
+ """ Create or update a user profile
+
+ @param changed: bool, set this to True if you updated the user profile values
+ """
+ if self._cfg.user_autocreate:
+ if not self.valid and not self.disabled or changed: # do we need to save/update?
+ self.save() # yes, create/update user profile
+
+ def __filename(self):
+ """ Get filename of the user's file on disk
+
+ @rtype: string
+ @return: full path and filename of user account file
+ """
+ return os.path.join(self._cfg.user_dir, self.id or "...NONE...")
+
+ def exists(self):
+ """ Do we have a user account for this user?
+
+ @rtype: bool
+ @return: true, if we have a user account
+ """
+ return os.path.exists(self.__filename())
+
+ def load_from_id(self, check_pass=0):
+ """ 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 check_pass: If 1, then self.enc_password must match the
+ password in the user account file.
+ """
+ if not self.exists():
+ return
+
+ data = codecs.open(self.__filename(), "r", config.charset).readlines()
+ user_data = {'enc_password': ''}
+ for line in data:
+ if line[0] == '#':
+ continue
+
+ try:
+ key, val = line.strip().split('=', 1)
+ if key not in self._cfg.user_transient_fields and key[0] != '_':
+ # Decode list values
+ if key.endswith('[]'):
+ key = key[:-2]
+ val = decodeList(val)
+ # Decode dict values
+ elif key.endswith('{}'):
+ key = key[:-2]
+ val = decodeDict(val)
+ # for compatibility reading old files, keep these explicit
+ # we will store them with [] appended
+ elif key in ['quicklinks', 'subscribed_pages', 'subscribed_events']:
+ val = decodeList(val)
+ user_data[key] = val
+ except ValueError:
+ pass
+
+ # 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
+
+ if check_pass:
+ # If we have no password set, we don't accept login with username
+ if not user_data['enc_password']:
+ return
+ # Check for a valid password, possibly changing encoding
+ valid, changed = self._validatePassword(user_data)
+ if not valid:
+ return
+
+ # Remove ignored checkbox values from user data
+ for key, label in self._cfg.user_checkbox_fields:
+ if key in user_data and key in self._cfg.user_checkbox_disable:
+ del user_data[key]
+
+ # Copy user data into user object
+ for key, val in user_data.items():
+ vars(self)[key] = val
+
+ self.tz_offset = int(self.tz_offset)
+
+ # Remove old unsupported attributes from user data file.
+ remove_attributes = ['passwd', 'show_emoticons']
+ for attr in remove_attributes:
+ if hasattr(self, attr):
+ delattr(self, attr)
+ changed = 1
+
+ # make sure checkboxes are boolean
+ for key, label in self._cfg.user_checkbox_fields:
+ try:
+ setattr(self, key, int(getattr(self, key)))
+ except ValueError:
+ setattr(self, key, 0)
+
+ # convert (old) hourly format to seconds
+ if -24 <= self.tz_offset and self.tz_offset <= 24:
+ self.tz_offset = self.tz_offset * 3600
+
+ # clear trail
+ self._trail = []
+
+ if not self.disabled:
+ self.valid = 1
+
+ # Mark this user as stored so saves don't send
+ # the "user created" event
+ self._stored = True
+
+ # If user data has been changed, save fixed user data.
+ if changed:
+ self.save()
+
+ def _validatePassword(self, data):
+ """ Try to validate user password
+
+ This is a private method and should not be used by clients.
+
+ In pre 1.3, the wiki used some 8 bit charset. The user password
+ was entered in this 8 bit password and passed to
+ encodePassword. So old passwords can use any of the charset
+ used.
+
+ In 1.3, we use unicode internally, so we encode the password in
+ encodePassword using utf-8.
+
+ When we compare passwords we must compare with same encoding, or
+ the passwords will not match. We don't know what encoding the
+ password on the user file uses. We may ask the wiki admin to put
+ this into the config, but he may be wrong.
+
+ The way chosen is to try to encode and compare passwords using
+ all the encoding that were available on 1.2, until we get a
+ match, which means that the user is valid.
+
+ If we get a match, we replace the user password hash with the
+ utf-8 encoded version, and next time it will match on first try
+ as before. The user password did not change, this change is
+ completely transparent for the user. Only the sha digest will
+ change.
+
+ @param data: dict with user data
+ @rtype: 2 tuple (bool, bool)
+ @return: password is valid, password did change
+ """
+ # First try with default encoded password. Match only non empty
+ # passwords. (require non empty enc_password)
+ if self.enc_password and self.enc_password == data['enc_password']:
+ return True, False
+
+ # Try to match using one of pre 1.3 8 bit charsets
+
+ # Get the clear text password from the form (require non empty
+ # password)
+ password = self._request.form.get('password', [None])[0]
+ if not password:
+ return False, False
+
+ # First get all available pre13 charsets on this system
+ pre13 = ['iso-8859-1', 'iso-8859-2', 'euc-jp', 'gb2312', 'big5', ]
+ available = []
+ for charset in pre13:
+ try:
+ encoder = codecs.getencoder(charset)
+ available.append(charset)
+ except LookupError:
+ pass # missing on this system
+
+ # Now try to match the password
+ for charset in available:
+ # Try to encode, failure is expected
+ try:
+ enc_password = encodePassword(password, charset=charset)
+ except UnicodeError:
+ continue
+
+ # And match (require non empty enc_password)
+ if enc_password and enc_password == data['enc_password']:
+ # User password match - replace the user password in the
+ # file with self.password
+ data['enc_password'] = self.enc_password
+ return True, True
+
+ # No encoded password match, this must be wrong password
+ return False, False
+
+ def persistent_items(self):
+ """ items we want to store into the user profile """
+ return [(key, value) for key, value in vars(self).items()
+ if key not in self._cfg.user_transient_fields and key[0] != '_']
+
+ def save(self):
+ """ Save user account data to user account file on disk.
+
+ This saves all member variables, except "id" and "valid" and
+ those starting with an underscore.
+ """
+ if not self.id:
+ return
+
+ user_dir = self._cfg.user_dir
+ if not os.path.exists(user_dir):
+ os.makedirs(user_dir)
+
+ self.last_saved = str(time.time())
+
+ # !!! should write to a temp file here to avoid race conditions,
+ # or even better, use locking
+
+ data = codecs.open(self.__filename(), "w", config.charset)
+ data.write("# Data saved '%s' for id '%s'\n" % (
+ time.strftime(self._cfg.datetime_fmt, time.localtime(time.time())),
+ self.id))
+ attrs = self.persistent_items()
+ attrs.sort()
+ for key, value in attrs:
+ # Encode list values
+ if isinstance(value, list):
+ key += '[]'
+ value = encodeList(value)
+ # Encode dict values
+ elif isinstance(value, dict):
+ key += '{}'
+ value = encodeDict(value)
+ line = u"%s=%s\n" % (key, unicode(value))
+ data.write(line)
+ data.close()
+
+ arena = 'user'
+ key = 'name2id'
+ caching.CacheEntry(self._request, arena, key, scope='wiki').remove()
+ try:
+ del self._request.cfg.cache.name2id
+ except:
+ pass
+ key = 'openid2id'
+ caching.CacheEntry(self._request, arena, key, scope='wiki').remove()
+ try:
+ del self._request.cfg.cache.openid2id
+ except:
+ pass
+
+ if not self.disabled:
+ self.valid = 1
+
+ if not self._stored:
+ self._stored = True
+ event = events.UserCreatedEvent(self._request, self)
+ events.send_event(event)
+
+ # -----------------------------------------------------------------
+ # Time and date formatting
+
+ def getTime(self, tm):
+ """ Get time in user's timezone.
+
+ @param tm: time (UTC UNIX timestamp)
+ @rtype: int
+ @return: tm tuple adjusted for user's timezone
+ """
+ return timefuncs.tmtuple(tm + self.tz_offset)
+
+
+ def getFormattedDate(self, tm):
+ """ Get formatted date adjusted for user's timezone.
+
+ @param tm: time (UTC UNIX timestamp)
+ @rtype: string
+ @return: formatted date, see cfg.date_fmt
+ """
+ date_fmt = self.date_fmt or self._cfg.date_fmt
+ return time.strftime(date_fmt, self.getTime(tm))
+
+
+ def getFormattedDateTime(self, tm):
+ """ Get formatted date and time adjusted for user's timezone.
+
+ @param tm: time (UTC UNIX timestamp)
+ @rtype: string
+ @return: formatted date and time, see cfg.datetime_fmt
+ """
+ datetime_fmt = self.datetime_fmt or self._cfg.datetime_fmt
+ return time.strftime(datetime_fmt, self.getTime(tm))
+
+ # -----------------------------------------------------------------
+ # Bookmark
+
+ def setBookmark(self, tm):
+ """ Set bookmark timestamp.
+
+ @param tm: timestamp
+ """
+ if self.valid:
+ interwikiname = unicode(self._cfg.interwikiname or '')
+ bookmark = unicode(tm)
+ self.bookmarks[interwikiname] = bookmark
+ self.save()
+
+ def getBookmark(self):
+ """ Get bookmark timestamp.
+
+ @rtype: int
+ @return: bookmark timestamp or None
+ """
+ bm = None
+ interwikiname = unicode(self._cfg.interwikiname or '')
+ if self.valid:
+ try:
+ bm = int(self.bookmarks[interwikiname])
+ except (ValueError, KeyError):
+ pass
+ return bm
+
+ def delBookmark(self):
+ """ Removes bookmark timestamp.
+
+ @rtype: int
+ @return: 0 on success, 1 on failure
+ """
+ interwikiname = unicode(self._cfg.interwikiname or '')
+ if self.valid:
+ try:
+ del self.bookmarks[interwikiname]
+ except KeyError:
+ return 1
+ self.save()
+ return 0
+ return 1
+
+ # -----------------------------------------------------------------
+ # Subscribe
+
+ def getSubscriptionList(self):
+ """ Get list of pages this user has subscribed to
+
+ @rtype: list
+ @return: pages this user has subscribed to
+ """
+ return self.subscribed_pages
+
+ def isSubscribedTo(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'
+
+ TODO: check if it's fast enough when getting called for many
+ users from page.getSubscribersList()
+
+ @param pagelist: list of pages to check for subscription
+ @rtype: bool
+ @return: if user is subscribed any page in pagelist
+ """
+ if not self.valid:
+ return False
+
+ import re
+ # Create a new list with both names and interwiki names.
+ pages = pagelist[:]
+ if self._cfg.interwikiname:
+ pages += [self._interWikiName(pagename) for pagename in pagelist]
+ # Create text for regular expression search
+ text = '\n'.join(pages)
+
+ for pattern in self.getSubscriptionList():
+ # Try simple match first
+ if pattern in pages:
+ return True
+ # Try regular expression search, skipping bad patterns
+ try:
+ pattern = re.compile(r'^%s$' % pattern, re.M)
+ except re.error:
+ continue
+ if pattern.search(text):
+ return True
+
+ return False
+
+ 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.
+
+ @param pagename: name of the page to subscribe
+ @type pagename: unicode
+ @rtype: bool
+ @return: if page was subscribed
+ """
+ if self._cfg.interwikiname:
+ pagename = self._interWikiName(pagename)
+
+ if pagename not in self.subscribed_pages:
+ self.subscribed_pages.append(pagename)
+ self.save()
+
+ # Send a notification
+ from MoinMoin.events import SubscribedToPageEvent, send_event
+ e = SubscribedToPageEvent(self._request, pagename, self.name)
+ send_event(e)
+ return True
+
+ return False
+
+ 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
+ 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.
+
+ @param pagename: name of the page to subscribe
+ @type pagename: unicode
+ @rtype: bool
+ @return: if unsubscrieb was successful. If the user has a
+ regular expression that match, it will always fail.
+ """
+ changed = False
+ if pagename in self.subscribed_pages:
+ self.subscribed_pages.remove(pagename)
+ changed = True
+
+ interWikiName = self._interWikiName(pagename)
+ if interWikiName and interWikiName in self.subscribed_pages:
+ self.subscribed_pages.remove(interWikiName)
+ changed = True
+
+ if changed:
+ self.save()
+ return not self.isSubscribedTo([pagename])
+
+ # -----------------------------------------------------------------
+ # Quicklinks
+
+ def getQuickLinks(self):
+ """ Get list of pages this user wants in the navibar
+
+ @rtype: list
+ @return: quicklinks from user account
+ """
+ return self.quicklinks
+
+ def isQuickLinkedTo(self, pagelist):
+ """ Check if user quicklink matches any page in pagelist.
+
+ @param pagelist: list of pages to check for quicklinks
+ @rtype: bool
+ @return: if user has quicklinked any page in pagelist
+ """
+ if not self.valid:
+ return False
+
+ for pagename in pagelist:
+ if pagename in self.quicklinks:
+ return True
+ interWikiName = self._interWikiName(pagename)
+ if interWikiName and interWikiName in self.quicklinks:
+ return True
+
+ return False
+
+ def addQuicklink(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.
+
+ @param pagename: page name
+ @type pagename: unicode
+ @rtype: bool
+ @return: if pagename was added
+ """
+ changed = False
+ interWikiName = self._interWikiName(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 changed:
+ self.save()
+ return changed
+
+ def removeQuicklink(self, pagename):
+ """ Remove a page from user quicklinks
+
+ Remove both interwiki and simple name from quicklinks.
+
+ @param pagename: page name
+ @type pagename: unicode
+ @rtype: bool
+ @return: if pagename was removed
+ """
+ changed = False
+ interWikiName = self._interWikiName(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 changed:
+ self.save()
+ return changed
+
+ def _interWikiName(self, pagename):
+ """ Return the inter wiki name of a page name
+
+ @param pagename: page name
+ @type pagename: unicode
+ """
+ if not self._cfg.interwikiname:
+ return None
+
+ return "%s:%s" % (self._cfg.interwikiname, pagename)
+
+ # -----------------------------------------------------------------
+ # Trail
+
+ def _wantTrail(self):
+ return (not self.valid and self._request.session.is_stored # anon session
+ or self.valid and (self.show_page_trail or self.remember_last_visit)) # logged-in session
+
+ def addTrail(self, page):
+ """ Add page to trail.
+
+ @param page: the page (object) to add to the trail
+ """
+ if self._wantTrail():
+ # load trail if not known
+ self.getTrail()
+
+ pagename = page.page_name
+ # Add only existing pages that the user may read
+ if not (page.exists() and self._request.user.may.read(pagename)):
+ return
+
+ # Save interwiki links internally
+ if self._cfg.interwikiname:
+ pagename = self._interWikiName(pagename)
+
+ # Don't append tail to trail ;)
+ if self._trail and self._trail[-1] == pagename:
+ return
+
+ # Append new page, limiting the length
+ self._trail = [p for p in self._trail if p != pagename]
+ self._trail = self._trail[-(self._cfg.trail_size-1):]
+ self._trail.append(pagename)
+ self.saveTrail()
+
+ def saveTrail(self):
+ """ Save trail into session """
+ if not self._request.session.is_new:
+ self._request.session['trail'] = self._trail
+
+ def getTrail(self):
+ """ Return list of recently visited pages.
+
+ @rtype: list
+ @return: pages in trail
+ """
+ if not self._trail and self._wantTrail():
+ trail = self._request.session.get('trail', [])
+ trail = [t.strip() for t in trail]
+ trail = [t for t in trail if t]
+ self._trail = trail[-self._cfg.trail_size:]
+
+ return self._trail
+
+ # -----------------------------------------------------------------
+ # Other
+
+ def isCurrentUser(self):
+ """ Check if this user object is the user doing the current request """
+ return self._request.user.name == self.name
+
+ def isSuperUser(self):
+ """ Check if this user is superuser """
+ request = self._request
+ if request.cfg.DesktopEdition and request.remote_addr == '127.0.0.1' and request.user and request.user.valid:
+ # the DesktopEdition gives any local user superuser powers
+ return True
+ superusers = request.cfg.superuser
+ assert isinstance(superusers, (list, tuple))
+ return self.valid and self.name and self.name in superusers
+
+ def host(self):
+ """ Return user host """
+ _ = self._request.getText
+ host = self.isCurrentUser() and self._cfg.show_hosts and self._request.remote_addr
+ return host or _("")
+
+ def wikiHomeLink(self):
+ """ Return wiki markup usable as a link to the user homepage,
+ it doesn't matter whether it already exists or not.
+ """
+ wikiname, pagename = wikiutil.getInterwikiHomePage(self._request, self.name)
+ if wikiname == 'Self':
+ if wikiutil.isStrictWikiname(self.name):
+ markup = pagename
+ else:
+ markup = '[[%s]]' % pagename
+ else:
+ markup = '[[%s:%s]]' % (wikiname, pagename)
+ return markup
+
+ def signature(self):
+ """ Return user signature using wiki markup
+
+ Users sign with a link to their homepage.
+ Visitors return their host address.
+
+ TODO: The signature use wiki format only, for example, it will
+ not create a link when using rst format. It will also break if
+ we change wiki syntax.
+ """
+ if self.name:
+ return self.wikiHomeLink()
+ else:
+ return self.host()
+
+ def generate_recovery_token(self):
+ key = random_string(64, "abcdefghijklmnopqrstuvwxyz0123456789")
+ msg = str(int(time.time()))
+ h = hmac.new(key, msg, sha).hexdigest()
+ self.recoverpass_key = key
+ self.save()
+ return msg + '-' + h
+
+ def apply_recovery_token(self, tok, newpass):
+ key = self.recoverpass_key
+ parts = tok.split('-')
+ if len(parts) != 2:
+ return False
+ try:
+ stamp = int(parts[0])
+ except ValueError:
+ return False
+ # only allow it to be valid for twelve hours
+ if stamp + 12*60*60 < time.time():
+ return False
+ # check hmac
+ h = hmac.new(self.recoverpass_key, str(stamp), sha).hexdigest()
+ if h != parts[1]:
+ return False
+ self.recoverpass_key = ""
+ self.enc_password = encodePassword(newpass)
+ self.save()
+ return True
+
+ def mailAccountData(self, cleartext_passwd=None):
+ """ Mail a user who forgot his password a message enabling
+ him to login again.
+ """
+ from MoinMoin.mail import sendmail
+ from MoinMoin.wikiutil import getLocalizedPage
+ _ = self._request.getText
+
+ tok = self.generate_recovery_token()
+
+ text = '\n' + _("""\
+Login Name: %s
+
+Password recovery token: %s
+
+Password reset URL: %s/?action=recoverpass&name=%s&token=%s
+""") % (
+ self.name,
+ tok,
+ self._request.getBaseURL(),
+ url_quote_plus(self.name),
+ tok, )
+
+ text = _("""\
+Somebody has requested to email you a password recovery token.
+
+If you lost your password, please go to the password reset URL below or
+go to the password recovery page again and enter your username and the
+recovery token.
+""") + text
+
+
+ subject = _('[%(sitename)s] Your wiki account data',
+ ) % {'sitename': self._cfg.sitename or "Wiki"}
+ mailok, msg = sendmail.sendmail(self._request, [self.email], subject,
+ text, mail_from=self._cfg.mail_from)
+ return mailok, msg
+
diff --git a/wiki-lenny/share/user.py b/wiki-lenny/share/user.py
new file mode 100644
index 00000000..9a28caef
--- /dev/null
+++ b/wiki-lenny/share/user.py
@@ -0,0 +1,1105 @@
+# -*- coding: iso-8859-1 -*-
+"""
+ MoinMoin - User Accounts
+
+ This module contains functions to access user accounts (list all users, get
+ some specific user). User instances are used to access the user profile of
+ some specific user (name, password, email, bookmark, trail, settings, ...).
+
+ Some related code is in the userform and userprefs modules.
+
+ TODO:
+ * code is a mixture of highlevel user stuff and lowlevel storage functions,
+ this has to get separated into:
+ * user object highlevel stuff
+ * storage code
+
+ @copyright: 2000-2004 Juergen Hermann ,
+ 2003-2007 MoinMoin:ThomasWaldmann
+ @license: GNU GPL, see COPYING for details.
+"""
+
+# add names here to hide them in the cgitb traceback
+unsafe_names = ("id", "key", "val", "user_data", "enc_password", "recoverpass_key")
+
+import os, time, sha, codecs, hmac
+
+from MoinMoin import config, caching, wikiutil, i18n, events
+from MoinMoin.util import timefuncs, filesys, random_string
+from MoinMoin.wikiutil import url_quote_plus
+### HACK SAUVAGE 1/2
+import socket
+### FIN HACK 1/2
+
+def getUserList(request):
+ """ Get a list of all (numerical) user IDs.
+
+ @param request: current request
+ @rtype: list
+ @return: all user IDs
+ """
+ import re
+ user_re = re.compile(r'^\d+\.\d+(\.\d+)?$')
+ files = filesys.dclistdir(request.cfg.user_dir)
+ userlist = [f for f in files if user_re.match(f)]
+ return userlist
+
+def get_by_filter(request, filter_func):
+ """ Searches for an user with a given filter function """
+ for uid in getUserList(request):
+ theuser = User(request, uid)
+ if filter_func(theuser):
+ return theuser
+
+def get_by_email_address(request, email_address):
+ """ Searches for an user with a particular e-mail address and returns it. """
+ filter_func = lambda user: user.valid and user.email.lower() == email_address.lower()
+ return get_by_filter(request, filter_func)
+
+def get_by_jabber_id(request, jabber_id):
+ """ Searches for an user with a perticular jabber id and returns it. """
+ filter_func = lambda user: user.valid and user.jid.lower() == jabber_id.lower()
+ return get_by_filter(request, filter_func)
+
+def _getUserIdByKey(request, key, search):
+ """ Get the user ID for a specified key/value pair.
+
+ This method must only be called for keys that are
+ guaranteed to be unique.
+
+ @param key: the key to look in
+ @param search: the value to look for
+ @return the corresponding user ID or None
+ """
+ if not search or not key:
+ return None
+ cfg = request.cfg
+ cachekey = '%s2id' % key
+ try:
+ _key2id = getattr(cfg.cache, cachekey)
+ except AttributeError:
+ arena = 'user'
+ cache = caching.CacheEntry(request, arena, cachekey, scope='wiki', use_pickle=True)
+ try:
+ _key2id = cache.content()
+ except caching.CacheError:
+ _key2id = {}
+ setattr(cfg.cache, cachekey, _key2id)
+ uid = _key2id.get(search, None)
+ if uid is None:
+ for userid in getUserList(request):
+ u = User(request, id=userid)
+ if hasattr(u, key):
+ value = getattr(u, key)
+ if isinstance(value, list):
+ for val in value:
+ _key2id[val] = userid
+ else:
+ _key2id[value] = userid
+ arena = 'user'
+ cache = caching.CacheEntry(request, arena, cachekey, scope='wiki', use_pickle=True)
+ try:
+ cache.update(_key2id)
+ except caching.CacheError:
+ pass
+ uid = _key2id.get(search, None)
+ return uid
+
+
+def getUserId(request, searchName):
+ """ Get the user ID for a specific user NAME.
+
+ @param searchName: the user name to look up
+ @rtype: string
+ @return: the corresponding user ID or None
+ """
+ return _getUserIdByKey(request, 'name', searchName)
+
+
+def getUserIdByOpenId(request, openid):
+ """ Get the user ID for a specific OpenID.
+
+ @param openid: the openid to look up
+ @rtype: string
+ @return: the corresponding user ID or None
+ """
+ return _getUserIdByKey(request, 'openids', openid)
+
+
+def getUserIdentification(request, username=None):
+ """ Return user name or IP or '' indicator.
+
+ @param request: the request object
+ @param username: (optional) user name
+ @rtype: string
+ @return: user name or IP or unknown indicator
+ """
+ _ = request.getText
+
+ if username is None:
+ username = request.user.name
+
+ return username or (request.cfg.show_hosts and request.remote_addr) or _("")
+
+
+def encodePassword(pwd, charset='utf-8'):
+ """ Encode a cleartext password
+
+ Compatible to Apache htpasswd SHA encoding.
+
+ When using different encoding than 'utf-8', the encoding might fail
+ and raise UnicodeError.
+
+ @param pwd: the cleartext password, (unicode)
+ @param charset: charset used to encode password, used only for
+ compatibility with old passwords generated on moin-1.2.
+ @rtype: string
+ @return: the password in apache htpasswd compatible SHA-encoding,
+ or None
+ """
+ import base64
+
+ # Might raise UnicodeError, but we can't do anything about it here,
+ # so let the caller handle it.
+ pwd = pwd.encode(charset)
+
+ pwd = sha.new(pwd).digest()
+ pwd = '{SHA}' + base64.encodestring(pwd).rstrip()
+ return pwd
+
+
+def normalizeName(name):
+ """ Make normalized user name
+
+ Prevent impersonating another user with names containing leading,
+ trailing or multiple whitespace, or using invisible unicode
+ characters.
+
+ Prevent creating user page as sub page, because '/' is not allowed
+ in user names.
+
+ Prevent using ':' and ',' which are reserved by acl.
+
+ @param name: user name, unicode
+ @rtype: unicode
+ @return: user name that can be used in acl lines
+ """
+ username_allowedchars = "'@.-_" # ' for names like O'Brian or email addresses.
+ # "," and ":" must not be allowed (ACL delimiters).
+ # We also allow _ in usernames for nicer URLs.
+ # Strip non alpha numeric characters (except username_allowedchars), keep white space
+ name = ''.join([c for c in name if c.isalnum() or c.isspace() or c in username_allowedchars])
+
+ # Normalize white space. Each name can contain multiple
+ # words separated with only one space.
+ name = ' '.join(name.split())
+
+ return name
+
+
+def isValidName(request, name):
+ """ Validate user name
+
+ @param name: user name, unicode
+ """
+ normalized = normalizeName(name)
+ return (name == normalized) and not wikiutil.isGroupPage(request, name)
+
+
+def encodeList(items):
+ """ Encode list of items in user data file
+
+ Items are separated by '\t' characters.
+
+ @param items: list unicode strings
+ @rtype: unicode
+ @return: list encoded as unicode
+ """
+ line = []
+ for item in items:
+ item = item.strip()
+ if not item:
+ continue
+ line.append(item)
+
+ line = '\t'.join(line)
+ return line
+
+def decodeList(line):
+ """ Decode list of items from user data file
+
+ @param line: line containing list of items, encoded with encodeList
+ @rtype: list of unicode strings
+ @return: list of items in encoded in line
+ """
+ items = []
+ for item in line.split('\t'):
+ item = item.strip()
+ if not item:
+ continue
+ items.append(item)
+ return items
+
+def encodeDict(items):
+ """ Encode dict of items in user data file
+
+ Items are separated by '\t' characters.
+ Each item is key:value.
+
+ @param items: dict of unicode:unicode
+ @rtype: unicode
+ @return: dict encoded as unicode
+ """
+ line = []
+ for key, value in items.items():
+ item = u'%s:%s' % (key, value)
+ line.append(item)
+ line = '\t'.join(line)
+ return line
+
+def decodeDict(line):
+ """ Decode dict of key:value pairs from user data file
+
+ @param line: line containing a dict, encoded with encodeDict
+ @rtype: dict
+ @return: dict unicode:unicode items
+ """
+ items = {}
+ for item in line.split('\t'):
+ item = item.strip()
+ if not item:
+ continue
+ key, value = item.split(':', 1)
+ items[key] = value
+ return items
+
+
+class User:
+ """ A MoinMoin User """
+
+ def __init__(self, request, id=None, name="", password=None, auth_username="", **kw):
+ """ Initialize User object
+
+ TODO: when this gets refactored, use "uid" not builtin "id"
+
+ @param request: the request object
+ @param id: (optional) user ID
+ @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)
+ @keyword auth_method: method that was used for authentication,
+ default: 'internal'
+ @keyword auth_attribs: tuple of user object attribute names that are
+ determined by auth method and should not be
+ changeable by preferences, default: ().
+ First tuple element was used for authentication.
+ """
+ self._cfg = request.cfg
+ self.valid = 0
+ self.id = id
+ self.auth_username = auth_username
+ self.auth_method = kw.get('auth_method', 'internal')
+ self.auth_attribs = kw.get('auth_attribs', ())
+ self.bookmarks = {} # interwikiname: bookmark
+
+ # create some vars automatically
+ self.__dict__.update(self._cfg.user_form_defaults)
+
+ if name:
+ self.name = name
+ elif auth_username: # this is needed for user_autocreate
+ self.name = auth_username
+
+ # create checkbox fields (with default 0)
+ for key, label in self._cfg.user_checkbox_fields:
+ setattr(self, key, self._cfg.user_checkbox_defaults.get(key, 0))
+
+ self.recoverpass_key = ""
+
+ self.enc_password = ""
+ if password:
+ try:
+ self.enc_password = encodePassword(password)
+ except UnicodeError:
+ pass # Should never happen
+
+ #self.edit_cols = 80
+ self.tz_offset = int(float(self._cfg.tz_offset) * 3600)
+ self.language = ""
+ self.real_language = "" # In case user uses "Browser setting". For language-statistics
+ self._stored = False
+ self.date_fmt = ""
+ self.datetime_fmt = ""
+ self.quicklinks = self._cfg.quicklinks_default
+ self.subscribed_pages = self._cfg.subscribed_pages_default
+ self.email_subscribed_events = self._cfg.email_subscribed_events_default
+ self.jabber_subscribed_events = self._cfg.jabber_subscribed_events_default
+ self.theme_name = self._cfg.theme_default
+ self.editor_default = self._cfg.editor_default
+ self.editor_ui = self._cfg.editor_ui
+ self.last_saved = str(time.time())
+
+ # attrs not saved to profile
+ self._request = request
+ self._trail = []
+
+ # we got an already authenticated username:
+ check_pass = 0
+ if not self.id and self.auth_username:
+ self.id = getUserId(request, self.auth_username)
+ if not password is None:
+ check_pass = 1
+ if self.id:
+ self.load_from_id(check_pass)
+ elif self.name:
+ self.id = getUserId(self._request, self.name)
+ if self.id:
+ self.load_from_id(1)
+ else:
+ self.id = self.make_id()
+ else:
+ self.id = self.make_id()
+
+ # "may" so we can say "if user.may.read(pagename):"
+ if self._cfg.SecurityPolicy:
+ self.may = self._cfg.SecurityPolicy(self)
+ else:
+ from MoinMoin.security import Default
+ self.may = Default(self)
+
+ if self.language and not self.language in i18n.wikiLanguages():
+ self.language = 'en'
+
+ def __repr__(self):
+ return "<%s.%s at 0x%x name:%r valid:%r>" % (
+ self.__class__.__module__, self.__class__.__name__,
+ id(self), self.name, self.valid)
+
+ def make_id(self):
+ """ make a new unique user id """
+ #!!! this should probably be a hash of REMOTE_ADDR, HTTP_USER_AGENT
+ # and some other things identifying remote users, then we could also
+ # use it reliably in edit locking
+ from random import randint
+ return "%s.%d" % (str(time.time()), randint(0, 65535))
+
+ def create_or_update(self, changed=False):
+ """ Create or update a user profile
+
+ @param changed: bool, set this to True if you updated the user profile values
+ """
+ if self._cfg.user_autocreate:
+ if not self.valid and not self.disabled or changed: # do we need to save/update?
+ self.save() # yes, create/update user profile
+
+ def __filename(self):
+ """ Get filename of the user's file on disk
+
+ @rtype: string
+ @return: full path and filename of user account file
+ """
+ return os.path.join(self._cfg.user_dir, self.id or "...NONE...")
+
+ def exists(self):
+ """ Do we have a user account for this user?
+
+ @rtype: bool
+ @return: true, if we have a user account
+ """
+ return os.path.exists(self.__filename())
+
+ def load_from_id(self, check_pass=0):
+ """ 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 check_pass: If 1, then self.enc_password must match the
+ password in the user account file.
+ """
+ if not self.exists():
+ return
+
+ data = codecs.open(self.__filename(), "r", config.charset).readlines()
+ user_data = {'enc_password': ''}
+ for line in data:
+ if line[0] == '#':
+ continue
+
+ try:
+ key, val = line.strip().split('=', 1)
+ if key not in self._cfg.user_transient_fields and key[0] != '_':
+ # Decode list values
+ if key.endswith('[]'):
+ key = key[:-2]
+ val = decodeList(val)
+ # Decode dict values
+ elif key.endswith('{}'):
+ key = key[:-2]
+ val = decodeDict(val)
+ # for compatibility reading old files, keep these explicit
+ # we will store them with [] appended
+ elif key in ['quicklinks', 'subscribed_pages', 'subscribed_events']:
+ val = decodeList(val)
+ user_data[key] = val
+ except ValueError:
+ pass
+
+ # 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
+
+ if check_pass:
+ # If we have no password set, we don't accept login with username
+ if not user_data['enc_password']:
+ return
+ # Check for a valid password, possibly changing encoding
+ valid, changed = self._validatePassword(user_data)
+ if not valid:
+ return
+
+ # Remove ignored checkbox values from user data
+ for key, label in self._cfg.user_checkbox_fields:
+ if key in user_data and key in self._cfg.user_checkbox_disable:
+ del user_data[key]
+
+ # Copy user data into user object
+ for key, val in user_data.items():
+ vars(self)[key] = val
+
+ self.tz_offset = int(self.tz_offset)
+
+ # Remove old unsupported attributes from user data file.
+ remove_attributes = ['passwd', 'show_emoticons']
+ for attr in remove_attributes:
+ if hasattr(self, attr):
+ delattr(self, attr)
+ changed = 1
+
+ # make sure checkboxes are boolean
+ for key, label in self._cfg.user_checkbox_fields:
+ try:
+ setattr(self, key, int(getattr(self, key)))
+ except ValueError:
+ setattr(self, key, 0)
+
+ # convert (old) hourly format to seconds
+ if -24 <= self.tz_offset and self.tz_offset <= 24:
+ self.tz_offset = self.tz_offset * 3600
+
+ # clear trail
+ self._trail = []
+
+ if not self.disabled:
+ self.valid = 1
+
+ # Mark this user as stored so saves don't send
+ # the "user created" event
+ self._stored = True
+
+ # If user data has been changed, save fixed user data.
+ if changed:
+ self.save()
+
+ def _validatePassword(self, data):
+ """ Try to validate user password
+
+ This is a private method and should not be used by clients.
+
+ In pre 1.3, the wiki used some 8 bit charset. The user password
+ was entered in this 8 bit password and passed to
+ encodePassword. So old passwords can use any of the charset
+ used.
+
+ In 1.3, we use unicode internally, so we encode the password in
+ encodePassword using utf-8.
+
+ When we compare passwords we must compare with same encoding, or
+ the passwords will not match. We don't know what encoding the
+ password on the user file uses. We may ask the wiki admin to put
+ this into the config, but he may be wrong.
+
+ The way chosen is to try to encode and compare passwords using
+ all the encoding that were available on 1.2, until we get a
+ match, which means that the user is valid.
+
+ If we get a match, we replace the user password hash with the
+ utf-8 encoded version, and next time it will match on first try
+ as before. The user password did not change, this change is
+ completely transparent for the user. Only the sha digest will
+ change.
+
+ @param data: dict with user data
+ @rtype: 2 tuple (bool, bool)
+ @return: password is valid, password did change
+ """
+ # First try with default encoded password. Match only non empty
+ # passwords. (require non empty enc_password)
+ if self.enc_password and self.enc_password == data['enc_password']:
+ return True, False
+
+ # Try to match using one of pre 1.3 8 bit charsets
+
+ # Get the clear text password from the form (require non empty
+ # password)
+ password = self._request.form.get('password', [None])[0]
+ if not password:
+ return False, False
+
+ # First get all available pre13 charsets on this system
+ pre13 = ['iso-8859-1', 'iso-8859-2', 'euc-jp', 'gb2312', 'big5', ]
+ available = []
+ for charset in pre13:
+ try:
+ encoder = codecs.getencoder(charset)
+ available.append(charset)
+ except LookupError:
+ pass # missing on this system
+
+ # Now try to match the password
+ for charset in available:
+ # Try to encode, failure is expected
+ try:
+ enc_password = encodePassword(password, charset=charset)
+ except UnicodeError:
+ continue
+
+ # And match (require non empty enc_password)
+ if enc_password and enc_password == data['enc_password']:
+ # User password match - replace the user password in the
+ # file with self.password
+ data['enc_password'] = self.enc_password
+ return True, True
+
+ # No encoded password match, this must be wrong password
+ return False, False
+
+ def persistent_items(self):
+ """ items we want to store into the user profile """
+ return [(key, value) for key, value in vars(self).items()
+ if key not in self._cfg.user_transient_fields and key[0] != '_']
+
+ def save(self):
+ """ Save user account data to user account file on disk.
+
+ This saves all member variables, except "id" and "valid" and
+ those starting with an underscore.
+ """
+ if not self.id:
+ return
+
+ user_dir = self._cfg.user_dir
+ if not os.path.exists(user_dir):
+ os.makedirs(user_dir)
+
+ self.last_saved = str(time.time())
+
+ # !!! should write to a temp file here to avoid race conditions,
+ # or even better, use locking
+
+ data = codecs.open(self.__filename(), "w", config.charset)
+ data.write("# Data saved '%s' for id '%s'\n" % (
+ time.strftime(self._cfg.datetime_fmt, time.localtime(time.time())),
+ self.id))
+ attrs = self.persistent_items()
+ attrs.sort()
+ for key, value in attrs:
+ # Encode list values
+ if isinstance(value, list):
+ key += '[]'
+ value = encodeList(value)
+ # Encode dict values
+ elif isinstance(value, dict):
+ key += '{}'
+ value = encodeDict(value)
+ line = u"%s=%s\n" % (key, unicode(value))
+ data.write(line)
+ data.close()
+
+ arena = 'user'
+ key = 'name2id'
+ caching.CacheEntry(self._request, arena, key, scope='wiki').remove()
+ try:
+ del self._request.cfg.cache.name2id
+ except:
+ pass
+ key = 'openid2id'
+ caching.CacheEntry(self._request, arena, key, scope='wiki').remove()
+ try:
+ del self._request.cfg.cache.openid2id
+ except:
+ pass
+
+ if not self.disabled:
+ self.valid = 1
+
+ if not self._stored:
+ self._stored = True
+ event = events.UserCreatedEvent(self._request, self)
+ events.send_event(event)
+
+ # -----------------------------------------------------------------
+ # Time and date formatting
+
+ def getTime(self, tm):
+ """ Get time in user's timezone.
+
+ @param tm: time (UTC UNIX timestamp)
+ @rtype: int
+ @return: tm tuple adjusted for user's timezone
+ """
+ return timefuncs.tmtuple(tm + self.tz_offset)
+
+
+ def getFormattedDate(self, tm):
+ """ Get formatted date adjusted for user's timezone.
+
+ @param tm: time (UTC UNIX timestamp)
+ @rtype: string
+ @return: formatted date, see cfg.date_fmt
+ """
+ date_fmt = self.date_fmt or self._cfg.date_fmt
+ return time.strftime(date_fmt, self.getTime(tm))
+
+
+ def getFormattedDateTime(self, tm):
+ """ Get formatted date and time adjusted for user's timezone.
+
+ @param tm: time (UTC UNIX timestamp)
+ @rtype: string
+ @return: formatted date and time, see cfg.datetime_fmt
+ """
+ datetime_fmt = self.datetime_fmt or self._cfg.datetime_fmt
+ return time.strftime(datetime_fmt, self.getTime(tm))
+
+ # -----------------------------------------------------------------
+ # Bookmark
+
+ def setBookmark(self, tm):
+ """ Set bookmark timestamp.
+
+ @param tm: timestamp
+ """
+ if self.valid:
+ interwikiname = unicode(self._cfg.interwikiname or '')
+ bookmark = unicode(tm)
+ self.bookmarks[interwikiname] = bookmark
+ self.save()
+
+ def getBookmark(self):
+ """ Get bookmark timestamp.
+
+ @rtype: int
+ @return: bookmark timestamp or None
+ """
+ bm = None
+ interwikiname = unicode(self._cfg.interwikiname or '')
+ if self.valid:
+ try:
+ bm = int(self.bookmarks[interwikiname])
+ except (ValueError, KeyError):
+ pass
+ return bm
+
+ def delBookmark(self):
+ """ Removes bookmark timestamp.
+
+ @rtype: int
+ @return: 0 on success, 1 on failure
+ """
+ interwikiname = unicode(self._cfg.interwikiname or '')
+ if self.valid:
+ try:
+ del self.bookmarks[interwikiname]
+ except KeyError:
+ return 1
+ self.save()
+ return 0
+ return 1
+
+ # -----------------------------------------------------------------
+ # Subscribe
+
+ def getSubscriptionList(self):
+ """ Get list of pages this user has subscribed to
+
+ @rtype: list
+ @return: pages this user has subscribed to
+ """
+ return self.subscribed_pages
+
+ def isSubscribedTo(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'
+
+ TODO: check if it's fast enough when getting called for many
+ users from page.getSubscribersList()
+
+ @param pagelist: list of pages to check for subscription
+ @rtype: bool
+ @return: if user is subscribed any page in pagelist
+ """
+ if not self.valid:
+ return False
+
+ import re
+ # Create a new list with both names and interwiki names.
+ pages = pagelist[:]
+ if self._cfg.interwikiname:
+ pages += [self._interWikiName(pagename) for pagename in pagelist]
+ # Create text for regular expression search
+ text = '\n'.join(pages)
+
+ for pattern in self.getSubscriptionList():
+ # Try simple match first
+ if pattern in pages:
+ return True
+ # Try regular expression search, skipping bad patterns
+ try:
+ pattern = re.compile(r'^%s$' % pattern, re.M)
+ except re.error:
+ continue
+ if pattern.search(text):
+ return True
+
+ return False
+
+ 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.
+
+ @param pagename: name of the page to subscribe
+ @type pagename: unicode
+ @rtype: bool
+ @return: if page was subscribed
+ """
+ if self._cfg.interwikiname:
+ pagename = self._interWikiName(pagename)
+
+ if pagename not in self.subscribed_pages:
+ self.subscribed_pages.append(pagename)
+ self.save()
+
+ # Send a notification
+ from MoinMoin.events import SubscribedToPageEvent, send_event
+ e = SubscribedToPageEvent(self._request, pagename, self.name)
+ send_event(e)
+ return True
+
+ return False
+
+ 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
+ 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.
+
+ @param pagename: name of the page to subscribe
+ @type pagename: unicode
+ @rtype: bool
+ @return: if unsubscrieb was successful. If the user has a
+ regular expression that match, it will always fail.
+ """
+ changed = False
+ if pagename in self.subscribed_pages:
+ self.subscribed_pages.remove(pagename)
+ changed = True
+
+ interWikiName = self._interWikiName(pagename)
+ if interWikiName and interWikiName in self.subscribed_pages:
+ self.subscribed_pages.remove(interWikiName)
+ changed = True
+
+ if changed:
+ self.save()
+ return not self.isSubscribedTo([pagename])
+
+ # -----------------------------------------------------------------
+ # Quicklinks
+
+ def getQuickLinks(self):
+ """ Get list of pages this user wants in the navibar
+
+ @rtype: list
+ @return: quicklinks from user account
+ """
+ return self.quicklinks
+
+ def isQuickLinkedTo(self, pagelist):
+ """ Check if user quicklink matches any page in pagelist.
+
+ @param pagelist: list of pages to check for quicklinks
+ @rtype: bool
+ @return: if user has quicklinked any page in pagelist
+ """
+ if not self.valid:
+ return False
+
+ for pagename in pagelist:
+ if pagename in self.quicklinks:
+ return True
+ interWikiName = self._interWikiName(pagename)
+ if interWikiName and interWikiName in self.quicklinks:
+ return True
+
+ return False
+
+ def addQuicklink(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.
+
+ @param pagename: page name
+ @type pagename: unicode
+ @rtype: bool
+ @return: if pagename was added
+ """
+ changed = False
+ interWikiName = self._interWikiName(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 changed:
+ self.save()
+ return changed
+
+ def removeQuicklink(self, pagename):
+ """ Remove a page from user quicklinks
+
+ Remove both interwiki and simple name from quicklinks.
+
+ @param pagename: page name
+ @type pagename: unicode
+ @rtype: bool
+ @return: if pagename was removed
+ """
+ changed = False
+ interWikiName = self._interWikiName(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 changed:
+ self.save()
+ return changed
+
+ def _interWikiName(self, pagename):
+ """ Return the inter wiki name of a page name
+
+ @param pagename: page name
+ @type pagename: unicode
+ """
+ if not self._cfg.interwikiname:
+ return None
+
+ return "%s:%s" % (self._cfg.interwikiname, pagename)
+
+ # -----------------------------------------------------------------
+ # Trail
+
+ def _wantTrail(self):
+ return (not self.valid and self._request.session.is_stored # anon session
+ or self.valid and (self.show_page_trail or self.remember_last_visit)) # logged-in session
+
+ def addTrail(self, page):
+ """ Add page to trail.
+
+ @param page: the page (object) to add to the trail
+ """
+ if self._wantTrail():
+ # load trail if not known
+ self.getTrail()
+
+ pagename = page.page_name
+ # Add only existing pages that the user may read
+ if not (page.exists() and self._request.user.may.read(pagename)):
+ return
+
+ # Save interwiki links internally
+ if self._cfg.interwikiname:
+ pagename = self._interWikiName(pagename)
+
+ # Don't append tail to trail ;)
+ if self._trail and self._trail[-1] == pagename:
+ return
+
+ # Append new page, limiting the length
+ self._trail = [p for p in self._trail if p != pagename]
+ self._trail = self._trail[-(self._cfg.trail_size-1):]
+ self._trail.append(pagename)
+ self.saveTrail()
+
+ def saveTrail(self):
+ """ Save trail into session """
+ if not self._request.session.is_new:
+ self._request.session['trail'] = self._trail
+
+ def getTrail(self):
+ """ Return list of recently visited pages.
+
+ @rtype: list
+ @return: pages in trail
+ """
+ if not self._trail and self._wantTrail():
+ trail = self._request.session.get('trail', [])
+ trail = [t.strip() for t in trail]
+ trail = [t for t in trail if t]
+ self._trail = trail[-self._cfg.trail_size:]
+
+ return self._trail
+
+ # -----------------------------------------------------------------
+ # Other
+
+ def isCurrentUser(self):
+ """ Check if this user object is the user doing the current request """
+ return self._request.user.name == self.name
+
+ def isSuperUser(self):
+ """ Check if this user is superuser """
+ request = self._request
+ if request.cfg.DesktopEdition and request.remote_addr == '127.0.0.1' and request.user and request.user.valid:
+ # the DesktopEdition gives any local user superuser powers
+ return True
+ superusers = request.cfg.superuser
+ assert isinstance(superusers, (list, tuple))
+ return self.valid and self.name and self.name in superusers
+
+ def host(self):
+ """ Return user host """
+ _ = self._request.getText
+ host = self.isCurrentUser() and self._cfg.show_hosts and self._request.remote_addr
+ return host or _("")
+
+ def wikiHomeLink(self):
+ """ Return wiki markup usable as a link to the user homepage,
+ it doesn't matter whether it already exists or not.
+ """
+ wikiname, pagename = wikiutil.getInterwikiHomePage(self._request, self.name)
+ if wikiname == 'Self':
+ if wikiutil.isStrictWikiname(self.name):
+ markup = pagename
+ else:
+ markup = '[[%s]]' % pagename
+ else:
+ markup = '[[%s:%s]]' % (wikiname, pagename)
+ return markup
+
+ def signature(self):
+ """ Return user signature using wiki markup
+
+ Users sign with a link to their homepage.
+ Visitors return their host address.
+
+ TODO: The signature use wiki format only, for example, it will
+ not create a link when using rst format. It will also break if
+ we change wiki syntax.
+ """
+ if self.name:
+ return self.wikiHomeLink()
+ else:
+ ### HACK SAUVAGE 2/2
+ # return self.host()
+ try:
+ hostname = socket.gethostbyaddr(self.host())[0]
+ except socket.herror:
+ hostname = self.host()
+ if (hostname+'.').split('.', 1)[1] in ['crans.org.', 'wifi.crans.org.']:
+ hostname = hostname.split('.',1)[0]
+ return hostname
+ ### FIN HACK 2/2
+
+ def generate_recovery_token(self):
+ key = random_string(64, "abcdefghijklmnopqrstuvwxyz0123456789")
+ msg = str(int(time.time()))
+ h = hmac.new(key, msg, sha).hexdigest()
+ self.recoverpass_key = key
+ self.save()
+ return msg + '-' + h
+
+ def apply_recovery_token(self, tok, newpass):
+ key = self.recoverpass_key
+ parts = tok.split('-')
+ if len(parts) != 2:
+ return False
+ try:
+ stamp = int(parts[0])
+ except ValueError:
+ return False
+ # only allow it to be valid for twelve hours
+ if stamp + 12*60*60 < time.time():
+ return False
+ # check hmac
+ h = hmac.new(self.recoverpass_key, str(stamp), sha).hexdigest()
+ if h != parts[1]:
+ return False
+ self.recoverpass_key = ""
+ self.enc_password = encodePassword(newpass)
+ self.save()
+ return True
+
+ def mailAccountData(self, cleartext_passwd=None):
+ """ Mail a user who forgot his password a message enabling
+ him to login again.
+ """
+ from MoinMoin.mail import sendmail
+ from MoinMoin.wikiutil import getLocalizedPage
+ _ = self._request.getText
+
+ tok = self.generate_recovery_token()
+
+ text = '\n' + _("""\
+Login Name: %s
+
+Password recovery token: %s
+
+Password reset URL: %s/?action=recoverpass&name=%s&token=%s
+""") % (
+ self.name,
+ tok,
+ self._request.getBaseURL(),
+ url_quote_plus(self.name),
+ tok, )
+
+ text = _("""\
+Somebody has requested to email you a password recovery token.
+
+If you lost your password, please go to the password reset URL below or
+go to the password recovery page again and enter your username and the
+recovery token.
+""") + text
+
+
+ subject = _('[%(sitename)s] Your wiki account data',
+ ) % {'sitename': self._cfg.sitename or "Wiki"}
+ mailok, msg = sendmail.sendmail(self._request, [self.email], subject,
+ text, mail_from=self._cfg.mail_from)
+ return mailok, msg
+