Back to index

plone3  3.1.7
RegistrationTool.py
Go to the documentation of this file.
00001 import re
00002 import random
00003 import md5
00004 from smtplib import SMTPRecipientsRefused
00005 
00006 from zope.component import getUtility
00007 
00008 from Products.CMFCore.interfaces import ISiteRoot
00009 
00010 from Products.CMFCore.utils import getToolByName
00011 from Products.CMFDefault.RegistrationTool import RegistrationTool as BaseTool
00012 from Products.CMFPlone import ToolNames
00013 
00014 from Products.CMFCore.permissions import AddPortalMember
00015 
00016 from Globals import InitializeClass
00017 from AccessControl import ClassSecurityInfo, Unauthorized
00018 from Products.CMFPlone.PloneBaseTool import PloneBaseTool
00019 from Products.SecureMailHost.SecureMailHost import EMAIL_RE
00020 from Products.CMFDefault.utils import checkEmailAddress
00021 from Products.CMFDefault.exceptions import EmailAddressInvalid
00022 
00023 from Products.PluggableAuthService.interfaces.authservice \
00024         import IPluggableAuthService
00025 
00026 # - remove '1', 'l', and 'I' to avoid confusion
00027 # - remove '0', 'O', and 'Q' to avoid confusion
00028 # - remove vowels to avoid spelling words
00029 invalid_password_chars = ['a','e','i','o','u','y','l','q']
00030 
00031 def getValidPasswordChars():
00032     password_chars = []
00033     for i in range(0, 26):
00034         if chr(ord('a')+i) not in invalid_password_chars:
00035             password_chars.append(chr(ord('a')+i))
00036             password_chars.append(chr(ord('A')+i))
00037     for i in range(2, 10):
00038         password_chars.append(chr(ord('0')+i))
00039     return password_chars
00040 
00041 password_chars = getValidPasswordChars()
00042 
00043 # seed the random number generator
00044 random.seed()
00045 
00046 
00047 class RegistrationTool(PloneBaseTool, BaseTool):
00048 
00049     meta_type = ToolNames.RegistrationTool
00050     security = ClassSecurityInfo()
00051     toolicon = 'skins/plone_images/pencil_icon.gif'
00052     plone_tool = 1
00053     __implements__ = (PloneBaseTool.__implements__, BaseTool.__implements__, )
00054     md5key = None
00055     _v_md5base = None
00056 
00057     def __init__(self):
00058         if hasattr(BaseTool, '__init__'):
00059             BaseTool.__init__(self)
00060         # build and persist an MD5 key
00061         self.md5key = ''
00062         for i in range(0, 20):
00063             self.md5key += chr(ord('a')+random.randint(0,26))
00064 
00065     def _md5base(self):
00066         if self._v_md5base is None:
00067             self._v_md5base = md5.new(self.md5key)
00068         return self._v_md5base
00069 
00070     # Get a password of the prescribed length
00071     #
00072     # For s=None, generates a random password
00073     # For s!=None, generates a deterministic password using a hash of s
00074     #   (length must be <= 16 for s != None)
00075     #
00076     # TODO: Could this be made private?
00077     def getPassword(self, length=5, s=None):
00078         global password_chars, md5base
00079 
00080         if s is None:
00081             password = ''
00082             nchars = len(password_chars)
00083             for i in range(0, length):
00084                 password += password_chars[random.randint(0,nchars-1)]
00085             return password
00086         else:
00087             m = self._md5base().copy()
00088             m.update(s)
00089             d = m.digest() # compute md5(md5key + s)
00090             assert(len(d) >= length)
00091             password = ''
00092             nchars = len(password_chars)
00093             for i in range(0, length):
00094                 password += password_chars[ord(d[i]) % nchars]
00095             return password
00096 
00097     security.declarePublic('isValidEmail')
00098     def isValidEmail(self, email):
00099         """ checks for valid email """
00100         if EMAIL_RE.search(email) == None:
00101             return 0
00102         try:
00103             checkEmailAddress(email)
00104         except EmailAddressInvalid:
00105             return 0
00106         else:
00107             return 1
00108 
00109     security.declarePublic( 'testPropertiesValidity' )
00110     def testPropertiesValidity(self, props, member=None):
00111 
00112         """ Verify that the properties supplied satisfy portal's requirements.
00113 
00114         o If the properties are valid, return None.
00115         o If not, return a string explaining why.
00116 
00117         This is a customized version of the CMFDefault version: we also
00118         check if the email property is writable before verifying it.
00119         """
00120         if member is None: # New member.
00121 
00122             username = props.get('username', '')
00123             if not username:
00124                 return 'You must enter a valid name.'
00125 
00126             if not self.isMemberIdAllowed(username):
00127                 return ('The login name you selected is already '
00128                         'in use or is not valid. Please choose another.')
00129 
00130             email = props.get('email')
00131             if email is None:
00132                 return 'You must enter an email address.'
00133 
00134             try:
00135                 checkEmailAddress( email )
00136             except EmailAddressInvalid: 
00137                 return 'You must enter a valid email address.'
00138 
00139         else: # Existing member.
00140             if not hasattr(member, 'canWriteProperty') or \
00141                     member.canWriteProperty('email'):
00142 
00143                 email = props.get('email')
00144 
00145                 if email is not None:
00146 
00147                     try:
00148                         checkEmailAddress( email )
00149                     except EmailAddressInvalid:
00150                         return 'You must enter a valid email address.'
00151 
00152                 # Not allowed to clear an existing non-empty email.
00153                 existing = member.getProperty('email')
00154                 
00155                 if existing and email == '':
00156                     return 'You must enter a valid email address.'
00157 
00158         return None
00159 
00160 
00161     security.declareProtected(AddPortalMember, 'isMemberIdAllowed')
00162     def isMemberIdAllowed(self, id):
00163         if len(id) < 1 or id == 'Anonymous User':
00164             return 0
00165         if not self._ALLOWED_MEMBER_ID_PATTERN.match( id ):
00166             return 0
00167 
00168         pas = getToolByName("acl_users")
00169         if IPluggableAuthService.providedBy(pas):
00170             results = pas.searchPrincipals(id=id)
00171             if results:
00172                 return 0
00173         else:
00174             membership = getToolByName(self, 'portal_membership')
00175             if membership.getMemberById(id) is not None:
00176                 return 0
00177             groups = getToolByName(self, 'portal_groups')
00178             if groups.getGroupById(id) is not None:
00179                 return 0
00180 
00181         return 1
00182 
00183 
00184     security.declarePublic('generatePassword')
00185     def generatePassword(self):
00186         """Generates a password which is guaranteed to comply
00187         with the password policy."""
00188         return self.getPassword(6)
00189 
00190     security.declarePublic('generateResetCode')
00191     def generateResetCode(self, salt, length=14):
00192         """Generates a reset code which is guaranteed to return the
00193         same value for a given length and salt, every time."""
00194         return self.getPassword(length, salt)
00195 
00196     security.declarePublic('mailPassword')
00197     def mailPassword(self, forgotten_userid, REQUEST):
00198         """ Wrapper around mailPassword """
00199         membership = getToolByName(self, 'portal_membership')
00200         if not membership.checkPermission('Mail forgotten password', self):
00201             raise Unauthorized, "Mailing forgotten passwords has been disabled"
00202 
00203         utils = getToolByName(self, 'plone_utils')
00204         member = membership.getMemberById(forgotten_userid)
00205 
00206         if member is None:
00207             raise ValueError, 'The username you entered could not be found'
00208 
00209         # assert that we can actually get an email address, otherwise
00210         # the template will be made with a blank To:, this is bad
00211         email = member.getProperty('email')
00212         if not email:
00213             raise ValueError('That user does not have an email address.')
00214         else:
00215             # add the single email address
00216             if not utils.validateSingleEmailAddress(email):
00217                 raise ValueError, 'The email address did not validate'
00218         check, msg = _checkEmail(email)
00219         if not check:
00220             raise ValueError, msg
00221 
00222         # Rather than have the template try to use the mailhost, we will
00223         # render the message ourselves and send it from here (where we
00224         # don't need to worry about 'UseMailHost' permissions).
00225         reset_tool = getToolByName(self, 'portal_password_reset')
00226         reset = reset_tool.requestReset(forgotten_userid)
00227 
00228         
00229         email_charset = getattr(self, 'email_charset', 'UTF-8')
00230         mail_text = self.mail_password_template( self
00231                                                , REQUEST
00232                                                , member=member
00233                                                , reset=reset
00234                                                , password=member.getPassword()
00235                                                , charset=email_charset
00236                                                )
00237         if isinstance(mail_text, unicode):
00238             mail_text = mail_text.encode(email_charset)
00239         host = self.MailHost
00240         try:
00241             host.send( mail_text )
00242 
00243             return self.mail_password_response( self, REQUEST )
00244         except SMTPRecipientsRefused:
00245             # Don't disclose email address on failure
00246             raise SMTPRecipientsRefused('Recipient address rejected by server')
00247 
00248     security.declarePublic('registeredNotify')
00249     def registeredNotify(self, new_member_id):
00250         """ Wrapper around registeredNotify """
00251         membership = getToolByName( self, 'portal_membership' )
00252         utils = getToolByName(self, 'plone_utils')
00253         member = membership.getMemberById( new_member_id )
00254 
00255         if member and member.getProperty('email'):
00256             # add the single email address
00257             if not utils.validateSingleEmailAddress(member.getProperty('email')):
00258                 raise ValueError, 'The email address did not validate'
00259 
00260         email = member.getProperty( 'email' )
00261         try:
00262             checkEmailAddress(email)
00263         except EmailAddressInvalid:
00264             raise ValueError, 'The email address did not validate'
00265 
00266         pwrt = getToolByName(self, 'portal_password_reset')
00267         reset = pwrt.requestReset(new_member_id)
00268 
00269         # Rather than have the template try to use the mailhost, we will
00270         # render the message ourselves and send it from here (where we
00271         # don't need to worry about 'UseMailHost' permissions).
00272         mail_text = self.registered_notify_template( self
00273                                                    , self.REQUEST
00274                                                    , member=member
00275                                                    , reset=reset
00276                                                    , email=email
00277                                                    )
00278 
00279         host = self.MailHost
00280         encoding = getUtility(ISiteRoot).getProperty('email_charset')
00281         host.send(mail_text.encode(encoding))
00282 
00283         return self.mail_password_response( self, self.REQUEST )
00284 
00285     def isMemberIdAllowed(self, id):
00286         if len(id) < 1 or id == 'Anonymous User':
00287             return 0
00288         if not self._ALLOWED_MEMBER_ID_PATTERN.match( id ):
00289             return 0
00290 
00291         pas = getToolByName(self, 'acl_users')
00292         if IPluggableAuthService.providedBy(pas):
00293             results = pas.searchPrincipals(id=id, exact_match=True)
00294             if results:
00295                 return 0
00296         else:
00297             membership = getToolByName(self, 'portal_membership')
00298             if membership.getMemberById(id) is not None:
00299                 return 0
00300             groups = getToolByName(self, 'portal_groups')
00301             if groups.getGroupById(id) is not None:
00302                 return 0
00303 
00304         return 1
00305 
00306 RegistrationTool.__doc__ = BaseTool.__doc__
00307 
00308 InitializeClass(RegistrationTool)
00309 
00310 _TESTS = ( ( re.compile("^[0-9a-zA-Z\.\-\_\+\']+\@[0-9a-zA-Z\.\-]+$")
00311            , True
00312            , "Failed a"
00313            )
00314          , ( re.compile("^[^0-9a-zA-Z]|[^0-9a-zA-Z]$")
00315            , False
00316            , "Failed b"
00317            )
00318          , ( re.compile("([0-9a-zA-Z_]{1})\@.")
00319            , True
00320            , "Failed c"
00321            )
00322          , ( re.compile(".\@([0-9a-zA-Z]{1})")
00323            , True
00324            , "Failed d"
00325            )
00326          , ( re.compile(".\.\-.|.\-\..|.\.\..|.\-\-.")
00327            , False
00328            , "Failed e"
00329            )
00330          , ( re.compile(".\.\_.|.\-\_.|.\_\..|.\_\-.|.\_\_.")
00331            , False
00332            , "Failed f"
00333            )
00334          , ( re.compile(".\.([a-zA-Z]{2,3})$|.\.([a-zA-Z]{2,4})$")
00335            , True
00336            , "Failed g"
00337            )
00338          )
00339 
00340 def _checkEmail( address ):
00341     for pattern, expected, message in _TESTS:
00342         matched = pattern.search( address ) is not None
00343         if matched != expected:
00344             return False, message
00345     return True, ''
00346