import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt import time, whrandom, smtplib, base64, sha, email, types from distutils.util import rfc822_escape from xml.sax.saxutils import escape as xmlescape import store, config, flamenco, trove class NotFound(Exception): pass class Unauthorised(Exception): pass class Forbidden(Exception): pass class Redirect(Exception): pass class FormError(Exception): pass __version__ = '1.0' URL_MACHINE = 'http://www.python.org' URL_PATH = '/pypi' # email sent to user indicating how they should complete their registration rego_message = '''Subject: Complete your PyPI registration To: %(email)s To complete your registration of the user "%(name)s" with the python module index, please visit the following URL: %(url)s?:action=user&otk=%(otk)s ''' # password change request email password_change_message = '''Subject: PyPI password change request To: %(email)s Someone, perhaps you, has requested that the password be changed for your username, "%(name)s". If you wish to proceed with the change, please follow the link below: %(url)s?:action=password_reset&email=%(email)s You should then receive another email with the new password. ''' # password reset email - indicates what the password is now password_message = '''Subject: PyPI password has been reset To: %(email)s Your login is: %(name)s Your password is now: %(password)s ''' unauth_message = '''

If you are a new user, please register.

If you have forgotten your password, you can have it reset for you.

'''%(URL_PATH, URL_PATH) chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' def packageURL(name, version): ''' return a URL for the link to display a particular package ''' return '%s?:action=display&name=%s&version=%s'%(URL_PATH, urllib.quote(name), urllib.quote(version)) def packageLink(name, version): ''' return a URL for the link to display a particular package ''' return '%s %s'%(packageURL(name, version), cgi.escape(name), cgi.escape(version)) class WebUI: ''' Handle a request as defined by the "env" parameter. "handler" gives access to the user via rfile and wfile, and a few convenience functions (see pypi). The handling of a request goes as follows: 1. open the database 2. see if the request is supplied with authentication information 3. perform the action defined by :action ("index" if none is supplied) 4a. handle exceptions sanely, including special ones like NotFound, Unauthorised, Redirect and FormError, or 4b. commit changes to the database 5. close the database to finish off ''' def __init__(self, handler, env): self.handler = handler self.config = handler.config self.wfile = handler.wfile self.env = env self.form = cgi.FieldStorage(fp=handler.rfile, environ=env) whrandom.seed(int(time.time())%256) self.nav_current = None def run(self): ''' Run the request, handling all uncaught errors and finishing off cleanly. ''' self.store = store.Store(self.config) self.store.open() try: try: self.inner_run() except NotFound: self.fail('Not Found', code=404) except Unauthorised, message: message = str(message) if not message: message = 'You must login to access this feature' self.fail(message, code=401, heading='Login required', content=unauth_message, headers={'WWW-Authenticate': 'Basic realm="pypi"'}) except Forbidden, message: message = str(message) self.fail(message, code=403, heading='Forbidden') except Redirect, path: self.handler.send_response(301) self.handler.send_header('Location', path) except FormError, message: message = str(message) self.fail(message, code=400, heading='Error processing form') except: s = StringIO.StringIO() traceback.print_exc(None, s) s = cgi.escape(s.getvalue()) self.fail('Internal Server Error', code=500, heading='Error...', content='
%s
'%s) finally: self.store.close() def fail(self, message, title="Python Packages Index", code=400, heading=None, headers={}, content=''): ''' Indicate to the user that something has failed. ''' self.page_head(title, message, heading, code, headers) self.wfile.write('

%s

'%message) self.wfile.write(content) self.page_foot() def success(self, message=None, title="Python Packages Index", code=200, heading=None, headers={}, content=''): ''' Indicate to the user that the operation has succeeded. ''' self.page_head(title, message, heading, code, headers) if message: self.wfile.write('

%s

'%message) self.wfile.write(content) self.page_foot() navlinks = ( ('home', 'PyPI home'), ('browse', 'Browse packages'), ('search_form', 'Search'), ('index', 'List all packages'), ('submit_form', 'Package submission'), ('list_classifiers', 'List trove classifiers'), ('rss', 'RSS (last 20 updates)'), ('role_form', 'Admin'), ) def page_head(self, title, message=None, heading=None, code=200, headers={}): ''' Spit out HTTP and HTML headers. "title" is the HTML page title "message" is a succint error or success message "code" is the HTTP response code "heading" is usually a slight variation on "title" and is used in a HTML header "headers" is a dictionary of additional HTTP headers to send ''' if not message: message = 'OK' self.handler.send_response(code, message) self.handler.send_header('Content-Type', 'text/html') for k,v in headers.items(): self.handler.send_header(k, v) self.handler.end_headers() if heading is None: heading = title w = self.wfile.write banner_num = whrandom.randrange(0,64) banner_color = [ '#3399ff', '#6699cc', '#3399ff', '#0066cc', '#3399ff', '#0066cc', '#0066cc', '#3399ff', '#3399ff', '#3399ff', '#3399ff', '#6699cc', '#3399ff', '#3399ff', '#ffffff', '#6699cc', '#0066cc', '#3399ff', '#0066cc', '#3399ff', '#6699cc', '#0066cc', '#6699cc', '#3399ff', '#3399ff', '#6699cc', '#3399ff', '#3399ff', '#6699cc', '#6699cc', '#0066cc', '#6699cc', '#0066cc', '#6699cc', '#0066cc', '#0066cc', '#6699cc', '#3399ff', '#0066cc', '#bbd6f1', '#0066cc', '#6699cc', '#3399ff', '#3399ff', '#0066cc', '#0066cc', '#0066cc', '#6699cc', '#6699cc', '#3399ff', '#3399ff', '#6699cc', '#0066cc', '#0066cc', '#6699cc', '#0066cc', '#6699cc', '#3399ff', '#6699cc', '#3399ff', '#d6ebff', '#6699cc', '#3399ff', '#0066cc', ][banner_num] w('''\ %s
  
 

