Back to index

moin  1.9.0~rc2
user.py
Go to the documentation of this file.
00001 # -*- coding: iso-8859-1 -*-
00002 """
00003     MoinMoin - User Accounts
00004 
00005     This module contains functions to access user accounts (list all users, get
00006     some specific user). User instances are used to access the user profile of
00007     some specific user (name, password, email, bookmark, trail, settings, ...).
00008 
00009     Some related code is in the userform and userprefs modules.
00010 
00011     TODO:
00012     * code is a mixture of highlevel user stuff and lowlevel storage functions,
00013       this has to get separated into:
00014       * user object highlevel stuff
00015       * storage code
00016 
00017     @copyright: 2000-2004 Juergen Hermann <jh@web.de>,
00018                 2003-2007 MoinMoin:ThomasWaldmann
00019     @license: GNU GPL, see COPYING for details.
00020 """
00021 
00022 import os, time, codecs, base64
00023 
00024 from MoinMoin.support.python_compatibility import hash_new, hmac_new
00025 
00026 from MoinMoin import config, caching, wikiutil, i18n, events
00027 from MoinMoin.util import timefuncs, filesys, random_string
00028 from MoinMoin.wikiutil import url_quote_plus
00029 
00030 
00031 def getUserList(request):
00032     """ Get a list of all (numerical) user IDs.
00033 
00034     @param request: current request
00035     @rtype: list
00036     @return: all user IDs
00037     """
00038     import re
00039     user_re = re.compile(r'^\d+\.\d+(\.\d+)?$')
00040     files = filesys.dclistdir(request.cfg.user_dir)
00041     userlist = [f for f in files if user_re.match(f)]
00042     return userlist
00043 
00044 def get_by_filter(request, filter_func):
00045     """ Searches for an user with a given filter function """
00046     for uid in getUserList(request):
00047         theuser = User(request, uid)
00048         if filter_func(theuser):
00049             return theuser
00050 
00051 def get_by_email_address(request, email_address):
00052     """ Searches for an user with a particular e-mail address and returns it. """
00053     filter_func = lambda user: user.valid and user.email.lower() == email_address.lower()
00054     return get_by_filter(request, filter_func)
00055 
00056 def get_by_jabber_id(request, jabber_id):
00057     """ Searches for an user with a perticular jabber id and returns it. """
00058     filter_func = lambda user: user.valid and user.jid.lower() == jabber_id.lower()
00059     return get_by_filter(request, filter_func)
00060 
00061 def _getUserIdByKey(request, key, search):
00062     """ Get the user ID for a specified key/value pair.
00063 
00064     This method must only be called for keys that are
00065     guaranteed to be unique.
00066 
00067     @param key: the key to look in
00068     @param search: the value to look for
00069     @return the corresponding user ID or None
00070     """
00071     if not search or not key:
00072         return None
00073     cfg = request.cfg
00074     cachekey = '%s2id' % key
00075     try:
00076         _key2id = getattr(cfg.cache, cachekey)
00077     except AttributeError:
00078         arena = 'user'
00079         cache = caching.CacheEntry(request, arena, cachekey, scope='wiki', use_pickle=True)
00080         try:
00081             _key2id = cache.content()
00082         except caching.CacheError:
00083             _key2id = {}
00084         setattr(cfg.cache, cachekey, _key2id)
00085     uid = _key2id.get(search, None)
00086     if uid is None:
00087         for userid in getUserList(request):
00088             u = User(request, id=userid)
00089             if hasattr(u, key):
00090                 value = getattr(u, key)
00091                 if isinstance(value, list):
00092                     for val in value:
00093                         _key2id[val] = userid
00094                 else:
00095                     _key2id[value] = userid
00096         arena = 'user'
00097         cache = caching.CacheEntry(request, arena, cachekey, scope='wiki', use_pickle=True)
00098         try:
00099             cache.update(_key2id)
00100         except caching.CacheError:
00101             pass
00102         uid = _key2id.get(search, None)
00103     return uid
00104 
00105 
00106 def getUserId(request, searchName):
00107     """ Get the user ID for a specific user NAME.
00108 
00109     @param searchName: the user name to look up
00110     @rtype: string
00111     @return: the corresponding user ID or None
00112     """
00113     return _getUserIdByKey(request, 'name', searchName)
00114 
00115 
00116 def getUserIdByOpenId(request, openid):
00117     """ Get the user ID for a specific OpenID.
00118 
00119     @param openid: the openid to look up
00120     @rtype: string
00121     @return: the corresponding user ID or None
00122     """
00123     return _getUserIdByKey(request, 'openids', openid)
00124 
00125 
00126 def getUserIdentification(request, username=None):
00127     """ Return user name or IP or '<unknown>' indicator.
00128 
00129     @param request: the request object
00130     @param username: (optional) user name
00131     @rtype: string
00132     @return: user name or IP or unknown indicator
00133     """
00134     _ = request.getText
00135 
00136     if username is None:
00137         username = request.user.name
00138 
00139     return username or (request.cfg.show_hosts and request.remote_addr) or _("<unknown>")
00140 
00141 
00142 def encodePassword(pwd, salt=None):
00143     """ Encode a cleartext password
00144 
00145     @param pwd: the cleartext password, (unicode)
00146     @param salt: the salt for the password (string)
00147     @rtype: string
00148     @return: the password in apache htpasswd compatible SHA-encoding,
00149         or None
00150     """
00151     pwd = pwd.encode('utf-8')
00152 
00153     if salt is None:
00154         salt = random_string(20)
00155     assert isinstance(salt, str)
00156     hash = hash_new('sha1', pwd)
00157     hash.update(salt)
00158 
00159     return '{SSHA}' + base64.encodestring(hash.digest() + salt).rstrip()
00160 
00161 
00162 def normalizeName(name):
00163     """ Make normalized user name
00164 
00165     Prevent impersonating another user with names containing leading,
00166     trailing or multiple whitespace, or using invisible unicode
00167     characters.
00168 
00169     Prevent creating user page as sub page, because '/' is not allowed
00170     in user names.
00171 
00172     Prevent using ':' and ',' which are reserved by acl.
00173 
00174     @param name: user name, unicode
00175     @rtype: unicode
00176     @return: user name that can be used in acl lines
00177     """
00178     username_allowedchars = "'@.-_" # ' for names like O'Brian or email addresses.
00179                                     # "," and ":" must not be allowed (ACL delimiters).
00180                                     # We also allow _ in usernames for nicer URLs.
00181     # Strip non alpha numeric characters (except username_allowedchars), keep white space
00182     name = ''.join([c for c in name if c.isalnum() or c.isspace() or c in username_allowedchars])
00183 
00184     # Normalize white space. Each name can contain multiple
00185     # words separated with only one space.
00186     name = ' '.join(name.split())
00187 
00188     return name
00189 
00190 
00191 def isValidName(request, name):
00192     """ Validate user name
00193 
00194     @param name: user name, unicode
00195     """
00196     normalized = normalizeName(name)
00197     return (name == normalized) and not wikiutil.isGroupPage(name, request.cfg)
00198 
00199 
00200 def encodeList(items):
00201     """ Encode list of items in user data file
00202 
00203     Items are separated by '\t' characters.
00204 
00205     @param items: list unicode strings
00206     @rtype: unicode
00207     @return: list encoded as unicode
00208     """
00209     line = []
00210     for item in items:
00211         item = item.strip()
00212         if not item:
00213             continue
00214         line.append(item)
00215 
00216     line = '\t'.join(line)
00217     return line
00218 
00219 def decodeList(line):
00220     """ Decode list of items from user data file
00221 
00222     @param line: line containing list of items, encoded with encodeList
00223     @rtype: list of unicode strings
00224     @return: list of items in encoded in line
00225     """
00226     items = []
00227     for item in line.split('\t'):
00228         item = item.strip()
00229         if not item:
00230             continue
00231         items.append(item)
00232     return items
00233 
00234 def encodeDict(items):
00235     """ Encode dict of items in user data file
00236 
00237     Items are separated by '\t' characters.
00238     Each item is key:value.
00239 
00240     @param items: dict of unicode:unicode
00241     @rtype: unicode
00242     @return: dict encoded as unicode
00243     """
00244     line = []
00245     for key, value in items.items():
00246         item = u'%s:%s' % (key, value)
00247         line.append(item)
00248     line = '\t'.join(line)
00249     return line
00250 
00251 def decodeDict(line):
00252     """ Decode dict of key:value pairs from user data file
00253 
00254     @param line: line containing a dict, encoded with encodeDict
00255     @rtype: dict
00256     @return: dict  unicode:unicode items
00257     """
00258     items = {}
00259     for item in line.split('\t'):
00260         item = item.strip()
00261         if not item:
00262             continue
00263         key, value = item.split(':', 1)
00264         items[key] = value
00265     return items
00266 
00267 
00268 class User:
00269     """ A MoinMoin User """
00270 
00271     def __init__(self, request, id=None, name="", password=None, auth_username="", **kw):
00272         """ Initialize User object
00273 
00274         TODO: when this gets refactored, use "uid" not builtin "id"
00275 
00276         @param request: the request object
00277         @param id: (optional) user ID
00278         @param name: (optional) user name
00279         @param password: (optional) user password (unicode)
00280         @param auth_username: (optional) already authenticated user name
00281                               (e.g. when using http basic auth) (unicode)
00282         @keyword auth_method: method that was used for authentication,
00283                               default: 'internal'
00284         @keyword auth_attribs: tuple of user object attribute names that are
00285                                determined by auth method and should not be
00286                                changeable by preferences, default: ().
00287                                First tuple element was used for authentication.
00288         """
00289         self._cfg = request.cfg
00290         self.valid = 0
00291         self.id = id
00292         self.auth_username = auth_username
00293         self.auth_method = kw.get('auth_method', 'internal')
00294         self.auth_attribs = kw.get('auth_attribs', ())
00295         self.bookmarks = {} # interwikiname: bookmark
00296 
00297         # create some vars automatically
00298         self.__dict__.update(self._cfg.user_form_defaults)
00299 
00300         if name:
00301             self.name = name
00302         elif auth_username: # this is needed for user autocreate
00303             self.name = auth_username
00304 
00305         # create checkbox fields (with default 0)
00306         for key, label in self._cfg.user_checkbox_fields:
00307             setattr(self, key, self._cfg.user_checkbox_defaults.get(key, 0))
00308 
00309         self.recoverpass_key = ""
00310 
00311         if password:
00312             self.enc_password = encodePassword(password)
00313 
00314         #self.edit_cols = 80
00315         self.tz_offset = int(float(self._cfg.tz_offset) * 3600)
00316         self.language = ""
00317         self.real_language = "" # In case user uses "Browser setting". For language-statistics
00318         self._stored = False
00319         self.date_fmt = ""
00320         self.datetime_fmt = ""
00321         self.quicklinks = self._cfg.quicklinks_default
00322         self.subscribed_pages = self._cfg.subscribed_pages_default
00323         self.email_subscribed_events = self._cfg.email_subscribed_events_default
00324         self.jabber_subscribed_events = self._cfg.jabber_subscribed_events_default
00325         self.theme_name = self._cfg.theme_default
00326         self.editor_default = self._cfg.editor_default
00327         self.editor_ui = self._cfg.editor_ui
00328         self.last_saved = str(time.time())
00329 
00330         # attrs not saved to profile
00331         self._request = request
00332 
00333         # we got an already authenticated username:
00334         check_password = None
00335         if not self.id and self.auth_username:
00336             self.id = getUserId(request, self.auth_username)
00337             if not password is None:
00338                 check_password = password
00339         if self.id:
00340             self.load_from_id(check_password)
00341         elif self.name:
00342             self.id = getUserId(self._request, self.name)
00343             if self.id:
00344                 # no password given should fail
00345                 self.load_from_id(password or u'')
00346         # Still no ID - make new user
00347         if not self.id:
00348             self.id = self.make_id()
00349             if password is not None:
00350                 self.enc_password = encodePassword(password)
00351 
00352         # "may" so we can say "if user.may.read(pagename):"
00353         if self._cfg.SecurityPolicy:
00354             self.may = self._cfg.SecurityPolicy(self)
00355         else:
00356             from MoinMoin.security import Default
00357             self.may = Default(self)
00358 
00359         if self.language and not self.language in i18n.wikiLanguages():
00360             self.language = 'en'
00361 
00362     def __repr__(self):
00363         return "<%s.%s at 0x%x name:%r valid:%r>" % (
00364             self.__class__.__module__, self.__class__.__name__,
00365             id(self), self.name, self.valid)
00366 
00367     def make_id(self):
00368         """ make a new unique user id """
00369         #!!! this should probably be a hash of REMOTE_ADDR, HTTP_USER_AGENT
00370         # and some other things identifying remote users, then we could also
00371         # use it reliably in edit locking
00372         from random import randint
00373         return "%s.%d" % (str(time.time()), randint(0, 65535))
00374 
00375     def create_or_update(self, changed=False):
00376         """ Create or update a user profile
00377 
00378         @param changed: bool, set this to True if you updated the user profile values
00379         """
00380         if not self.valid and not self.disabled or changed: # do we need to save/update?
00381             self.save() # yes, create/update user profile
00382 
00383     def __filename(self):
00384         """ Get filename of the user's file on disk
00385 
00386         @rtype: string
00387         @return: full path and filename of user account file
00388         """
00389         return os.path.join(self._cfg.user_dir, self.id or "...NONE...")
00390 
00391     def exists(self):
00392         """ Do we have a user account for this user?
00393 
00394         @rtype: bool
00395         @return: true, if we have a user account
00396         """
00397         return os.path.exists(self.__filename())
00398 
00399     def load_from_id(self, password=None):
00400         """ Load user account data from disk.
00401 
00402         Can only load user data if the id number is already known.
00403 
00404         This loads all member variables, except "id" and "valid" and
00405         those starting with an underscore.
00406 
00407         @param password: If not None, then the given password must match the
00408                          password in the user account file.
00409         """
00410         if not self.exists():
00411             return
00412 
00413         data = codecs.open(self.__filename(), "r", config.charset).readlines()
00414         user_data = {'enc_password': ''}
00415         for line in data:
00416             if line[0] == '#':
00417                 continue
00418 
00419             try:
00420                 key, val = line.strip().split('=', 1)
00421                 if key not in self._cfg.user_transient_fields and key[0] != '_':
00422                     # Decode list values
00423                     if key.endswith('[]'):
00424                         key = key[:-2]
00425                         val = decodeList(val)
00426                     # Decode dict values
00427                     elif key.endswith('{}'):
00428                         key = key[:-2]
00429                         val = decodeDict(val)
00430                     # for compatibility reading old files, keep these explicit
00431                     # we will store them with [] appended
00432                     elif key in ['quicklinks', 'subscribed_pages', 'subscribed_events']:
00433                         val = decodeList(val)
00434                     user_data[key] = val
00435             except ValueError:
00436                 pass
00437 
00438         # Validate data from user file. In case we need to change some
00439         # values, we set 'changed' flag, and later save the user data.
00440         changed = 0
00441 
00442         if password is not None:
00443             # Check for a valid password, possibly changing storage
00444             valid, changed = self._validatePassword(user_data, password)
00445             if not valid:
00446                 return
00447 
00448         # Remove ignored checkbox values from user data
00449         for key, label in self._cfg.user_checkbox_fields:
00450             if key in user_data and key in self._cfg.user_checkbox_disable:
00451                 del user_data[key]
00452 
00453         # Copy user data into user object
00454         for key, val in user_data.items():
00455             vars(self)[key] = val
00456 
00457         self.tz_offset = int(self.tz_offset)
00458 
00459         # Remove old unsupported attributes from user data file.
00460         remove_attributes = ['passwd', 'show_emoticons']
00461         for attr in remove_attributes:
00462             if hasattr(self, attr):
00463                 delattr(self, attr)
00464                 changed = 1
00465 
00466         # make sure checkboxes are boolean
00467         for key, label in self._cfg.user_checkbox_fields:
00468             try:
00469                 setattr(self, key, int(getattr(self, key)))
00470             except ValueError:
00471                 setattr(self, key, 0)
00472 
00473         # convert (old) hourly format to seconds
00474         if -24 <= self.tz_offset and self.tz_offset <= 24:
00475             self.tz_offset = self.tz_offset * 3600
00476 
00477         if not self.disabled:
00478             self.valid = 1
00479 
00480         # Mark this user as stored so saves don't send
00481         # the "user created" event
00482         self._stored = True
00483 
00484         # If user data has been changed, save fixed user data.
00485         if changed:
00486             self.save()
00487 
00488     def _validatePassword(self, data, password):
00489         """
00490         Check user password.
00491 
00492         This is a private method and should not be used by clients.
00493 
00494         @param data: dict with user data (from storage)
00495         @param password: password to verify [unicode]
00496         @rtype: 2 tuple (bool, bool)
00497         @return: password is valid, enc_password changed
00498         """
00499         epwd = data['enc_password']
00500 
00501         # If we have no password set, we don't accept login with username
00502         if not epwd:
00503             return False, False
00504 
00505         # require non empty password
00506         if not password:
00507             return False, False
00508 
00509         if epwd[:5] == '{SHA}':
00510             enc = '{SHA}' + base64.encodestring(hash_new('sha1', password.encode('utf-8')).digest()).rstrip()
00511             if epwd == enc:
00512                 data['enc_password'] = encodePassword(password) # upgrade to SSHA
00513                 return True, True
00514             return False, False
00515 
00516         if epwd[:6] == '{SSHA}':
00517             data = base64.decodestring(epwd[6:])
00518             salt = data[20:]
00519             hash = hash_new('sha1', password.encode('utf-8'))
00520             hash.update(salt)
00521             return hash.digest() == data[:20], False
00522 
00523         # No encoded password match, this must be wrong password
00524         return False, False
00525 
00526     def persistent_items(self):
00527         """ items we want to store into the user profile """
00528         return [(key, value) for key, value in vars(self).items()
00529                     if key not in self._cfg.user_transient_fields and key[0] != '_']
00530 
00531     def save(self):
00532         """ Save user account data to user account file on disk.
00533 
00534         This saves all member variables, except "id" and "valid" and
00535         those starting with an underscore.
00536         """
00537         if not self.id:
00538             return
00539 
00540         user_dir = self._cfg.user_dir
00541         if not os.path.exists(user_dir):
00542             os.makedirs(user_dir)
00543 
00544         self.last_saved = str(time.time())
00545 
00546         # !!! should write to a temp file here to avoid race conditions,
00547         # or even better, use locking
00548 
00549         data = codecs.open(self.__filename(), "w", config.charset)
00550         data.write("# Data saved '%s' for id '%s'\n" % (
00551             time.strftime(self._cfg.datetime_fmt, time.localtime(time.time())),
00552             self.id))
00553         attrs = self.persistent_items()
00554         attrs.sort()
00555         for key, value in attrs:
00556             # Encode list values
00557             if isinstance(value, list):
00558                 key += '[]'
00559                 value = encodeList(value)
00560             # Encode dict values
00561             elif isinstance(value, dict):
00562                 key += '{}'
00563                 value = encodeDict(value)
00564             line = u"%s=%s\n" % (key, unicode(value))
00565             data.write(line)
00566         data.close()
00567 
00568         arena = 'user'
00569         key = 'name2id'
00570         caching.CacheEntry(self._request, arena, key, scope='wiki').remove()
00571         try:
00572             del self._request.cfg.cache.name2id
00573         except:
00574             pass
00575         key = 'openid2id'
00576         caching.CacheEntry(self._request, arena, key, scope='wiki').remove()
00577         try:
00578             del self._request.cfg.cache.openid2id
00579         except:
00580             pass
00581 
00582         if not self.disabled:
00583             self.valid = 1
00584 
00585         if not self._stored:
00586             self._stored = True
00587             event = events.UserCreatedEvent(self._request, self)
00588             events.send_event(event)
00589 
00590     # -----------------------------------------------------------------
00591     # Time and date formatting
00592 
00593     def getTime(self, tm):
00594         """ Get time in user's timezone.
00595 
00596         @param tm: time (UTC UNIX timestamp)
00597         @rtype: int
00598         @return: tm tuple adjusted for user's timezone
00599         """
00600         return timefuncs.tmtuple(tm + self.tz_offset)
00601 
00602 
00603     def getFormattedDate(self, tm):
00604         """ Get formatted date adjusted for user's timezone.
00605 
00606         @param tm: time (UTC UNIX timestamp)
00607         @rtype: string
00608         @return: formatted date, see cfg.date_fmt
00609         """
00610         date_fmt = self.date_fmt or self._cfg.date_fmt
00611         return time.strftime(date_fmt, self.getTime(tm))
00612 
00613 
00614     def getFormattedDateTime(self, tm):
00615         """ Get formatted date and time adjusted for user's timezone.
00616 
00617         @param tm: time (UTC UNIX timestamp)
00618         @rtype: string
00619         @return: formatted date and time, see cfg.datetime_fmt
00620         """
00621         datetime_fmt = self.datetime_fmt or self._cfg.datetime_fmt
00622         return time.strftime(datetime_fmt, self.getTime(tm))
00623 
00624     # -----------------------------------------------------------------
00625     # Bookmark
00626 
00627     def setBookmark(self, tm):
00628         """ Set bookmark timestamp.
00629 
00630         @param tm: timestamp
00631         """
00632         if self.valid:
00633             interwikiname = self._cfg.interwikiname or u''
00634             bookmark = unicode(tm)
00635             self.bookmarks[interwikiname] = bookmark
00636             self.save()
00637 
00638     def getBookmark(self):
00639         """ Get bookmark timestamp.
00640 
00641         @rtype: int
00642         @return: bookmark timestamp or None
00643         """
00644         bm = None
00645         interwikiname = self._cfg.interwikiname or u''
00646         if self.valid:
00647             try:
00648                 bm = int(self.bookmarks[interwikiname])
00649             except (ValueError, KeyError):
00650                 pass
00651         return bm
00652 
00653     def delBookmark(self):
00654         """ Removes bookmark timestamp.
00655 
00656         @rtype: int
00657         @return: 0 on success, 1 on failure
00658         """
00659         interwikiname = self._cfg.interwikiname or u''
00660         if self.valid:
00661             try:
00662                 del self.bookmarks[interwikiname]
00663             except KeyError:
00664                 return 1
00665             self.save()
00666             return 0
00667         return 1
00668 
00669     # -----------------------------------------------------------------
00670     # Subscribe
00671 
00672     def getSubscriptionList(self):
00673         """ Get list of pages this user has subscribed to
00674 
00675         @rtype: list
00676         @return: pages this user has subscribed to
00677         """
00678         return self.subscribed_pages
00679 
00680     def isSubscribedTo(self, pagelist):
00681         """ Check if user subscription matches any page in pagelist.
00682 
00683         The subscription list may contain page names or interwiki page
00684         names. e.g 'Page Name' or 'WikiName:Page_Name'
00685 
00686         TODO: check if it's fast enough when getting called for many
00687               users from page.getSubscribersList()
00688 
00689         @param pagelist: list of pages to check for subscription
00690         @rtype: bool
00691         @return: if user is subscribed any page in pagelist
00692         """
00693         if not self.valid:
00694             return False
00695 
00696         import re
00697         # Create a new list with both names and interwiki names.
00698         pages = pagelist[:]
00699         if self._cfg.interwikiname:
00700             pages += [self._interWikiName(pagename) for pagename in pagelist]
00701         # Create text for regular expression search
00702         text = '\n'.join(pages)
00703 
00704         for pattern in self.getSubscriptionList():
00705             # Try simple match first
00706             if pattern in pages:
00707                 return True
00708             # Try regular expression search, skipping bad patterns
00709             try:
00710                 pattern = re.compile(r'^%s$' % pattern, re.M)
00711             except re.error:
00712                 continue
00713             if pattern.search(text):
00714                 return True
00715 
00716         return False
00717 
00718     def subscribe(self, pagename):
00719         """ Subscribe to a wiki page.
00720 
00721         To enable shared farm users, if the wiki has an interwiki name,
00722         page names are saved as interwiki names.
00723 
00724         @param pagename: name of the page to subscribe
00725         @type pagename: unicode
00726         @rtype: bool
00727         @return: if page was subscribed
00728         """
00729         if self._cfg.interwikiname:
00730             pagename = self._interWikiName(pagename)
00731 
00732         if pagename not in self.subscribed_pages:
00733             self.subscribed_pages.append(pagename)
00734             self.save()
00735 
00736             # Send a notification
00737             from MoinMoin.events import SubscribedToPageEvent, send_event
00738             e = SubscribedToPageEvent(self._request, pagename, self.name)
00739             send_event(e)
00740             return True
00741 
00742         return False
00743 
00744     def unsubscribe(self, pagename):
00745         """ Unsubscribe a wiki page.
00746 
00747         Try to unsubscribe by removing non-interwiki name (leftover
00748         from old use files) and interwiki name from the subscription
00749         list.
00750 
00751         Its possible that the user will be subscribed to a page by more
00752         then one pattern. It can be both pagename and interwiki name,
00753         or few patterns that all of them match the page. Therefore, we
00754         must check if the user is still subscribed to the page after we
00755         try to remove names from the list.
00756 
00757         @param pagename: name of the page to subscribe
00758         @type pagename: unicode
00759         @rtype: bool
00760         @return: if unsubscrieb was successful. If the user has a
00761             regular expression that match, it will always fail.
00762         """
00763         changed = False
00764         if pagename in self.subscribed_pages:
00765             self.subscribed_pages.remove(pagename)
00766             changed = True
00767 
00768         interWikiName = self._interWikiName(pagename)
00769         if interWikiName and interWikiName in self.subscribed_pages:
00770             self.subscribed_pages.remove(interWikiName)
00771             changed = True
00772 
00773         if changed:
00774             self.save()
00775         return not self.isSubscribedTo([pagename])
00776 
00777     # -----------------------------------------------------------------
00778     # Quicklinks
00779 
00780     def getQuickLinks(self):
00781         """ Get list of pages this user wants in the navibar
00782 
00783         @rtype: list
00784         @return: quicklinks from user account
00785         """
00786         return self.quicklinks
00787 
00788     def isQuickLinkedTo(self, pagelist):
00789         """ Check if user quicklink matches any page in pagelist.
00790 
00791         @param pagelist: list of pages to check for quicklinks
00792         @rtype: bool
00793         @return: if user has quicklinked any page in pagelist
00794         """
00795         if not self.valid:
00796             return False
00797 
00798         for pagename in pagelist:
00799             if pagename in self.quicklinks:
00800                 return True
00801             interWikiName = self._interWikiName(pagename)
00802             if interWikiName and interWikiName in self.quicklinks:
00803                 return True
00804 
00805         return False
00806 
00807     def addQuicklink(self, pagename):
00808         """ Adds a page to the user quicklinks
00809 
00810         If the wiki has an interwiki name, all links are saved as
00811         interwiki names. If not, as simple page name.
00812 
00813         @param pagename: page name
00814         @type pagename: unicode
00815         @rtype: bool
00816         @return: if pagename was added
00817         """
00818         changed = False
00819         interWikiName = self._interWikiName(pagename)
00820         if interWikiName:
00821             if pagename in self.quicklinks:
00822                 self.quicklinks.remove(pagename)
00823                 changed = True
00824             if interWikiName not in self.quicklinks:
00825                 self.quicklinks.append(interWikiName)
00826                 changed = True
00827         else:
00828             if pagename not in self.quicklinks:
00829                 self.quicklinks.append(pagename)
00830                 changed = True
00831 
00832         if changed:
00833             self.save()
00834         return changed
00835 
00836     def removeQuicklink(self, pagename):
00837         """ Remove a page from user quicklinks
00838 
00839         Remove both interwiki and simple name from quicklinks.
00840 
00841         @param pagename: page name
00842         @type pagename: unicode
00843         @rtype: bool
00844         @return: if pagename was removed
00845         """
00846         changed = False
00847         interWikiName = self._interWikiName(pagename)
00848         if interWikiName and interWikiName in self.quicklinks:
00849             self.quicklinks.remove(interWikiName)
00850             changed = True
00851         if pagename in self.quicklinks:
00852             self.quicklinks.remove(pagename)
00853             changed = True
00854 
00855         if changed:
00856             self.save()
00857         return changed
00858 
00859     def _interWikiName(self, pagename):
00860         """ Return the inter wiki name of a page name
00861 
00862         @param pagename: page name
00863         @type pagename: unicode
00864         """
00865         if not self._cfg.interwikiname:
00866             return None
00867 
00868         return "%s:%s" % (self._cfg.interwikiname, pagename)
00869 
00870     # -----------------------------------------------------------------
00871     # Trail
00872 
00873     def _wantTrail(self):
00874         return (not self.valid and self._request.cfg.cookie_lifetime[0]  # anon sessions enabled
00875                 or self.valid and (self.show_page_trail or self.remember_last_visit))  # logged-in session
00876 
00877     def addTrail(self, page):
00878         """ Add page to trail.
00879 
00880         @param page: the page (object) to add to the trail
00881         """
00882         if self._wantTrail():
00883             pagename = page.page_name
00884             # Add only existing pages that the user may read
00885             if not (page.exists() and self._request.user.may.read(pagename)):
00886                 return
00887 
00888             # Save interwiki links internally
00889             if self._cfg.interwikiname:
00890                 pagename = self._interWikiName(pagename)
00891 
00892             trail = self._request.session.get('trail', [])
00893 
00894             # Don't append tail to trail ;)
00895             if trail and trail[-1] == pagename:
00896                 return
00897 
00898             # Append new page, limiting the length
00899             trail = [p for p in trail if p != pagename]
00900             pagename_stripped = pagename.strip()
00901             if pagename_stripped:
00902                 trail.append(pagename_stripped)
00903             self._request.session['trail'] = trail[-(self._cfg.trail_size-1):]
00904 
00905     def getTrail(self):
00906         """ Return list of recently visited pages.
00907 
00908         @rtype: list
00909         @return: pages in trail
00910         """
00911         if self._wantTrail():
00912             trail = self._request.session.get('trail', [])
00913         else:
00914             trail = []
00915         return trail
00916 
00917     # -----------------------------------------------------------------
00918     # Other
00919 
00920     def isCurrentUser(self):
00921         """ Check if this user object is the user doing the current request """
00922         return self._request.user.name == self.name
00923 
00924     def isSuperUser(self):
00925         """ Check if this user is superuser """
00926         if not self.valid:
00927             return False
00928         request = self._request
00929         if request.cfg.DesktopEdition and request.remote_addr == '127.0.0.1':
00930             # the DesktopEdition gives any local user superuser powers
00931             return True
00932         superusers = request.cfg.superuser
00933         assert isinstance(superusers, (list, tuple))
00934         return self.name and self.name in superusers
00935 
00936     def host(self):
00937         """ Return user host """
00938         _ = self._request.getText
00939         host = self.isCurrentUser() and self._cfg.show_hosts and self._request.remote_addr
00940         return host or _("<unknown>")
00941 
00942     def wikiHomeLink(self):
00943         """ Return wiki markup usable as a link to the user homepage,
00944             it doesn't matter whether it already exists or not.
00945         """
00946         wikiname, pagename = wikiutil.getInterwikiHomePage(self._request, self.name)
00947         if wikiname == 'Self':
00948             if wikiutil.isStrictWikiname(self.name):
00949                 markup = pagename
00950             else:
00951                 markup = '[[%s]]' % pagename
00952         else:
00953             markup = '[[%s:%s]]' % (wikiname, pagename)
00954         return markup
00955 
00956     def signature(self):
00957         """ Return user signature using wiki markup
00958 
00959         Users sign with a link to their homepage.
00960         Visitors return their host address.
00961 
00962         TODO: The signature use wiki format only, for example, it will
00963         not create a link when using rst format. It will also break if
00964         we change wiki syntax.
00965         """
00966         if self.name:
00967             return self.wikiHomeLink()
00968         else:
00969             return self.host()
00970 
00971     def generate_recovery_token(self):
00972         key = random_string(64, "abcdefghijklmnopqrstuvwxyz0123456789")
00973         msg = str(int(time.time()))
00974         h = hmac_new(key, msg).hexdigest()
00975         self.recoverpass_key = key
00976         self.save()
00977         return msg + '-' + h
00978 
00979     def apply_recovery_token(self, tok, newpass):
00980         parts = tok.split('-')
00981         if len(parts) != 2:
00982             return False
00983         try:
00984             stamp = int(parts[0])
00985         except ValueError:
00986             return False
00987         # only allow it to be valid for twelve hours
00988         if stamp + 12*60*60 < time.time():
00989             return False
00990         # check hmac
00991         # key must be of type string
00992         h = hmac_new(str(self.recoverpass_key), str(stamp)).hexdigest()
00993         if h != parts[1]:
00994             return False
00995         self.recoverpass_key = ""
00996         self.enc_password = encodePassword(newpass)
00997         self.save()
00998         return True
00999 
01000     def mailAccountData(self, cleartext_passwd=None):
01001         """ Mail a user who forgot his password a message enabling
01002             him to login again.
01003         """
01004         from MoinMoin.mail import sendmail
01005         from MoinMoin.wikiutil import getLocalizedPage
01006         _ = self._request.getText
01007 
01008         tok = self.generate_recovery_token()
01009 
01010         text = '\n' + _("""\
01011 Login Name: %s
01012 
01013 Password recovery token: %s
01014 
01015 Password reset URL: %s/?action=recoverpass&name=%s&token=%s
01016 """) % (
01017                         self.name,
01018                         tok,
01019                         self._request.script_root,
01020                         url_quote_plus(self.name),
01021                         tok, )
01022 
01023         text = _("""\
01024 Somebody has requested to email you a password recovery token.
01025 
01026 If you lost your password, please go to the password reset URL below or
01027 go to the password recovery page again and enter your username and the
01028 recovery token.
01029 """) + text
01030 
01031 
01032         subject = _('[%(sitename)s] Your wiki account data',
01033                 ) % {'sitename': self._cfg.sitename or "Wiki"}
01034         mailok, msg = sendmail.sendmail(self._request, [self.email], subject,
01035                                     text, mail_from=self._cfg.mail_from)
01036         return mailok, msg
01037