Attachment 'user_moin1-5-4-1_patched.py'
Download 1 # -*- coding: iso-8859-1 -*-
2 """
3 MoinMoin - User Accounts
4
5 @copyright: 2000-2004 by Jürgen Hermann <jh@web.de>
6 @license: GNU GPL, see COPYING for details.
7 """
8
9 import os, time, sha, codecs
10
11 try:
12 import cPickle as pickle
13 except ImportError:
14 import pickle
15
16 # Set pickle protocol, see http://docs.python.org/lib/node64.html
17 PICKLE_PROTOCOL = pickle.HIGHEST_PROTOCOL
18
19 from MoinMoin import config, caching, wikiutil
20 from MoinMoin.util import filesys, timefuncs
21
22
23 def getUserList(request):
24 """ Get a list of all (numerical) user IDs.
25
26 @param request: current request
27 @rtype: list
28 @return: all user IDs
29 """
30 import re, dircache
31 user_re = re.compile(r'^\d+\.\d+(\.\d+)?$')
32 files = dircache.listdir(request.cfg.user_dir)
33 userlist = [f for f in files if user_re.match(f)]
34 return userlist
35
36
37 def getUserId(request, searchName):
38 """
39 Get the user ID for a specific user NAME.
40
41 @param searchName: the user name to look up
42 @rtype: string
43 @return: the corresponding user ID or None
44 """
45 if not searchName:
46 return None
47 cfg = request.cfg
48 try:
49 _name2id = cfg._name2id
50 except AttributeError:
51 arena = 'user'
52 key = 'name2id'
53 cache = caching.CacheEntry(request, arena, key)
54 try:
55 _name2id = pickle.loads(cache.content())
56 except (pickle.UnpicklingError, IOError, EOFError, ValueError):
57 _name2id = {}
58 cfg._name2id = _name2id
59 id = _name2id.get(searchName, None)
60 if id is None:
61 for userid in getUserList(request):
62 name = User(request, id=userid).name
63 _name2id[name] = userid
64 cfg._name2id = _name2id
65 arena = 'user'
66 key = 'name2id'
67 cache = caching.CacheEntry(request, arena, key)
68 cache.update(pickle.dumps(_name2id, PICKLE_PROTOCOL))
69 id = _name2id.get(searchName, None)
70 return id
71
72
73 def getUserIdentification(request, username=None):
74 """
75 Return user name or IP or '<unknown>' indicator.
76
77 @param request: the request object
78 @param username: (optional) user name
79 @rtype: string
80 @return: user name or IP or unknown indicator
81 """
82 _ = request.getText
83
84 if username is None:
85 username = request.user.name
86
87 return username or (request.cfg.show_hosts and request.remote_addr) or _("<unknown>")
88
89
90 def encodePassword(pwd, charset='utf-8'):
91 """ Encode a cleartext password
92
93 Compatible to Apache htpasswd SHA encoding.
94
95 When using different encoding than 'utf-8', the encoding might fail
96 and raise UnicodeError.
97
98 @param pwd: the cleartext password, (unicode)
99 @param charset: charset used to encode password, used only for
100 compatibility with old passwords generated on moin-1.2.
101 @rtype: string
102 @return: the password in apache htpasswd compatible SHA-encoding,
103 or None
104 """
105 import base64
106
107 # Might raise UnicodeError, but we can't do anything about it here,
108 # so let the caller handle it.
109 pwd = pwd.encode(charset)
110
111 pwd = sha.new(pwd).digest()
112 pwd = '{SHA}' + base64.encodestring(pwd).rstrip()
113 return pwd
114
115
116 def normalizeName(name):
117 """ Make normalized user name
118
119 Prevent impersonating another user with names containing leading,
120 trailing or multiple whitespace, or using invisible unicode
121 characters.
122
123 Prevent creating user page as sub page, because '/' is not allowed
124 in user names.
125
126 Prevent using ':' and ',' which are reserved by acl.
127
128 @param name: user name, unicode
129 @rtype: unicode
130 @return: user name that can be used in acl lines
131 """
132 name = name.replace('_', ' ') # we treat _ as a blank
133 username_allowedchars = "'@.-" # ' for names like O'Brian or email addresses.
134 # "," and ":" must not be allowed (ACL delimiters).
135 # Strip non alpha numeric characters (except username_allowedchars), keep white space
136 name = ''.join([c for c in name if c.isalnum() or c.isspace() or c in username_allowedchars])
137
138 # Normalize white space. Each name can contain multiple
139 # words separated with only one space.
140 name = ' '.join(name.split())
141
142 return name
143
144
145 def isValidName(request, name):
146 """ Validate user name
147
148 @param name: user name, unicode
149 """
150 normalized = normalizeName(name)
151 name = name.replace('_', ' ') # we treat _ as a blank
152 return (name == normalized) and not wikiutil.isGroupPage(request, name)
153
154
155 def encodeList(items):
156 """ Encode list of items in user data file
157
158 Items are separated by '\t' characters.
159
160 @param items: list unicode strings
161 @rtype: unicode
162 @return: list encoded as unicode
163 """
164 line = []
165 for item in items:
166 item = item.strip()
167 if not item:
168 continue
169 line.append(item)
170
171 line = '\t'.join(line)
172 return line
173
174
175 def decodeList(line):
176 """ Decode list of items from user data file
177
178 @param line: line containing list of items, encoded with encodeList
179 @rtype: list of unicode strings
180 @return: list of items in encoded in line
181 """
182 items = []
183 for item in line.split('\t'):
184 item = item.strip()
185 if not item:
186 continue
187 items.append(item)
188 return items
189
190
191 class User:
192 """A MoinMoin User"""
193
194 def __init__(self, request, id=None, name="", password=None, auth_username="", **kw):
195 """ Initialize User object
196
197 @param request: the request object
198 @param id: (optional) user ID
199 @param name: (optional) user name
200 @param password: (optional) user password
201 @param auth_username: (optional) already authenticated user name
202 (e.g. when using http basic auth)
203 @keyword auth_method: method that was used for authentication,
204 default: 'internal'
205 @keyword auth_attribs: tuple of user object attribute names that are
206 determined by auth method and should not be
207 changed by UserPreferences form, default: ().
208 First tuple element was used for authentication.
209 """
210 self._cfg = request.cfg
211 self.valid = 0
212 self.trusted = 0
213 self.id = id
214 self.auth_username = auth_username
215 self.auth_method = kw.get('auth_method', 'internal')
216 self.auth_attribs = kw.get('auth_attribs', ())
217
218 # create some vars automatically
219 for tuple in self._cfg.user_form_fields:
220 key = tuple[0]
221 default = self._cfg.user_form_defaults.get(key, '')
222 setattr(self, key, default)
223
224 if name:
225 self.name = name
226 elif auth_username: # this is needed for user_autocreate
227 self.name = auth_username
228
229 # create checkbox fields (with default 0)
230 for key, label in self._cfg.user_checkbox_fields:
231 setattr(self, key, self._cfg.user_checkbox_defaults.get(key, 0))
232
233 self.enc_password = ""
234 if password:
235 if password.startswith('{SHA}'):
236 self.enc_password = password
237 else:
238 try:
239 self.enc_password = encodePassword(password)
240 except UnicodeError:
241 pass # Should never happen
242
243 #self.edit_cols = 80
244 self.tz_offset = int(float(self._cfg.tz_offset) * 3600)
245 self.language = ""
246 self.date_fmt = ""
247 self.datetime_fmt = ""
248 self.quicklinks = []
249 self.subscribed_pages = []
250 self.theme_name = self._cfg.theme_default
251 self.editor_default = self._cfg.editor_default
252 self.editor_ui = self._cfg.editor_ui
253 self.last_saved = str(time.time())
254
255 # attrs not saved to profile
256 self._request = request
257 self._trail = []
258
259 # we got an already authenticated username:
260 check_pass = 0
261 if not self.id and self.auth_username:
262 self.id = getUserId(request, self.auth_username)
263 if not password is None:
264 check_pass = 1
265 if self.id:
266 self.load_from_id(check_pass)
267 if self.name == self.auth_username:
268 self.trusted = 1
269 elif self.name:
270 self.id = getUserId(self._request, self.name)
271 if self.id:
272 self.load_from_id(1)
273 else:
274 self.id = self.make_id()
275 else:
276 self.id = self.make_id()
277
278 # "may" so we can say "if user.may.read(pagename):"
279 if self._cfg.SecurityPolicy:
280 self.may = self._cfg.SecurityPolicy(self)
281 else:
282 from security import Default
283 self.may = Default(self)
284
285 from MoinMoin.i18n.meta import languages
286 if self.language and not languages.has_key(self.language):
287 self.language = 'en'
288
289 def __repr__(self):
290 return "<%s.%s at 0x%x name:%r id:%s valid:%r>" % (
291 self.__class__.__module__, self.__class__.__name__,
292 id(self), self.name, self.id, self.valid)
293
294 def make_id(self):
295 """ make a new unique user id """
296 #!!! this should probably be a hash of REMOTE_ADDR, HTTP_USER_AGENT
297 # and some other things identifying remote users, then we could also
298 # use it reliably in edit locking
299 from random import randint
300 return "%s.%d" % (str(time.time()), randint(0,65535))
301
302 def create_or_update(self, changed=False):
303 """ Create or update a user profile
304
305 @param changed: bool, set this to True if you updated the user profile values
306 """
307 if self._cfg.user_autocreate:
308 if not self.valid and not self.disabled or changed: # do we need to save/update?
309 self.save() # yes, create/update user profile
310
311 def __filename(self):
312 """ Get filename of the user's file on disk
313
314 @rtype: string
315 @return: full path and filename of user account file
316 """
317 return os.path.join(self._cfg.user_dir, self.id or "...NONE...")
318
319 def __bookmark_filename(self):
320 if self._cfg.interwikiname:
321 return (self.__filename() + "." + self._cfg.interwikiname +
322 ".bookmark")
323 else:
324 return self.__filename() + ".bookmark"
325
326 def exists(self):
327 """ Do we have a user account for this user?
328
329 @rtype: bool
330 @return: true, if we have a user account
331 """
332 return os.path.exists(self.__filename())
333
334 def load_from_id(self, check_pass=0):
335 """ Load user account data from disk.
336
337 Can only load user data if the id number is already known.
338
339 This loads all member variables, except "id" and "valid" and
340 those starting with an underscore.
341
342 @param check_pass: If 1, then self.enc_password must match the
343 password in the user account file.
344 """
345 if not self.exists():
346 return
347
348 data = codecs.open(self.__filename(), "r", config.charset).readlines()
349 user_data = {'enc_password': ''}
350 for line in data:
351 if line[0] == '#':
352 continue
353
354 try:
355 key, val = line.strip().split('=', 1)
356 if key not in self._cfg.user_transient_fields and key[0] != '_':
357 # Decode list values
358 if key in ['quicklinks', 'subscribed_pages']:
359 val = decodeList(val)
360 user_data[key] = val
361 except ValueError:
362 pass
363
364 # Validate data from user file. In case we need to change some
365 # values, we set 'changed' flag, and later save the user data.
366 changed = 0
367
368 if check_pass:
369 # If we have no password set, we don't accept login with username
370 if not user_data['enc_password']:
371 return
372 # Check for a valid password, possibly changing encoding
373 valid, changed = self._validatePassword(user_data)
374 if not valid:
375 return
376 else:
377 self.trusted = 1
378
379 # Remove ignored checkbox values from user data
380 for key, label in self._cfg.user_checkbox_fields:
381 if user_data.has_key(key) and key in self._cfg.user_checkbox_disable:
382 del user_data[key]
383
384 # Copy user data into user object
385 for key, val in user_data.items():
386 vars(self)[key] = val
387
388 self.tz_offset = int(self.tz_offset)
389
390 # Remove old unsupported attributes from user data file.
391 remove_attributes = ['passwd', 'show_emoticons']
392 for attr in remove_attributes:
393 if hasattr(self, attr):
394 delattr(self, attr)
395 changed = 1
396
397 # make sure checkboxes are boolean
398 for key, label in self._cfg.user_checkbox_fields:
399 try:
400 setattr(self, key, int(getattr(self, key)))
401 except ValueError:
402 setattr(self, key, 0)
403
404 # convert (old) hourly format to seconds
405 if -24 <= self.tz_offset and self.tz_offset <= 24:
406 self.tz_offset = self.tz_offset * 3600
407
408 # clear trail
409 self._trail = []
410
411 if not self.disabled:
412 self.valid = 1
413
414 # If user data has been changed, save fixed user data.
415 if changed:
416 self.save()
417
418 def _validatePassword(self, data):
419 """ Try to validate user password
420
421 This is a private method and should not be used by clients.
422
423 In pre 1.3, the wiki used some 8 bit charset. The user password
424 was entered in this 8 bit password and passed to
425 encodePassword. So old passwords can use any of the charset
426 used.
427
428 In 1.3, we use unicode internally, so we encode the password in
429 encodePassword using utf-8.
430
431 When we compare passwords we must compare with same encoding, or
432 the passwords will not match. We don't know what encoding the
433 password on the user file uses. We may ask the wiki admin to put
434 this into the config, but he may be wrong.
435
436 The way chosen is to try to encode and compare passwords using
437 all the encoding that were available on 1.2, until we get a
438 match, which means that the user is valid.
439
440 If we get a match, we replace the user password hash with the
441 utf-8 encoded version, and next time it will match on first try
442 as before. The user password did not change, this change is
443 completely transparent for the user. Only the sha digest will
444 change.
445
446 @param data: dict with user data
447 @rtype: 2 tuple (bool, bool)
448 @return: password is valid, password did change
449 """
450 # First try with default encoded password. Match only non empty
451 # passwords. (require non empty enc_password)
452 if self.enc_password and self.enc_password == data['enc_password']:
453 return True, False
454
455 # Try to match using one of pre 1.3 8 bit charsets
456
457 # Get the clear text password from the form (require non empty
458 # password)
459 password = self._request.form.get('password',[None])[0]
460 if not password:
461 return False, False
462
463 # First get all available pre13 charsets on this system
464 pre13 = ['iso-8859-1', 'iso-8859-2', 'euc-jp', 'gb2312', 'big5',]
465 available = []
466 for charset in pre13:
467 try:
468 encoder = codecs.getencoder(charset)
469 available.append(charset)
470 except LookupError:
471 pass # missing on this system
472
473 # Now try to match the password
474 for charset in available:
475 # Try to encode, failure is expected
476 try:
477 enc_password = encodePassword(password, charset=charset)
478 except UnicodeError:
479 continue
480
481 # And match (require non empty enc_password)
482 if enc_password and enc_password == data['enc_password']:
483 # User password match - replace the user password in the
484 # file with self.password
485 data['enc_password'] = self.enc_password
486 return True, True
487
488 # No encoded password match, this must be wrong password
489 return False, False
490
491 def save(self):
492 """ Save user account data to user account file on disk.
493
494 This saves all member variables, except "id" and "valid" and
495 those starting with an underscore.
496 """
497 if not self.id:
498 return
499
500 user_dir = self._cfg.user_dir
501 filesys.makeDirs(user_dir)
502
503 self.last_saved = str(time.time())
504
505 # !!! should write to a temp file here to avoid race conditions,
506 # or even better, use locking
507
508 data = codecs.open(self.__filename(), "w", config.charset)
509 data.write("# Data saved '%s' for id '%s'\n" % (
510 time.strftime(self._cfg.datetime_fmt, time.localtime(time.time())),
511 self.id))
512 attrs = vars(self).items()
513 attrs.sort()
514 for key, value in attrs:
515 if key not in self._cfg.user_transient_fields and key[0] != '_':
516 # Encode list values
517 if key in ['quicklinks', 'subscribed_pages']:
518 value = encodeList(value)
519 line = u"%s=%s\n" % (key, unicode(value))
520 data.write(line)
521 data.close()
522
523 try:
524 os.chmod(self.__filename(), 0666 & config.umask)
525 except OSError:
526 pass
527
528 if not self.disabled:
529 self.valid = 1
530
531 # -----------------------------------------------------------------
532 # Time and date formatting
533
534 def getTime(self, tm):
535 """ Get time in user's timezone.
536
537 @param tm: time (UTC UNIX timestamp)
538 @rtype: int
539 @return: tm tuple adjusted for user's timezone
540 """
541 return timefuncs.tmtuple(tm + self.tz_offset)
542
543
544 def getFormattedDate(self, tm):
545 """ Get formatted date adjusted for user's timezone.
546
547 @param tm: time (UTC UNIX timestamp)
548 @rtype: string
549 @return: formatted date, see cfg.date_fmt
550 """
551 date_fmt = self.date_fmt or self._cfg.date_fmt
552 return time.strftime(date_fmt, self.getTime(tm))
553
554
555 def getFormattedDateTime(self, tm):
556 """ Get formatted date and time adjusted for user's timezone.
557
558 @param tm: time (UTC UNIX timestamp)
559 @rtype: string
560 @return: formatted date and time, see cfg.datetime_fmt
561 """
562 datetime_fmt = self.datetime_fmt or self._cfg.datetime_fmt
563 return time.strftime(datetime_fmt, self.getTime(tm))
564
565 # -----------------------------------------------------------------
566 # Bookmark
567
568 def setBookmark(self, tm):
569 """ Set bookmark timestamp.
570
571 @param tm: timestamp
572 """
573 if self.valid:
574 bm_fn = self.__bookmark_filename()
575 bmfile = open(bm_fn, "w")
576 bmfile.write(str(tm)+"\n")
577 bmfile.close()
578 try:
579 os.chmod(bm_fn, 0666 & config.umask)
580 except OSError:
581 pass
582
583 def getBookmark(self):
584 """ Get bookmark timestamp.
585
586 @rtype: int
587 @return: bookmark timestamp or None
588 """
589 bm = None
590 bm_fn = self.__bookmark_filename()
591
592 if self.valid and os.path.exists(bm_fn):
593 try:
594 bm = long(open(bm_fn, 'r').readline()) # must be long for py 2.2
595 except (OSError, ValueError):
596 pass
597 return bm
598
599 def delBookmark(self):
600 """ Removes bookmark timestamp.
601
602 @rtype: int
603 @return: 0 on success, 1 on failure
604 """
605 bm_fn = self.__bookmark_filename()
606 if self.valid:
607 if os.path.exists(bm_fn):
608 try:
609 os.unlink(bm_fn)
610 except OSError:
611 return 1
612 return 0
613 return 1
614
615 # -----------------------------------------------------------------
616 # Subscribe
617
618 def getSubscriptionList(self):
619 """ Get list of pages this user has subscribed to
620
621 @rtype: list
622 @return: pages this user has subscribed to
623 """
624 return self.subscribed_pages
625
626 def isSubscribedTo(self, pagelist):
627 """ Check if user subscription matches any page in pagelist.
628
629 The subscription list may contain page names or interwiki page
630 names. e.g 'Page Name' or 'WikiName:Page_Name'
631
632 TODO: check if its fast enough when calling with many users
633 from page.getSubscribersList()
634
635 @param pagelist: list of pages to check for subscription
636 @rtype: bool
637 @return: if user is subscribed any page in pagelist
638 """
639 if not self.valid:
640 return False
641
642 import re
643 # Create a new list with both names and interwiki names.
644 pages = pagelist[:]
645 if self._cfg.interwikiname:
646 pages += [self._interWikiName(pagename) for pagename in pagelist]
647 # Create text for regular expression search
648 text = '\n'.join(pages)
649
650 for pattern in self.getSubscriptionList():
651 # Try simple match first
652 if pattern in pages:
653 return True
654 # Try regular expression search, skipping bad patterns
655 try:
656 pattern = re.compile(r'^%s$' % pattern, re.M)
657 except re.error:
658 continue
659 if pattern.search(text):
660 return True
661
662 return False
663
664 def subscribe(self, pagename):
665 """ Subscribe to a wiki page.
666
667 To enable shared farm users, if the wiki has an interwiki name,
668 page names are saved as interwiki names.
669
670 @param pagename: name of the page to subscribe
671 @type pagename: unicode
672 @rtype: bool
673 @return: if page was subscribed
674 """
675 if self._cfg.interwikiname:
676 pagename = self._interWikiName(pagename)
677
678 if pagename not in self.subscribed_pages:
679 self.subscribed_pages.append(pagename)
680 self.save()
681 return True
682
683 return False
684
685 def unsubscribe(self, pagename):
686 """ Unsubscribe a wiki page.
687
688 Try to unsubscribe by removing non-interwiki name (leftover
689 from old use files) and interwiki name from the subscription
690 list.
691
692 Its possible that the user will be subscribed to a page by more
693 then one pattern. It can be both pagename and interwiki name,
694 or few patterns that all of them match the page. Therefore, we
695 must check if the user is still subscribed to the page after we
696 try to remove names from the list.
697
698 TODO: should we remove non-interwiki subscription? what if the
699 user want to subscribe to the same page in multiple wikis?
700
701 @param pagename: name of the page to subscribe
702 @type pagename: unicode
703 @rtype: bool
704 @return: if unsubscrieb was successful. If the user has a
705 regular expression that match, it will always fail.
706 """
707 changed = False
708 if pagename in self.subscribed_pages:
709 self.subscribed_pages.remove(pagename)
710 changed = True
711
712 interWikiName = self._interWikiName(pagename)
713 if interWikiName and interWikiName in self.subscribed_pages:
714 self.subscribed_pages.remove(interWikiName)
715 changed = True
716
717 if changed:
718 self.save()
719 return not self.isSubscribedTo([pagename])
720
721 # -----------------------------------------------------------------
722 # Quicklinks
723
724 def getQuickLinks(self):
725 """ Get list of pages this user wants in the navibar
726
727 @rtype: list
728 @return: quicklinks from user account
729 """
730 return self.quicklinks
731
732 def isQuickLinkedTo(self, pagelist):
733 """ Check if user quicklink matches any page in pagelist.
734
735 @param pagelist: list of pages to check for quicklinks
736 @rtype: bool
737 @return: if user has quicklinked any page in pagelist
738 """
739 if not self.valid:
740 return False
741
742 for pagename in pagelist:
743 if pagename in self.quicklinks:
744 return True
745 interWikiName = self._interWikiName(pagename)
746 if interWikiName and interWikiName in self.quicklinks:
747 return True
748
749 return False
750
751 def addQuicklink(self, pagename):
752 """ Adds a page to the user quicklinks
753
754 If the wiki has an interwiki name, all links are saved as
755 interwiki names. If not, as simple page name.
756
757 @param pagename: page name
758 @type pagename: unicode
759 @rtype: bool
760 @return: if pagename was added
761 """
762 changed = False
763 interWikiName = self._interWikiName(pagename)
764 if interWikiName:
765 if pagename in self.quicklinks:
766 self.quicklinks.remove(pagename)
767 changed = True
768 if interWikiName not in self.quicklinks:
769 self.quicklinks.append(interWikiName)
770 changed = True
771 else:
772 if pagename not in self.quicklinks:
773 self.quicklinks.append(pagename)
774 changed = True
775
776 if changed:
777 self.save()
778 return changed
779
780 def removeQuicklink(self, pagename):
781 """ Remove a page from user quicklinks
782
783 Remove both interwiki and simple name from quicklinks.
784
785 @param pagename: page name
786 @type pagename: unicode
787 @rtype: bool
788 @return: if pagename was removed
789 """
790 changed = False
791 interWikiName = self._interWikiName(pagename)
792 if interWikiName and interWikiName in self.quicklinks:
793 self.quicklinks.remove(interWikiName)
794 changed = True
795 if pagename in self.quicklinks:
796 self.quicklinks.remove(pagename)
797 changed = True
798
799 if changed:
800 self.save()
801 return changed
802
803 def _interWikiName(self, pagename):
804 """ Return the inter wiki name of a page name
805
806 @param pagename: page name
807 @type pagename: unicode
808 """
809 if not self._cfg.interwikiname:
810 return None
811
812 # Interwiki links must use _ e.g Wiki:Main_Page
813 pagename = pagename.replace(" ", "_")
814 return "%s:%s" % (self._cfg.interwikiname, pagename)
815
816 # -----------------------------------------------------------------
817 # Trail
818
819 def addTrail(self, pagename):
820 """ Add page to trail.
821
822 @param pagename: the page name to add to the trail
823 """
824 # TODO: acquire lock here, so multiple processes don't clobber
825 # each one trail.
826
827 if self.valid and (self.show_page_trail or self.remember_last_visit):
828 # load trail if not known
829 self.getTrail()
830
831 # Add only existing pages that the user may read
832 if self._request:
833 from MoinMoin.Page import Page
834 page = Page(self._request, pagename)
835 if not (page.exists() and
836 self._request.user.may.read(page.page_name)):
837 return
838
839 # Save interwiki links internally
840 if self._cfg.interwikiname:
841 pagename = self._interWikiName(pagename)
842
843 # Don't append tail to trail ;)
844 if self._trail and self._trail[-1] == pagename:
845 return
846
847 # Append new page, limiting the length
848 self._trail = [p for p in self._trail if p != pagename]
849 self._trail = self._trail[-(self._cfg.trail_size-1):]
850 self._trail.append(pagename)
851 self.saveTrail()
852
853 # TODO: release lock here
854
855 def saveTrail(self):
856 """ Save trail file
857
858 Save using one write call, which should be fine in most cases,
859 but will fail in rare cases without real file locking.
860 """
861 data = '\n'.join(self._trail) + '\n'
862 path = self.__filename() + ".trail"
863 try:
864 file = codecs.open(path, "w", config.charset)
865 try:
866 file.write(data)
867 finally:
868 file.close()
869
870 try:
871 os.chmod(path, 0666 & config.umask)
872 except OSError, err:
873 self._request.log("Can't change mode of trail file: %s" %
874 str(err))
875 except (IOError, OSError), err:
876 self._request.log("Can't save trail file: %s" % str(err))
877
878 def getTrail(self):
879 """ Return list of recently visited pages.
880
881 @rtype: list
882 @return: pages in trail
883 """
884 if self.valid and (self.show_page_trail or self.remember_last_visit) \
885 and not self._trail \
886 and os.path.exists(self.__filename() + ".trail"):
887 try:
888 trail = codecs.open(self.__filename() + ".trail", 'r', config.charset).readlines()
889 except (OSError, ValueError):
890 trail = []
891 trail = [t.strip() for t in trail]
892 trail = [t for t in trail if t]
893 self._trail = trail[-self._cfg.trail_size:]
894
895 return self._trail
896
897 # -----------------------------------------------------------------
898 # Other
899
900 def isCurrentUser(self):
901 return self._request.user.name == self.name
902
903 def isSuperUser(self):
904 superusers = self._request.cfg.superuser
905 assert isinstance(superusers, (list, tuple))
906 return self.valid and self.name and self.name in superusers
907
908 def host(self):
909 """ Return user host """
910 _ = self._request.getText
911 host = self.isCurrentUser() and self._cfg.show_hosts and self._request.remote_addr
912 return host or _("<unknown>")
913
914 def signature(self):
915 """ Return user signature using markup
916
917 Users sign with a link to their homepage, or with text if they
918 don't have one. The text may be parsed as a link if it's using
919 CamelCase. Visitors return their host address.
920
921 TODO: The signature use wiki format only, for example, it will
922 not create a link when using rst format. It will also break if
923 we change wiki syntax.
924 """
925 if not self.name:
926 return self.host()
927
928 wikiname, pagename = wikiutil.getInterwikiHomePage(self._request,
929 self.name)
930 if wikiname == 'Self':
931 if not wikiutil.isStrictWikiname(self.name):
932 markup = '["%s"]' % pagename
933 else:
934 markup = pagename
935 else:
936 markup = '%s:%s' % (wikiname, pagename.replace(" ","_"))
937 return markup
938
939 def mailAccountData(self, cleartext_passwd=None, invitation=False):
940 from MoinMoin.util import mail
941 from MoinMoin.wikiutil import getSysPage
942 _ = self._request.getText
943
944 if not self.enc_password: # generate pw if there is none yet
945 from random import randint
946 import base64
947
948 charset = 'utf-8'
949 pwd = "%s%d" % (str(time.time()), randint(0, 65535))
950 pwd = pwd.encode(charset)
951
952 pwd = sha.new(pwd).digest()
953 pwd = '{SHA}%s' % base64.encodestring(pwd).rstrip()
954
955 self.enc_password = pwd
956 self.save()
957
958 if not invitation:
959
960 text = '\n' + _("""\
961 Login Name: %s
962
963 Login Password: %s
964
965 Login URL: %s/%s?action=login
966 """, formatted=False) % (
967 self.name, self.enc_password, self._request.getBaseURL(), getSysPage(self._request, 'UserPreferences').page_name)
968
969 text = _("""\
970 Somebody has requested to submit your account data to this email address.
971
972 If you lost your password, please use the data below and just enter the
973 password AS SHOWN into the wiki's password form field (use copy and paste
974 for that).
975
976 After successfully logging in, it is of course a good idea to set a new and known password.
977 """, formatted=False) + text
978
979 else:
980
981 text = '\n' + _("""\
982 Login Name: %s
983
984 Login Password: %s
985
986 Login URL: %s/%s?action=login
987 """, formatted=False) % (
988 self.name, cleartext_passwd, self._request.getBaseURL(), getSysPage(self._request, 'UserPreferences').page_name)
989
990 text = _("""\
991 Welcome to the %(sitename)s Wiki!
992
993 You are invited to join our wiki community and share your ideas with us.
994 Please use the login name and password below to log in. To ease login, you can
995 also use copy and paste.
996 Please do also make sure that in your browser's settings cookies are enabled
997 for the login url so that authentications does really work and is kept while
998 navigating in the wiki.
999
1000 After successfully logging in, it is of course a good idea to set a new password
1001 of your choice.
1002 """, formatted=False) % {'sitename': self._cfg.sitename or "Wiki"} + text
1003
1004
1005
1006 subject = _('[%(sitename)s] Your wiki account data',
1007 formatted=False) % {'sitename': self._cfg.sitename or "Wiki"}
1008 mailok, msg = mail.sendmail(self._request, [self.email], subject,
1009 text, mail_from=self._cfg.mail_from)
1010 return msg
Attached Files
To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.You are not allowed to attach a file to this page.