PyPI: %s

'''%heading) def page_foot(self): self.wfile.write('''
''') def inner_run(self): ''' Figure out what the request is, and farm off to the appropriate handler. ''' # see if the user has provided a username/password self.username = None auth = self.env.get('HTTP_CGI_AUTHORIZATION', '').strip() if auth: authtype, auth = auth.split() if authtype.lower() == 'basic': un, pw = base64.decodestring(auth).split(':') if self.store.has_user(un): pw = sha.sha(pw).hexdigest() user = self.store.get_user(un) if pw != user['password']: raise Unauthorised, 'Incorrect password' self.username = un self.store.set_user(un, self.env['REMOTE_ADDR']) # now handle the request if self.form.has_key(':action'): action = self.form[':action'].value else: action = 'home' # make sure the user has permission if action in ('submit', ): if self.username is None: raise Unauthorised if self.store.get_otk(self.username): raise Unauthorised # handle the action if action in 'home browse rss submit submit_pkg_info remove_pkg pkg_edit verify submit_form display display_pkginfo search_form register_form user_form forgotten_password_form user password_reset index search role role_form list_classifiers login logout'.split(): getattr(self, action)() else: raise ValueError, 'Unknown action' # commit any database changes self.store.commit() def home(self, nav_current='home'): content = StringIO.StringIO() w = content.write w('''

Welcome to the Python Package Index (PyPI).

