summaryrefslogtreecommitdiff
path: root/webui.py
diff options
context:
space:
mode:
Diffstat (limited to 'webui.py')
-rw-r--r--webui.py573
1 files changed, 573 insertions, 0 deletions
diff --git a/webui.py b/webui.py
new file mode 100644
index 0000000..2c767e8
--- /dev/null
+++ b/webui.py
@@ -0,0 +1,573 @@
+
+import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt
+import time, whrandom, smtplib, base64, sha
+
+import store, config
+
+class NotFound(Exception):
+ pass
+class Unauthorised(Exception):
+ pass
+class Redirect(Exception):
+ pass
+class FormError(Exception):
+ pass
+
+rego_message = '''Subject: Complete your 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_message = '''Subject: Password has been reset
+To: %(email)s
+
+Your login is: %(name)s
+Your password is now: %(password)s
+'''
+
+chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
+
+class WebUI:
+ ''' Handle a request as defined by the "env" and "cgi" parameters. Use
+ the "handler" to respond to the user.
+ '''
+ 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)
+
+ 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.handler.send_error(404)
+ except Unauthorised, message:
+ self.handler.send_response(401)
+ self.handler.send_header('Content-Type', 'text/plain')
+ self.handler.send_header('WWW-Authenticate',
+ 'Basic realm="python packages index"')
+ self.handler.end_headers()
+ self.wfile.write(str(message))
+ except Redirect, path:
+ self.handler.send_response(301)
+ self.handler.send_header('Location', path)
+ except FormError, message:
+ self.page_head('Python Packages Index', 'Error processing form',
+ 400)
+ self.wfile.write('<p class="errror">Error processing form:'\
+ ' %s</p>'%message)
+ except:
+ self.page_head('Python Packages Index', 'Error...', 400)
+ self.wfile.write("<pre>")
+ s = StringIO.StringIO()
+ traceback.print_exc(None, s)
+ self.wfile.write(cgi.escape(s.getvalue()))
+ self.wfile.write("</pre>\n")
+ finally:
+ self.store.close()
+
+ 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 = 'index'
+
+ # 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 == 'submit':
+ self.submit()
+ elif action == 'submit_form':
+ self.submit_form()
+ elif action == 'display':
+ self.display()
+ elif action == 'search_form':
+ self.search_form()
+ elif action == 'register_form':
+ self.register_form()
+ elif action == 'user':
+ self.user()
+ elif action == 'password_reset':
+ self.password_reset()
+ elif action == 'index':
+ self.index()
+ elif action == 'role':
+ self.role()
+ elif action == 'role_form':
+ self.role_form()
+ else:
+ raise ValueError, 'Unknown action'
+
+ # commit any database changes
+ self.store.commit()
+
+ def page_head(self, title, heading=None, code=200):
+ ''' Page header
+ '''
+ self.handler.send_response(code)
+ self.handler.send_header('Content-Type', 'text/html')
+ self.handler.end_headers()
+ if heading is None: heading = title
+ self.wfile.write('''
+<html><head><title>%s</title>
+<link rel="stylesheet" type="text/css" href="http://mechanicalcat.net/style.css">
+<link rel="stylesheet" type="text/css" href="http://mechanicalcat.net/page.css">
+</head>
+<body>
+<div id="header"><h1>%s</h1></div>
+<div id="content">
+'''%(title, heading))
+
+ def page_foot(self):
+ self.wfile.write('\n</div>\n</body></html>\n')
+
+ def index(self):
+ ''' Print up an index page
+ '''
+ self.page_head('Python modules index')
+ w = self.wfile.write
+ w('<a href="?:action=search_form">search</a>\n')
+ w('| <a href="?:action=role_form">admin</a>\n')
+ w('| <a href="?:action=submit_form">manual submission</a>\n')
+ w('| <a href="?:action=register_form">register for a login</a>\n')
+ w('<ul>\n')
+ spec = {}
+ for k in self.form.keys():
+ if k.startswith(':'):
+ continue
+ value = self.form[k]
+ k = k.replace('-', '_')
+ if type(value) == type([]):
+ spec[k] = filter(None, [x.value.strip() for x in value])
+ else:
+ spec[k] = filter(None, [value.value.strip()])
+ for name, version in self.store.query_packages(spec):
+ w('<li><a href="?:action=display&name=%s&version=%s">%s %s</a>\n'%(
+ urllib.quote(name), urllib.quote(version), name, version))
+ w('''
+</ul>
+<hr>
+To list your <b>distutils</b>-packaged distribution here:
+<ol>
+<li>Copy <a href="http://mechanicalcat.net/tech/distutils_rego/register.py">register.py</a> to your
+python lib "distutils/command" directory (typically something like
+"/usr/lib/python2.1/distutils/command/").
+<li>Run "setup.py register" as you would normally run "setup.py" for your
+distribution - this will register your distribution's metadata with the
+index.
+<li>... that's <em>it</em>
+</ol>
+<p>In a nutshell, this server is a set of three modules (
+<a href="http://mechanicalcat.net/tech/distutils_rego/config.py">config.py</a>
+, <a
+href="http://mechanicalcat.net/tech/distutils_rego/store.py">store.py</a> and <a
+href="http://mechanicalcat.net/tech/distutils_rego/webui.py">webui.py</a>)
+and one config file (<a
+href="http://mechanicalcat.net/tech/distutils_rego/config.ini">config.ini</a>)
+which sit over a single <a href="http://www.hwaci.com/sw/sqlite/">sqlite</a> (simple, speedy) table. The register command posts the
+metadata and the web interface stores it away. The storage layer handles
+searching, but the web interface doesn't expose it yet :)</p>
+<p>Entries are unique by (name, version) and multiple submissions of the same
+(name, version) result in updates to the existing entry.</p>
+''')
+ self.page_foot()
+
+ def search_form(self):
+ ''' A form used to generate filtered index displays
+ '''
+ self.page_head('Python modules search')
+ self.wfile.write('''
+<form>
+<input type="hidden" name=":action" value="index">
+<table class="form">
+<tr><th>Name:</th>
+ <td><input name="name"></td>
+</tr>
+<tr><th>Version:</th>
+ <td><input name="version"></td>
+</tr>
+<tr><th>Summary:</th>
+ <td><input name="summary"></td>
+</tr>
+<tr><th>Description:</th>
+ <td><input name="description"></td>
+</tr>
+<tr><th>Long Description:</th>
+ <td><input name="long_description"></td>
+</tr>
+<tr><th>Keywords:</th>
+ <td><input name="keywords"></td>
+</tr>
+
+<tr><th>Hidden:</th>
+ <td><select name="hidden">
+ <option value="0">No</option>
+ <option value="1">Yes</option>
+ <option value="">Don't Care</option>
+ </select></td>
+</tr>
+<tr><td>&nbsp;</td><td><input type="submit" value="Search"></td></tr>
+</table>
+</form>
+''')
+ self.page_foot()
+
+ def role_form(self):
+ ''' A form used to maintain user Roles
+ '''
+ 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 = '''
+<tr><th>Package Name:</th>
+ <td><input type="hidden" name="package_name" value="%s">%s</td>
+</tr>
+'''%(urllib.quote(package_name), cgi.escape(package_name))
+ elif not self.store.has_role('Admin', ''):
+ raise Unauthorised
+ else:
+ package = '''
+<tr><th>Package Name:</th>
+ <td><input name="package_name"></td>
+</tr>
+'''
+ # now write the body
+ self.page_head('Python package index', 'Role maintenance')
+ self.wfile.write('''
+<form>
+<input type="hidden" name=":action" value="role">
+<table class="form">
+<tr><th>User Name:</th>
+ <td><input name="user_name"></td>
+</tr>
+%s
+<tr><th>Role to Add:</th>
+ <td><select name="role_name">
+ <option value="Owner">Owner</option>
+ <option value="Maintainer">Maintainer</option>
+ </select></td>
+</tr>
+<tr><td>&nbsp;</td><td><input type="submit" value="Add Role"></td></tr>
+</table>
+</form>
+'''%package)
+ self.page_foot()
+
+ 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'
+
+ # 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"
+ 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)
+
+ self.plain_response('Role Added OK')
+
+ def display(self):
+ ''' Print up an entry
+ '''
+ w = self.wfile.write
+ name = self.form['name'].value
+ version = self.form['version'].value
+ info = self.store.get_package(name, version)
+ self.page_head('Python module: %s %s'%(name, version))
+ un = urllib.quote(name)
+ uv = urllib.quote(version)
+ w('<a href="?:action=index">index</a>\n')
+ w('| <a href="?:action=role_form&package_name=%s">admin</a>\n'%un)
+ w('| <a href="?:action=submit_form&name=%s&version=%s"'
+ '>edit</a><br>'%(un, uv))
+ w('<table class="form">\n')
+ keys = info.keys()
+ keys.sort()
+ for key in keys:
+ value = info[key]
+ if value is None:
+ value = ''
+ label = key.capitalize().replace('_', ' ')
+ if key in ('url', 'home_page') and value != 'UNKNOWN':
+ w('<tr><th nowrap>%s: </th><td><a href="%s">%s</a></td></tr>\n'%(label,
+ value, value))
+ else:
+ w('<tr><th nowrap>%s: </th><td>%s</td></tr>\n'%(label, value))
+ w('\n</table>\n')
+
+ w('<table class="journal">\n')
+ w('<tr><th>Date</th><th>User</th><th>IP Address</th><th>Action</th></tr>\n')
+ for entry in self.store.get_journal(name, version):
+ w('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>\n'%(
+ entry['submitted_date'], entry['submitted_by'],
+ entry['submitted_from'], entry['action']))
+ w('\n</table>\n')
+
+ self.page_foot()
+
+ def submit_form(self):
+ ''' A form used to submit or edit package metadata.
+ '''
+ # are we editing a specific entry?
+ info = {}
+ 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 Unauthorised, 'Not Owner or Maintainer'
+
+ # get the stored info
+ for k,v in self.store.get_package(name, version).items():
+ info[k] = v
+
+ self.page_head('Python package index', 'Manual submission')
+ w = self.wfile.write
+ w('''
+<form>
+<input type="hidden" name=":action" value="submit">
+<input type="hidden" name=":display" value="html">
+<table class="form">
+''')
+
+ # display all the properties
+ for property in 'name version author author_email maintainer maintainer_email home_page license summary description long_description keywords platform download_url 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 == 'hidden':
+ a = value=='1' and ' selected' or ''
+ b = value=='0' and ' selected' or ''
+ field = '''<select name="hidden">
+ <option value="1"%s>Yes</option>
+ <option value="0"%s>No</option>
+ </select>'''%(a,b)
+ elif property.endswith('description'):
+ field = '<textarea name="%s" rows="5" cols="80">%s</textarea>'%(
+ property, value)
+ else:
+ field = '<input size="40" name="%s" value="%s">'%(property, value)
+
+ # now spit out the form line
+ label = property.replace('_', ' ').capitalize()
+ w('<tr><th>%s:</th><td>%s</td></tr>'%(label, field))
+
+ w('''
+<tr><td>&nbsp;</td><td><input type="submit" value="Add Information"></td></tr>
+</table>
+</form>
+''')
+ self.page_foot()
+
+ def submit(self):
+ ''' Handle the submission of distro metadata.
+ '''
+ data = {}
+ for k in self.form.keys():
+ if k.startswith(':'): continue
+ v = self.form[k]
+ if type(v) == type([]):
+ data[k.lower().replace('-','_')]=','.join([x.value for x in v])
+ else:
+ data[k.lower().replace('-','_')] = v.value
+ result = 'plain'
+ if self.form.has_key(':display'):
+ result = self.form[':display'].value
+ try:
+ self.validate_metadata(data)
+ except ValueError, message:
+ if result == 'html':
+ raise FormError, message
+ raise
+ self.store.store_package(data['name'], data['version'], data)
+ self.store.commit()
+ if result == 'html':
+ self.display()
+ else:
+ self.plain_response('Submission OK')
+
+ def validate_metadata(self, data):
+ ''' Validate the contents of the metadata.
+
+ XXX actually _do_ this
+ XXX implement the "validate" :action
+ '''
+ 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']
+
+ def plain_response(self, message):
+ ''' Return a plain-text response to the user.
+ '''
+ self.handler.send_response(200)
+ self.handler.send_header('Content-Type', 'text/plain')
+ self.handler.end_headers()
+ self.wfile.write(message)
+
+ def register_form(self):
+ ''' Throw up a form for regstering.
+ '''
+ self.page_head('Python package index', 'Manual user registration')
+ w = self.wfile.write
+ w('''
+<form>
+<input type="hidden" name=":action" value="user">
+<table class="form">
+<tr><th>Username:</th>
+ <td><input name="name"></td>
+</tr>
+<tr><th>Password:</th>
+ <td><input type="password" name="password"></td>
+</tr>
+<tr><th>Confirm:</th>
+ <td><input type="password" name="confirm"></td>
+</tr>
+<tr><th>Email Address:</th>
+ <td><input name="email"></td>
+</tr>
+<tr><td>&nbsp;</td><td><input type="submit" value="Register"></td></tr>
+</table>
+<p>A confirmation email will be sent to the address you nominate above.</p>
+<p>To complete the registration process, visit the link indicated in the
+email.</p>
+''')
+
+ 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):
+ info[param] = self.form[param].value
+
+ 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:
+ # validate a complete set of stuff
+ # new user, create entry and email otk
+ name = info['name']
+ if self.store.has_user(name):
+ self.plain_response('Error: user "%s" already exists'%name)
+ return
+ if info.has_key('confirm') and info['password'] != info['confirm']:
+ self.plain_response("Error: password and confirm don't match")
+ 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', user['password'])
+ email = info.get('email', user['email'])
+ self.store.store_user(self.username, password, email)
+ response = 'Details updated OK'
+
+ self.plain_response(response)
+
+ def password_reset(self):
+ ''' Reset the user's password and send an email to the address given.
+ '''
+ email = self.form['email'].value
+ user = self.store.get_user_by_email(email)
+ if user is None:
+ response = 'Error: email address unknown to me'
+ else:
+ 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)
+ response = 'Email sent OK'
+ self.plain_response(response)
+
+ 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)
+