comparison roundup/cgi/client.py @ 1477:ed725179953d

Added password reset facility for forgotten passwords. Uses similar mechanism to PyPI.
author Richard Jones <richard@users.sourceforge.net>
date Thu, 27 Feb 2003 05:43:02 +0000
parents 77942e0a12fe
children 2704d8438823
comparison
equal deleted inserted replaced
1476:5a01e90b7dc9 1477:ed725179953d
1 # $Id: client.py,v 1.100 2003-02-26 04:57:49 richard Exp $ 1 # $Id: client.py,v 1.101 2003-02-27 05:43:01 richard Exp $
2 2
3 __doc__ = """ 3 __doc__ = """
4 WWW request handler (also used in the stand-alone server). 4 WWW request handler (also used in the stand-alone server).
5 """ 5 """
6 6
7 import os, os.path, cgi, StringIO, urlparse, re, traceback, mimetypes, urllib 7 import os, os.path, cgi, StringIO, urlparse, re, traceback, mimetypes, urllib
8 import binascii, Cookie, time, random, MimeWriter, smtplib, socket, quopri 8 import binascii, Cookie, time, random, MimeWriter, smtplib, socket, quopri
9 import stat, rfc822 9 import stat, rfc822, string
10 10
11 from roundup import roundupdb, date, hyperdb, password 11 from roundup import roundupdb, date, hyperdb, password
12 from roundup.i18n import _ 12 from roundup.i18n import _
13 from roundup.cgi.templating import Templates, HTMLRequest, NoTemplate 13 from roundup.cgi.templating import Templates, HTMLRequest, NoTemplate
14 from roundup.cgi import cgitb 14 from roundup.cgi import cgitb
15 from roundup.cgi.PageTemplates import PageTemplate 15 from roundup.cgi.PageTemplates import PageTemplate
16 from roundup.rfc2822 import encode_header 16 from roundup.rfc2822 import encode_header
17 from roundup.mailgw import uidFromAddress
17 18
18 class HTTPException(Exception): 19 class HTTPException(Exception):
19 pass 20 pass
20 class Unauthorised(HTTPException): 21 class Unauthorised(HTTPException):
21 pass 22 pass
255 except: 256 except:
256 # everything else 257 # everything else
257 self.write(cgitb.html()) 258 self.write(cgitb.html())
258 259
259 def clean_sessions(self): 260 def clean_sessions(self):
260 '''age sessions, remove when they haven't been used for a week. 261 ''' Age sessions, remove when they haven't been used for a week.
261 Do it only once an hour''' 262
263 Do it only once an hour.
264
265 Note: also cleans One Time Keys, and other "session" based
266 stuff.
267 '''
262 sessions = self.db.sessions 268 sessions = self.db.sessions
263 last_clean = sessions.get('last_clean', 'last_use') or 0 269 last_clean = sessions.get('last_clean', 'last_use') or 0
264 270
265 week = 60*60*24*7 271 week = 60*60*24*7
266 hour = 60*60 272 hour = 60*60
267 now = time.time() 273 now = time.time()
268 if now - last_clean > hour: 274 if now - last_clean > hour:
269 # remove age sessions 275 # remove aged sessions
270 for sessid in sessions.list(): 276 for sessid in sessions.list():
271 interval = now - sessions.get(sessid, 'last_use') 277 interval = now - sessions.get(sessid, 'last_use')
272 if interval > week: 278 if interval > week:
273 sessions.destroy(sessid) 279 sessions.destroy(sessid)
280 # remove aged otks
281 otks = self.db.otks
282 for sessid in otks.list():
283 interval = now - okts.get(sessid, '__time')
284 if interval > week:
285 otk.destroy(sessid)
274 sessions.set('last_clean', last_use=time.time()) 286 sessions.set('last_clean', last_use=time.time())
275 287
276 def determine_user(self): 288 def determine_user(self):
277 ''' Determine who the user is 289 ''' Determine who the user is
278 ''' 290 '''
477 ('edit', 'editItemAction'), 489 ('edit', 'editItemAction'),
478 ('editcsv', 'editCSVAction'), 490 ('editcsv', 'editCSVAction'),
479 ('new', 'newItemAction'), 491 ('new', 'newItemAction'),
480 ('register', 'registerAction'), 492 ('register', 'registerAction'),
481 ('confrego', 'confRegoAction'), 493 ('confrego', 'confRegoAction'),
494 ('passrst', 'passResetAction'),
482 ('login', 'loginAction'), 495 ('login', 'loginAction'),
483 ('logout', 'logout_action'), 496 ('logout', 'logout_action'),
484 ('search', 'searchAction'), 497 ('search', 'searchAction'),
485 ('retire', 'retireAction'), 498 ('retire', 'retireAction'),
486 ('show', 'showAction'), 499 ('show', 'showAction'),
487 ) 500 )
488 def handle_action(self): 501 def handle_action(self):
489 ''' Determine whether there should be an Action called. 502 ''' Determine whether there should be an Action called.
490 503
491 The action is defined by the form variable :action which 504 The action is defined by the form variable :action which
492 identifies the method on this object to call. The four basic 505 identifies the method on this object to call. The actions
493 actions are defined in the "actions" sequence on this class: 506 are defined in the "actions" sequence on this class.
494 "edit" -> self.editItemAction
495 "editcsv" -> self.editCSVAction
496 "new" -> self.newItemAction
497 "register" -> self.registerAction
498 "confrego" -> self.confRegoAction
499 "login" -> self.loginAction
500 "logout" -> self.logout_action
501 "search" -> self.searchAction
502 "retire" -> self.retireAction
503 ''' 507 '''
504 if self.form.has_key(':action'): 508 if self.form.has_key(':action'):
505 action = self.form[':action'].value.lower() 509 action = self.form[':action'].value.lower()
506 elif self.form.has_key('@action'): 510 elif self.form.has_key('@action'):
507 action = self.form['@action'].value.lower() 511 action = self.form['@action'].value.lower()
673 now, self.cookie_path) 677 now, self.cookie_path)
674 678
675 # Let the user know what's going on 679 # Let the user know what's going on
676 self.ok_message.append(_('You are logged out')) 680 self.ok_message.append(_('You are logged out'))
677 681
678 chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' 682 chars = string.letters+string.digits
679 def registerAction(self): 683 def registerAction(self):
680 '''Attempt to create a new user based on the contents of the form 684 '''Attempt to create a new user based on the contents of the form
681 and then set the cookie. 685 and then set the cookie.
682 686
683 return 1 on successful login 687 return 1 on successful login
711 props[propname] = str(value) 715 props[propname] = str(value)
712 elif isinstance(proptype, hyperdb.Interval): 716 elif isinstance(proptype, hyperdb.Interval):
713 props[propname] = str(value) 717 props[propname] = str(value)
714 elif isinstance(proptype, hyperdb.Password): 718 elif isinstance(proptype, hyperdb.Password):
715 props[propname] = str(value) 719 props[propname] = str(value)
720 props['__time'] = time.time()
716 self.db.otks.set(otk, **props) 721 self.db.otks.set(otk, **props)
717 722
723 # send the email
724 tracker_name = self.db.config.TRACKER_NAME
725 subject = 'Complete your registration to %s'%tracker_name
726 body = '''
727 To complete your registration of the user "%(name)s" with %(tracker)s,
728 please visit the following URL:
729
730 %(url)s?@action=confrego&otk=%(otk)s
731 '''%{'name': props['username'], 'tracker': tracker_name, 'url': self.base,
732 'otk': otk}
733 if not self.sendEmail(props['address'], subject, body):
734 return
735
736 # commit changes to the database
737 self.db.commit()
738
739 # redirect to the "you're almost there" page
740 raise Redirect, '%suser?@template=rego_progress'%self.base
741
742 def sendEmail(self, to, subject, content):
718 # send email to the user's email address 743 # send email to the user's email address
719 message = StringIO.StringIO() 744 message = StringIO.StringIO()
720 writer = MimeWriter.MimeWriter(message) 745 writer = MimeWriter.MimeWriter(message)
721 tracker_name = self.db.config.TRACKER_NAME 746 tracker_name = self.db.config.TRACKER_NAME
722 s = 'Complete your registration to %s'%tracker_name 747 writer.addheader('Subject', encode_header(subject))
723 writer.addheader('Subject', encode_header(s)) 748 writer.addheader('To', to)
724 writer.addheader('To', props['address'])
725 writer.addheader('From', roundupdb.straddr((tracker_name, 749 writer.addheader('From', roundupdb.straddr((tracker_name,
726 self.db.config.ADMIN_EMAIL))) 750 self.db.config.ADMIN_EMAIL)))
727 writer.addheader('Date', time.strftime("%a, %d %b %Y %H:%M:%S +0000", 751 writer.addheader('Date', time.strftime("%a, %d %b %Y %H:%M:%S +0000",
728 time.gmtime())) 752 time.gmtime()))
729 # add a uniquely Roundup header to help filtering 753 # add a uniquely Roundup header to help filtering
732 writer.addheader('X-Roundup-Loop', 'hello') 756 writer.addheader('X-Roundup-Loop', 'hello')
733 writer.addheader('Content-Transfer-Encoding', 'quoted-printable') 757 writer.addheader('Content-Transfer-Encoding', 'quoted-printable')
734 body = writer.startbody('text/plain; charset=utf-8') 758 body = writer.startbody('text/plain; charset=utf-8')
735 759
736 # message body, encoded quoted-printable 760 # message body, encoded quoted-printable
737 content = StringIO.StringIO(''' 761 content = StringIO.StringIO(content)
738 To complete your registration of the user "%(name)s" with %(tracker)s,
739 please visit the following URL:
740
741 http://localhost:8001/test/?@action=confrego&otk=%(otk)s
742 '''%{'name': props['username'], 'tracker': tracker_name, 'url': self.base,
743 'otk': otk})
744 quopri.encode(content, body, 0) 762 quopri.encode(content, body, 0)
745 763
746 # now try to send the message 764 # now try to send the message
747 try: 765 try:
748 # send the message as admin so bounces are sent there 766 # send the message as admin so bounces are sent there
749 # instead of to roundup 767 # instead of to roundup
750 smtp = smtplib.SMTP(self.db.config.MAILHOST) 768 smtp = smtplib.SMTP(self.db.config.MAILHOST)
751 smtp.sendmail(self.db.config.ADMIN_EMAIL, [props['address']], 769 smtp.sendmail(self.db.config.ADMIN_EMAIL, [to], message.getvalue())
752 message.getvalue())
753 except socket.error, value: 770 except socket.error, value:
754 self.error_message.append("Error: couldn't send " 771 self.error_message.append("Error: couldn't send email: "
755 "confirmation email: mailhost %s"%value) 772 "mailhost %s"%value)
756 return 773 return 0
757 except smtplib.SMTPException, value: 774 except smtplib.SMTPException, value:
758 self.error_message.append("Error: couldn't send " 775 self.error_message.append("Error: couldn't send email: %s"%value)
759 "confirmation email: %s"%value) 776 return 0
760 return 777 return 1
761
762 # commit changes to the database
763 self.db.commit()
764
765 # redirect to the "you're almost there" page
766 raise Redirect, '%s?:template=rego_step1_done'%self.base
767 778
768 def registerPermission(self, props): 779 def registerPermission(self, props):
769 ''' Determine whether the user has permission to register 780 ''' Determine whether the user has permission to register
770 781
771 Base behaviour is to check the user has "Web Registration". 782 Base behaviour is to check the user has "Web Registration".
803 cl = self.db.user 814 cl = self.db.user
804 # XXX we need to make the "default" page be able to display errors! 815 # XXX we need to make the "default" page be able to display errors!
805 # try: 816 # try:
806 if 1: 817 if 1:
807 props['roles'] = self.instance.config.NEW_WEB_USER_ROLES 818 props['roles'] = self.instance.config.NEW_WEB_USER_ROLES
819 del props['__time']
808 self.userid = cl.create(**props) 820 self.userid = cl.create(**props)
809 # clear the props from the otk database 821 # clear the props from the otk database
810 self.db.otks.destroy(otk) 822 self.db.otks.destroy(otk)
811 self.db.commit() 823 self.db.commit()
812 # except (ValueError, KeyError), message: 824 # except (ValueError, KeyError), message:
830 message = _('You are now registered, welcome!') 842 message = _('You are now registered, welcome!')
831 843
832 # redirect to the item's edit page 844 # redirect to the item's edit page
833 raise Redirect, '%suser%s?@ok_message=%s'%( 845 raise Redirect, '%suser%s?@ok_message=%s'%(
834 self.base, self.userid, urllib.quote(message)) 846 self.base, self.userid, urllib.quote(message))
847
848 def passResetAction(self):
849 ''' Handle password reset requests.
850
851 Presence of either "name" or "address" generate email.
852 Presense of "otk" performs the reset.
853 '''
854 if self.form.has_key('otk'):
855 # pull the rego information out of the otk database
856 otk = self.form['otk'].value
857 uid = self.db.otks.get(otk, 'uid')
858
859 # re-open the database as "admin"
860 if self.user != 'admin':
861 self.opendb('admin')
862
863 # change the password
864 newpw = ''.join([random.choice(self.chars) for x in range(8)])
865
866 cl = self.db.user
867 # XXX we need to make the "default" page be able to display errors!
868 # try:
869 if 1:
870 # set the password
871 cl.set(uid, password=password.Password(newpw))
872 # clear the props from the otk database
873 self.db.otks.destroy(otk)
874 self.db.commit()
875 # except (ValueError, KeyError), message:
876 # self.error_message.append(str(message))
877 # return
878
879 # user info
880 address = self.db.user.get(uid, 'address')
881 name = self.db.user.get(uid, 'username')
882
883 # send the email
884 tracker_name = self.db.config.TRACKER_NAME
885 subject = 'Password reset for %s'%tracker_name
886 body = '''
887 The password has been reset for username "%(name)s".
888
889 Your password is now: %(password)s
890 '''%{'name': name, 'password': newpw}
891 if not self.sendEmail(address, subject, body):
892 return
893
894 self.ok_message.append('Password reset and email sent to %s'%address)
895 return
896
897 # no OTK, so now figure the user
898 if self.form.has_key('username'):
899 name = self.form['username'].value
900 try:
901 uid = self.db.user.lookup(name)
902 except KeyError:
903 self.error_message.append('Unknown username')
904 return
905 address = self.db.user.get(uid, 'address')
906 elif self.form.has_key('address'):
907 address = self.form['address'].value
908 uid = uidFromAddress(self.db, ('', address), create=0)
909 if not uid:
910 self.error_message.append('Unknown email address')
911 return
912 name = self.db.user.get(uid, 'username')
913 else:
914 self.error_message.append('You need to specify a username '
915 'or address')
916 return
917
918 # generate the one-time-key and store the props for later
919 otk = ''.join([random.choice(self.chars) for x in range(32)])
920 self.db.otks.set(otk, uid=uid, __time=time.time())
921
922 # send the email
923 tracker_name = self.db.config.TRACKER_NAME
924 subject = 'Confirm reset of password for %s'%tracker_name
925 body = '''
926 Someone, perhaps you, has requested that the password be changed for your
927 username, "%(name)s". If you wish to proceed with the change, please follow
928 the link below:
929
930 %(url)suser?@template=forgotten&@action=passrst&otk=%(otk)s
931
932 You should then receive another email with the new password.
933 '''%{'name': name, 'tracker': tracker_name, 'url': self.base, 'otk': otk}
934 if not self.sendEmail(address, subject, body):
935 return
936
937 self.ok_message.append('Email sent to %s'%address)
835 938
836 def editItemAction(self): 939 def editItemAction(self):
837 ''' Perform an edit of an item in the database. 940 ''' Perform an edit of an item in the database.
838 941
839 See parsePropsFromForm and _editnodes for special variables 942 See parsePropsFromForm and _editnodes for special variables

Roundup Issue Tracker: http://roundup-tracker.org/