Tip of the week: Did you know that you can get a full list of the available Trove classifiers online and through the register command? The online version is available through the sidebar link "List trove classifiers", and the register command version using the "--list-classifiers" option (ie. python setup.py register --list-classifiers".)

You may:

Last 20 updates:

'''%(URL_PATH, URL_PATH, URL_PATH, URL_PATH, URL_PATH)) i=0 for name, version, date, summary in self.store.latest_updates(): w(''' '''%((i/3)%2 and ' class="alt"' or '', date[:10], packageLink(name, version), cgi.escape(str(summary)))) i+=1 w('''
UpdatedPackageDescription
%s %s %s
 
''') self.success(heading='Home', content=content.getvalue()) def rss(self): """Dump the last N days' updates as an RSS feed. """ self.handler.send_response(200, 'OK') self.handler.send_header('Content-Type', 'text/xml') self.handler.end_headers() w = self.wfile.write w(''' PyPI recent updates %s%s Updates to the Python Packages Index (PyPI) en '''%(__version__, URL_MACHINE, URL_PATH)) for name, version, date, summary in self.store.latest_updates(): date = date.replace(' ','T') w(''' %s %s http://www.python.org%s %s %sZ '''%(xmlescape(name), xmlescape(version), xmlescape(packageURL(name, version)), xmlescape(summary), date)) w(''' ''') def browse(self, nav_current='browse'): content = StringIO.StringIO() w = content.write tree = trove.Trove(self.store.cursor) qs = os.environ.get('QUERY_STRING', '') l = [x for x in cgi.parse_qsl(qs) if not x[0].startswith(':')] q = flamenco.Query(self.store.cursor, tree, l) # do the query matches, choices = q.list_choices() # format the result if q.query: w('

Current query:
') for fld, value in q.query: n = q.trove[value] newq = q.copy() newq.remove_field(fld, value) w(cgi.escape(n.path)) w(' [ignore]
\n'%( URL_PATH, newq.as_href())) else: w('

Currently querying everything') w('

') if q.query and matches: w('') w('') i=0 for summary, name, version in matches: w(''' '''%((i/3)%2 and ' class="alt"' or '', packageLink(name, version), cgi.escape(str(summary)))) i+=1 w('''
PackageDescription
%s %s
 
''') else: w('

Number of matches: %i

\n'%len(matches)) w('
\n') choices.sort() for field, header, options, exist_value in choices: if len(options) == 0: continue w('
') w('%s
'%cgi.escape(header)) options.sort() l = [] for text, node_id, count in options: newq = q.copy() newq.set_field(field, exist_value, node_id) l.append('%s (%i)'%( URL_PATH, newq.as_href(), cgi.escape(text), count)) w(' / \n'.join(l)) w("
") self.success(heading='Browsing', content=content.getvalue()) def index(self, nav_current='index'): ''' Print up an index page ''' self.nav_current = nav_current content = StringIO.StringIO() w = content.write w('\n') w('\n') spec = self.form_metadata() if not spec.has_key('_pypi_hidden'): spec['_pypi_hidden'] = '0' i=0 for pkg in self.store.query_packages(spec): name = pkg['name'] version = pkg['version'] w(''' '''%((i/3)%2 and ' class="alt"' or '', packageLink(name, version), cgi.escape(str(pkg['summary'])))) i+=1 w('''
PackageDescription
%s %s
 
''') self.success(heading='Index of packages', content=content.getvalue()) def search(self): """Same as index, but don't disable the search or index nav links """ self.index(nav_current=None) def logout(self): raise Unauthorised def login(self): if not self.username: raise Unauthorised self.index() def search_form(self): ''' A form used to generate filtered index displays ''' self.nav_current = 'search_form' self.page_head('Search') self.wfile.write('''
Name:
Version:
Summary:
Description:
Keywords:
Hidden:
 
'''%URL_PATH) self.page_foot() def role_form(self): ''' A form used to maintain user Roles ''' self.nav_current = 'role_form' package_name = '' if self.form.has_key('package_name'): package_name = self.form['package_name'].value if not (self.store.has_role('Admin', package_name) or self.store.has_role('Owner', package_name)): raise Unauthorised package = ''' Package Name: '''%cgi.escape(package_name) elif not self.store.has_role('Admin', ''): raise Unauthorised else: s = '\n'.join([''%( cgi.escape(x['name']), cgi.escape(x['name'])) for x in self.store.get_packages()]) package = ''' Package Name: '''%s s = '\n'.join([''%(x['name'], x['name'], x['email']) for x in self.store.get_users()]) users = ''%s if self.store.has_role('Admin', None): admin = '' else: admin = '' # now write the body s = '''

Use this form to add or remove a user\'s Role for a Package. The available Roles are defined as:

Owner
Owns a package name, may assign Maintainer Role for that name. The first user to register information about a package is deemed Owner of the package name. The Admin user may change this if necessary. May submit updates for the package name.
Maintainer
Can submit and update info for a particular package name.

 

%s
User Name: %s
Role to Add:
 
'''%(users, package, admin) # list the existing role assignments if package_name: s += self.package_role_list(package_name, 'Existing Roles') self.success(heading='Role maintenance', content=s) def package_role_list(self, name, heading='Assigned Roles'): ''' Generate an HTML fragment for a package Role display. ''' l = ['', ''%heading, ''] for assignment in self.store.get_package_roles(name): l.append(''%( cgi.escape(assignment['user_name']), cgi.escape(assignment['role_name']))) l.append('
%s
UserRole
%s%s
') return '\n'.join(l) def role(self): ''' Add a Role to a user. ''' # required fields if not self.form.has_key('package_name'): raise FormError, 'package_name is required' if not self.form.has_key('user_name'): raise FormError, 'user_name is required' if not self.form.has_key('role_name'): raise FormError, 'role_name is required' if not self.form.has_key(':operation'): raise FormError, ':operation is required' # get the values package_name = self.form['package_name'].value user_name = self.form['user_name'].value role_name = self.form['role_name'].value # further validation if role_name not in ('Owner', 'Maintainer'): raise FormError, 'role_name not Owner or Maintainer' if not self.store.has_user(user_name): raise FormError, "user doesn't exist" # add or remove operation = self.form[':operation'].value if operation == 'Add Role': # make sure the user doesn't have the role if self.store.has_role(role_name, package_name, user_name): raise FormError, 'user has that role already' self.store.add_role(user_name, role_name, package_name) message = 'Role Added OK' else: # make sure if the user has the role, and is the current user # and the role is Owner, that we can't do this! if self.username == user_name and role_name == 'Owner': raise FormError, "sanity: can't remove own Owner Role" # make sure the user has the role if not self.store.has_role(role_name, package_name, user_name): raise FormError, "user doesn't have that role" self.store.delete_role(user_name, role_name, package_name) message = 'Role Removed OK' # XXX make this call display self.success(message=message, heading='Role maintenance') def display_pkginfo(self, name=None, version=None): '''Reconstruct and send a PKG-INFO metadata file. ''' # get the appropriate package info from the database if name is None: name = self.form['name'].value if version is None: if self.form.has_key('version'): version = self.form['version'].value else: raise NotImplementedError, 'get the latest version' info = self.store.get_package(name, version) if not info: return self.fail('No such package / version', heading='%s %s'%(name, version), content="I can't find the package / version you're requesting") content = StringIO.StringIO() w = content.write # Some things (download-url, classifier) aren't in metadata v1.0 as # defined in PEP 241, but they'd be nice to publish anyway. PEP 241 # doesn't take an explicit stance on the handling of undefined fields. w("Metadata-Version: 1.0\n") # now the package info keys = info.keys() keys.sort() keypref = 'name version author author_email maintainer maintainer_email home_page download_url summary license description keywords platform'.split() for key in keypref: value = info.get(key) if not value: continue label = key.capitalize().replace('_', '-') if key == 'description': value = rfc822_escape(value) w('%s: %s\n' % (label, value)) # Classifiers aren't PEP 241, but PEP 301 suggests they be in # metadata files. classifiers = self.store.get_release_classifiers(name, version) for c in classifiers: w('Classifier: %s\n' % (c,)) w('\n') # Not using self.success or page_head because we want # plain-text without all the html trappings. self.handler.send_response(200, "OK") self.handler.send_header('Content-Type', 'text/plain') self.handler.end_headers() self.wfile.write(content.getvalue()) def display(self, name=None, version=None, ok_message=None, error_message=None): ''' Print up an entry ''' content = StringIO.StringIO() w = content.write # get the appropriate package info from the database if name is None: name = self.form['name'].value if version is None: if self.form.has_key('version'): version = self.form['version'].value else: raise NotImplementedError, 'get the latest version' info = self.store.get_package(name, version) if not info: return self.fail('No such package / version', heading='%s %s'%(name, version), content="I can't find the package / version you're requesting") # top links un = urllib.quote(name) uv = urllib.quote(version) w('
Package: ') w('admin\n'%( URL_PATH, un)) w('| edit'%(URL_PATH, un, uv)) w('| PKG-INFO'%(URL_PATH, un, uv)) w('
') # now the package info w('\n') keys = info.keys() keys.sort() keypref = 'name version author author_email maintainer maintainer_email home_page download_url summary license description keywords platform'.split() for key in keypref: if not info.has_key(key): continue value = info[key] if not value: continue if key == 'download_url': label = "Download URL" else: label = key.capitalize().replace('_', ' ') if (key in ('download_url', 'url', 'home_page') and value != 'UNKNOWN'): w('\n'%(label, value, cgi.escape(value))) elif key == 'description': w('\n'%( label, cgi.escape(value))) elif key.endswith('_email'): value = cgi.escape(value) value = value.replace('@', ' at ') value = value.replace('.', ' ') w('\n'%(label, value)) else: w('\n'%(label, cgi.escape(value))) classifiers = self.store.get_release_classifiers(name, version) if classifiers: w('\n') w('\n
%s: %s
%s:
%s
%s: %s
%s: %s
Classifiers: ') w('\n
'.join([cgi.escape(x) for x in classifiers])) w('\n
\n') # package's role assignments w(self.package_role_list(name)) # package's journal w('\n') w('\n') w('\n') for entry in self.store.get_journal(name, version): w('\n'%( entry['submitted_date'], entry['submitted_by'], entry['action'])) w('\n
Journal
DateUserAction
%s%s%s
\n') if error_message: self.fail(error_message, heading='%s %s'%(name, version), content=content.getvalue()) else: self.success(message=ok_message, heading='%s %s'%(name, version), content=content.getvalue()) def submit_form(self): ''' A form used to submit or edit package metadata. ''' # submission of this form requires a login, so we should check # before someone fills it in ;) if not self.username: raise Unauthorised, 'You must log in.' # are we editing a specific entry? info = {} name = version = None if self.form.has_key('name') and self.form.has_key('version'): name = self.form['name'].value version = self.form['version'].value # permission to do this? if not (self.store.has_role('Owner', name) or self.store.has_role('Maintainer', name)): raise Forbidden, 'Not Owner or Maintainer' # get the stored info for k,v in self.store.get_package(name, version).items(): info[k] = v content = StringIO.StringIO() w = content.write self.nav_current = 'submit_form' w('''

To submit information to this index, you have three options:

1. Use the new distutils "register" command. If you\'re not using python 2.3, you need to:

  1. Copy register.py to your python lib "distutils/command" directory (typically something like "/usr/lib/python2.2/distutils/command/").
  2. Run "setup.py register" as you would normally run "setup.py" for your distribution - this will register your distribution\'s metadata with the index.
  3. ... that\'s it

2. Or upload your PKG-INFO file (generated by distutils) here:

PKG-INFO file:
 

3. Or enter the information manually:

''') # display all the properties for property in 'name version author author_email maintainer maintainer_email home_page license summary description keywords platform download_url _pypi_hidden'.split(): # get the existing entry if self.form.has_key(property): value = self.form[property].value else: value = info.get(property, '') if value is None: value = '' # form field if property == '_pypi_hidden': a = value=='0' and ' selected' or '' b = value=='1' and ' selected' or '' field = ''''''%(a,b) elif property.endswith('description'): field = ''%(property, cgi.escape(value)) else: field = ''%(property, cgi.escape(value)) # now spit out the form line label = property.replace('_', ' ').capitalize() if label in ('Name', 'Version'): req = 'class="required"' elif property == 'download_url': label = "Download URL" elif property == '_pypi_hidden': label = "Hidden" else: req = '' w('\n'%(req, label, field)) # if we're editing if name is not None: release_cifiers = {} for classifier in self.store.get_release_classifiers(name, version): release_cifiers[classifier] = 1 else: release_cifiers = {} # now list 'em all w('''
%s:%s
Classifiers:
highlightedinformation is required
 
''') self.success(heading='Submitting package information', content=content.getvalue()) def submit_pkg_info(self): ''' Handle the submission of distro metadata as a PKG-INFO file. ''' # make sure the user is identified if not self.username: raise Unauthorised, \ "You must be identified to store package information" if not self.form.has_key('pkginfo'): raise FormError, \ "You must supply the PKG-INFO file" # get the data mess = email.message_from_file(self.form['pkginfo'].file) data = {} for k, v in mess.items(): # clean up the keys and values k = k.lower() v = v.strip() # Platform, Classifiers, ...? if data.has_key(k): l = data[k] if isinstance(l, types.ListType): l.append(v) else: data[k] = [v] else: data[k] = v # flatten platforms into one string platform = data.get('platform', '') if isinstance(platform, types.ListType): data['platform'] = ','.join(data['platform']) # rename classifiers if data.has_key('classifier'): classifiers = data['classifier'] if not isinstance(classifiers, types.ListType): classifiers = [classifiers] data['classifiers'] = classifiers # validate the data try: self.validate_metadata(data) except ValueError, message: raise FormError, message name = data['name'] version = data['version'] # don't hide by default if not data.has_key('_pypi_hidden'): data['_pypi_hidden'] = '0' # make sure the user has permission to do stuff if self.store.has_package(name) and not ( self.store.has_role('Owner', name) or self.store.has_role('Maintainer', name)): raise Forbidden, \ "You are not allowed to store '%s' package information"%name # save off the data message = self.store.store_package(name, version, data) self.store.commit() # return a display of the package self.form.value.append(cgi.MiniFieldStorage('name', data['name'])) self.form.value.append(cgi.MiniFieldStorage('version', data['version'])) self.display(ok_message=message) def submit(self): ''' Handle the submission of distro metadata. ''' # make sure the user is identified if not self.username: raise Unauthorised, \ "You must be identified to store package information" # pull the package information out of the form submission data = self.form_metadata() # make sure classifiers is a list if data.has_key('classifiers'): classifiers = data['classifiers'] if not isinstance(classifiers, types.ListType): classifiers = [classifiers] data['classifiers'] = classifiers # validate the data try: self.validate_metadata(data) except ValueError, message: raise FormError, message name = data['name'] version = data['version'] # make sure the user has permission to do stuff if self.store.has_package(name) and not ( self.store.has_role('Owner', name) or self.store.has_role('Maintainer', name)): raise Forbidden, \ "You are not allowed to store '%s' package information"%name # make sure the _pypi_hidden flag is set if not data.has_key('_pypi_hidden'): data['_pypi_hidden'] = '0' # save off the data message = self.store.store_package(name, version, data) self.store.commit() # return a display of the package self.display(ok_message=message) def form_metadata(self): ''' Extract metadata from the form. ''' data = {} for k in self.form.keys(): if k.startswith(':'): continue v = self.form[k] if type(v) == type([]): if k == 'classifiers': v = [x.value.strip() for x in v] else: v = ','.join([x.value.strip() for x in v]) else: v = v.value.strip() data[k.lower()] = v return data def verify(self): ''' Validate the input data. ''' data = self.form_metadata() try: self.validate_metadata(data) except ValueError, message: self.fail(message, code=400, heading='Package verification') return self.success(heading='Package verification', message='Validated OK') def validate_metadata(self, data): ''' Validate the contents of the metadata. ''' if not data.has_key('name'): raise ValueError, 'Missing required field "name"' if not data.has_key('version'): raise ValueError, 'Missing required field "version"' if data.has_key('metadata_version'): del data['metadata_version'] if data.has_key('classifiers'): d = {} for entry in self.store.get_classifiers(): d[entry] = 1 for entry in data['classifiers']: if d.has_key(entry): continue raise ValueError, 'Invalid classifier "%s"'%entry def pkg_edit(self): ''' Edit info about a bunch of packages at one go ''' # make sure the user is identified if not self.username: raise Unauthorised, \ "You must be identified to edit package information" name = self.form['name'].value # make sure the user has permission to do stuff if not (self.store.has_role('Owner', name) or self.store.has_role('Maintainer', name)): raise Forbidden, \ "You are not allowed to edit '%s' package information"%name releases = self.store.get_package_releases(name) reldict = {} for release in releases: info = {} for k,v in release.items(): info[k] = v reldict[info['version']] = info content = StringIO.StringIO() w = content.write # see if we're editing for key in self.form.keys(): if key.startswith('hid_'): ver = key[4:] info = reldict[ver] info['_pypi_hidden'] = self.form[key].value elif key.startswith('sum_'): ver = key[4:] info = reldict[ver] info['summary'] = self.form[key].value # update the database for version, info in reldict.items(): self.store.store_package(name, version, info) # now display un = urllib.quote(name) cn = cgi.escape(name) url = URL_PATH w('''

Each package may have a release for each version of the package that is released. You may use this form to hide releases from users.

Role admin

Remove package

'''%(URL_PATH, un, URL_PATH, un, cn)) w('''''') for release in releases: release = reldict[release['version']] uv = urllib.quote(release['version']) cv = cgi.escape(release['version']) selname = 'hid_' + uv selyes = selno = '' if release['_pypi_hidden'] == '1': selyes = ' selected' else: selno = ' selected' sumname = 'sum_' + uv summary = cgi.escape(release['summary']) w(''' '''%locals()) w('''
VersionHide?SummaryActions
%(cv)s show edit remove
''') self.success(heading='Package %s'%cn, content=content.getvalue()) def remove_pkg(self): ''' Remove a release or a whole package from the db. Only owner may remove an entire package - Maintainers may remove releases. ''' # make sure the user is identified if not self.username: raise Unauthorised, \ "You must be identified to edit package information" # vars name = self.form['name'].value cn = cgi.escape(name) if self.form.has_key('version'): version = self.form['version'].value cv = cgi.escape(version) desc = 'release %s of package %s.'%(cv, cn) else: version = None desc = 'all information about package %s.'%cn # make sure the user has permission to do stuff if not (self.store.has_role('Owner', name) or (version and self.store.has_role('Maintainer', name))): raise Forbidden, \ "You are not allowed to edit '%s' package information"%name if not self.form.has_key('confirm'): content = StringIO.StringIO() w = content.write w('''

You are about to remove %s This action cannot be undone!
Are you sure?

'''%(desc, cn)) if version: w(''%cv) w('''
''') return self.success(heading='Confirm removal of %s'%desc, content=content.getvalue()) elif self.form['confirm'].value == 'CANCEL': return self.pkg_edit() # ok, do it if self.form.has_key('version'): self.store.remove_release(name, version) self.success(heading='Removed %s'%desc, message='Release removed') else: self.store.remove_package(name) self.success(heading='Removed %s'%desc, message='Package removed') def list_classifiers(self): ''' Just return the list of classifiers. ''' c = '\n'.join(self.store.get_classifiers()) self.handler.send_response(200, 'OK') self.handler.send_header('Content-Type', 'text/plain') self.handler.end_headers() self.wfile.write(c + '\n') # # User handling code (registration, password changing # def user_form(self): ''' Make the user authenticate before viewing the "register" form. ''' if not self.username: raise Unauthorised, 'You must authenticate' self.register_form() def register_form(self): ''' Throw up a form for regstering. ''' info = {'name': '', 'password': '', 'confirm': '', 'email': ''} if self.username: user = self.store.get_user(self.username) info['name'] = '%s'%( urllib.quote(user['name']), cgi.escape(user['name'])) info['email'] = cgi.escape(user['email']) info['action'] = 'Update details' heading = 'User profile' self.nav_current = 'user_form' else: info['action'] = 'Register' info['name'] = '' heading = 'Manual user registration' self.nav_current = 'register_form' content = '''
Username: %(name)s
Password:
Confirm:
Email Address:
 
'''%info if not self.username: content += '''

A confirmation email will be sent to the address you nominate above.

To complete the registration process, visit the link indicated in the email.

''' self.success(content=content, heading=heading) def user(self): ''' Register, update or validate a user. This interface handles one of three cases: 1. new user sending in name, password and email 2. completion of rego with One Time Key 3. updating existing user details for currently authed user ''' info = {} for param in 'name password email otk confirm'.split(): if self.form.has_key(param): v = self.form[param].value.strip() if v: info[param] = v if info.has_key('otk'): if self.username is None: raise Unauthorised # finish off rego if info['otk'] != self.store.get_otk(self.username): response = 'Error: One Time Key invalid' else: # OK, delete the key self.store.delete_otk(info['otk']) response = 'Registration complete' elif self.username is None: for param in 'name password email confirm'.split(): if not info.has_key(param): raise FormError, '%s is required'%param # validate a complete set of stuff # new user, create entry and email otk name = info['name'] if self.store.has_user(name): self.fail('user "%s" already exists'%name, heading='User registration') return if not info.has_key('confirm') or info['password']<>info['confirm']: self.fail("password and confirm don't match", heading='Users') return info['otk'] = self.store.store_user(name, info['password'], info['email']) info['url'] = self.config.url self.send_email(info['email'], rego_message%info) response = 'Registration OK' else: # update details user = self.store.get_user(self.username) password = info.get('password', '').strip() if not password: # no password entered - leave it alone password = None else: # make sure the confirm matches if password != info.get('confirm', ''): self.fail("password and confirm don't match", heading='User profile') return email = info.get('email', user['email']) self.store.store_user(self.username, password, email) response = 'Details updated OK' self.success(message=response, heading='Users') def forgotten_password_form(self): ''' Enable the user to reset their password. ''' self.page_head('Forgotten Password', heading='Request password reset') w = self.wfile.write w('''

You have two options if you have forgotten your password. If you know the email address you registered with, enter it below.

Email Address:
 

Or, if you know your username, then enter it below.

Username:
 

A confirmation email will be sent to you - please follow the instructions within it to complete the reset process.

''') def password_reset(self): """Reset the user's password and send an email to the address given. """ if self.form.has_key('email') and self.form['email'].value.strip(): email = self.form['email'].value.strip() user = self.store.get_user_by_email(email) if user is None: self.fail('email address unknown to me') return pw = ''.join([whrandom.choice(chars) for x in range(10)]) self.store.store_user(user['name'], pw, user['email']) info = {'name': user['name'], 'password': pw, 'email':user['email']} self.send_email(email, password_message%info) self.success(message='Email sent with new password') elif self.form.has_key('name') and self.form['name'].value.strip(): name = self.form['name'].value.strip() user = self.store.get_user(name) if user is None: self.fail('user name unknown to me') return info = {'name': user['name'], 'email':user['email'], 'url': self.config.url} self.send_email(user['email'], password_change_message%info) self.success(message='Email sent to confirm password change') else: self.fail(message='You must supply a username or email address') def send_email(self, recipient, message): ''' Send an administrative email to the recipient ''' smtp = smtplib.SMTP(self.config.mailhost) smtp.sendmail(self.config.adminemail, recipient, message)