Mercurial > p > roundup > code
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 |
