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 $
22
33__doc__ = """
44WWW request handler (also used in the stand-alone server).
55"""
66
77import os , os .path , cgi , StringIO , urlparse , re , traceback , mimetypes , urllib
88import binascii , Cookie , time , random , MimeWriter , smtplib , socket , quopri
9- import stat , rfc822
9+ import stat , rfc822 , string
1010
1111from roundup import roundupdb , date , hyperdb , password
1212from roundup .i18n import _
1313from roundup .cgi .templating import Templates , HTMLRequest , NoTemplate
1414from roundup .cgi import cgitb
1515from roundup .cgi .PageTemplates import PageTemplate
1616from roundup .rfc2822 import encode_header
17+ from roundup .mailgw import uidFromAddress
1718
1819class HTTPException (Exception ):
1920 pass
@@ -257,20 +258,31 @@ def inner_main(self):
257258 self .write (cgitb .html ())
258259
259260 def clean_sessions (self ):
260- '''age sessions, remove when they haven't been used for a week.
261- Do it only once an hour'''
261+ ''' Age sessions, remove when they haven't been used for a week.
262+
263+ Do it only once an hour.
264+
265+ Note: also cleans One Time Keys, and other "session" based
266+ stuff.
267+ '''
262268 sessions = self .db .sessions
263269 last_clean = sessions .get ('last_clean' , 'last_use' ) or 0
264270
265271 week = 60 * 60 * 24 * 7
266272 hour = 60 * 60
267273 now = time .time ()
268274 if now - last_clean > hour :
269- # remove age sessions
275+ # remove aged sessions
270276 for sessid in sessions .list ():
271277 interval = now - sessions .get (sessid , 'last_use' )
272278 if interval > week :
273279 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 )
274286 sessions .set ('last_clean' , last_use = time .time ())
275287
276288 def determine_user (self ):
@@ -479,6 +491,7 @@ def renderContext(self):
479491 ('new' , 'newItemAction' ),
480492 ('register' , 'registerAction' ),
481493 ('confrego' , 'confRegoAction' ),
494+ ('passrst' , 'passResetAction' ),
482495 ('login' , 'loginAction' ),
483496 ('logout' , 'logout_action' ),
484497 ('search' , 'searchAction' ),
@@ -489,17 +502,8 @@ def handle_action(self):
489502 ''' Determine whether there should be an Action called.
490503
491504 The action is defined by the form variable :action which
492- identifies the method on this object to call. The four basic
493- actions 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
505+ identifies the method on this object to call. The actions
506+ are defined in the "actions" sequence on this class.
503507 '''
504508 if self .form .has_key (':action' ):
505509 action = self .form [':action' ].value .lower ()
@@ -675,7 +679,7 @@ def logout_action(self):
675679 # Let the user know what's going on
676680 self .ok_message .append (_ ('You are logged out' ))
677681
678- chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
682+ chars = string . letters + string . digits
679683 def registerAction (self ):
680684 '''Attempt to create a new user based on the contents of the form
681685 and then set the cookie.
@@ -713,15 +717,35 @@ def registerAction(self):
713717 props [propname ] = str (value )
714718 elif isinstance (proptype , hyperdb .Password ):
715719 props [propname ] = str (value )
720+ props ['__time' ] = time .time ()
716721 self .db .otks .set (otk , ** props )
717722
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 ):
718743 # send email to the user's email address
719744 message = StringIO .StringIO ()
720745 writer = MimeWriter .MimeWriter (message )
721746 tracker_name = self .db .config .TRACKER_NAME
722- s = 'Complete your registration to %s' % tracker_name
723- writer .addheader ('Subject' , encode_header (s ))
724- writer .addheader ('To' , props ['address' ])
747+ writer .addheader ('Subject' , encode_header (subject ))
748+ writer .addheader ('To' , to )
725749 writer .addheader ('From' , roundupdb .straddr ((tracker_name ,
726750 self .db .config .ADMIN_EMAIL )))
727751 writer .addheader ('Date' , time .strftime ("%a, %d %b %Y %H:%M:%S +0000" ,
@@ -734,36 +758,23 @@ def registerAction(self):
734758 body = writer .startbody ('text/plain; charset=utf-8' )
735759
736760 # message body, encoded quoted-printable
737- content = StringIO .StringIO ('''
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 })
761+ content = StringIO .StringIO (content )
744762 quopri .encode (content , body , 0 )
745763
746764 # now try to send the message
747765 try :
748766 # send the message as admin so bounces are sent there
749767 # instead of to roundup
750768 smtp = smtplib .SMTP (self .db .config .MAILHOST )
751- smtp .sendmail (self .db .config .ADMIN_EMAIL , [props ['address' ]],
752- message .getvalue ())
769+ smtp .sendmail (self .db .config .ADMIN_EMAIL , [to ], message .getvalue ())
753770 except socket .error , value :
754- self .error_message .append ("Error: couldn't send "
755- "confirmation email: mailhost %s" % value )
756- return
771+ self .error_message .append ("Error: couldn't send email: "
772+ "mailhost %s" % value )
773+ return 0
757774 except smtplib .SMTPException , value :
758- self .error_message .append ("Error: couldn't send "
759- "confirmation email: %s" % value )
760- return
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
775+ self .error_message .append ("Error: couldn't send email: %s" % value )
776+ return 0
777+ return 1
767778
768779 def registerPermission (self , props ):
769780 ''' Determine whether the user has permission to register
@@ -805,6 +816,7 @@ def confRegoAction(self):
805816# try:
806817 if 1 :
807818 props ['roles' ] = self .instance .config .NEW_WEB_USER_ROLES
819+ del props ['__time' ]
808820 self .userid = cl .create (** props )
809821 # clear the props from the otk database
810822 self .db .otks .destroy (otk )
@@ -833,6 +845,97 @@ def confRegoAction(self):
833845 raise Redirect , '%suser%s?@ok_message=%s' % (
834846 self .base , self .userid , urllib .quote (message ))
835847
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 )
938+
836939 def editItemAction (self ):
837940 ''' Perform an edit of an item in the database.
838941
0 commit comments