=== templates/account.cs
==================================================================
--- templates/account.cs  (revision 2663)
+++ templates/account.cs  (local)
@@ -0,0 +1,48 @@
+<?cs include "header.cs"?>
+<?cs include "macros.cs"?>
+
+<div id="ctxtnav" class="nav"></div>
+
+<div id="content" class="register">
+
+ <h1>My Account</h1>
+
+ <p>
+ Manage your user account.
+ </p>
+
+ <?cs if account.error ?>
+ <div class="system-message">
+  <h2>Error</h2>
+  <p><?cs var:account.error ?></p>
+ </div>
+ <?cs /if ?>
+
+ <?cs if account.message ?>
+ <p><?cs var:account.message ?></p>
+ <?cs /if ?>
+
+ <form method="post" action="">
+  <div>
+   <input type="hidden" name="action" value="change_password" />
+   <label for="password">New Password:</label>
+   <input type="password" id="password" name="password" class="textwidget"
+          size="20" />
+  </div>
+  <div>
+   <label for="password_confirm">Confirm Password:</label>
+   <input type="password" id="password_confirm" name="password_confirm"
+          class="textwidget" size="20" />
+  </div>
+  <input type="submit" value="Change password" />
+ </form>
+
+ <form method="post" action=""
+       onsubmit="return confirm('Are you sure you want to delete your account?');">
+  <input type="hidden" name="action" value="delete" />
+  <input type="submit" value="Delete account" />
+ </form>
+
+</div>
+
+<?cs include:"footer.cs"?>
=== templates/register.cs
==================================================================
--- templates/register.cs  (revision 2663)
+++ templates/register.cs  (local)
@@ -0,0 +1,41 @@
+<?cs include "header.cs"?>
+<?cs include "macros.cs"?>
+
+<div id="ctxtnav" class="nav"></div>
+
+<div id="content" class="register">
+
+ <h1>Register an account</h1>
+
+ <p>
+ Description
+ </p>
+
+ <?cs if registration.error ?>
+ <div class="system-message">
+  <h2>Error</h2>
+  <p><?cs var:registration.error ?></p>
+ </div>
+ <?cs /if ?>
+
+ <form method="post" action="">
+  <div>
+   <input type="hidden" name="action" value="create" />
+   <label for="user">Username:</label>
+   <input type="text" id="user" name="user" class="textwidget" size="20" />
+  </div>
+  <div>
+   <label for="password">Password:</label>
+   <input type="password" id="password" name="password" class="textwidget" size="20" />
+  </div>
+  <div>
+   <label for="password_confirm">Confirm Password:</label>
+   <input type="password" id="password_confirm" name="password_confirm"
+          class="textwidget" size="20" />
+  </div>
+  <input type="submit" value="Create account" />
+ </form>
+
+</div>
+
+<?cs include:"footer.cs"?>
=== trac/util.py
==================================================================
--- trac/util.py  (revision 2663)
+++ trac/util.py  (local)
@@ -375,3 +375,23 @@
                     return '</span>'
                 return '<span class="code-%s">' % mtype
 
+class switch(object):
+    def __init__(self, value):
+        self.value = value
+        self.fall = False
+
+    def __iter__(self):
+        """Return the match method once, then stop"""
+        yield self.match
+        raise StopIteration
+
+    def match(self, *args):
+        """Indicate whether or not to enter a case suite"""
+        if self.fall or not args:
+            return True
+        elif self.value in args: # changed for v1.5, see below
+            self.fall = True
+            return True
+        else:
+            return False
+
=== trac/Account.py
==================================================================
--- trac/Account.py  (revision 2663)
+++ trac/Account.py  (local)
@@ -0,0 +1,310 @@
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2005 Matthew Good <trac@matt-good.net>
+#
+# Trac is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the
+# License, or (at your option) any later version.
+#
+# Trac is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+# Author: Matthew Good <trac@matt-good.net>
+
+from __future__ import generators
+
+import os.path
+import fileinput
+import inspect
+import md5
+from binascii import hexlify
+
+from trac import perm, util
+from trac.util import switch
+from trac.core import *
+from trac.web.chrome import INavigationContributor
+from trac.web.main import IRequestHandler
+from trac.md5crypt import md5crypt
+
+class IPasswordStore(Interface):
+    def config_key(self):
+        '''
+        Returns a string used to identify this implementation in the config.
+        This password storage implementation will be used if the value of
+        the config property "account-management.password_format" matches.
+        '''
+
+    def get_users(self):
+        '''
+        Returns an iterable of the known usernames
+        '''
+
+    def has_user(self, user):
+        '''
+        Returns whether the user account exists.
+        '''
+
+    def set_password(self, user, password):
+        '''
+        Sets the password for the user.  This should create the user account
+        if it doesn't already exist.
+        '''
+
+    def delete_user(self, user):
+        '''
+        Deletes the user account.
+        '''
+
+# os.urandom was added in Python 2.4
+# try to fall back on reading from /dev/urandom on older Python versions
+try:
+    from os import urandom
+except ImportError:
+    def urandom(n):
+        return open('/dev/urandom').read(n)
+
+class AccountModule(Component):
+
+    implements(INavigationContributor, IRequestHandler)
+
+    #INavigationContributor methods
+    def get_active_navigation_item(self, req):
+        return 'account'
+
+    def get_navigation_items(self, req):
+        if not self.is_enabled():
+            return
+        if req.authname != 'anonymous':
+            yield 'metanav', 'account', '<a href="%s">My Account</a>' \
+                  % (self.env.href.account())
+
+    # IRequestHandler methods
+    def match_request(self, req):
+        return self.is_enabled() and req.path_info == '/account'
+
+    def process_request(self, req):
+        if req.authname == 'anonymous':
+            req.redirect(self.env.href.wiki())
+        action = req.args.get('action')
+        if req.method == 'POST':
+            for case in switch(action):
+                if case('change_password'):
+                    self._do_change_password(req)
+                    break
+                elif case('delete'):
+                    self._do_delete(req)
+                    break
+        return 'account.cs', None
+
+    def _do_change_password(self, req):
+        user = req.authname
+        password = req.args.get('password')
+        if not password:
+            req.hdf['account.error'] = 'Password cannot be empty.'
+            return
+
+        if password != req.args.get('password_confirm'):
+            req.hdf['account.error'] = 'The passwords must match.'
+            return
+
+        AccountManager(self.env).set_password(user, password)
+        req.hdf['account.message'] = 'Password successfully updated.'
+
+    def _do_delete(self, req):
+        user = req.authname
+        AccountManager(self.env).delete_user(user)
+        req.redirect(self.env.href.logout())
+
+    def is_enabled(self):
+        return self.config.get('account-management',
+                               'enabled').lower() in util.TRUE
+
+
+class RegistrationModule(Component):
+
+    implements(INavigationContributor, IRequestHandler)
+
+    #INavigationContributor methods
+    def get_active_navigation_item(self, req):
+        return 'register'
+
+    def get_navigation_items(self, req):
+        if not self.is_enabled():
+            return
+        if req.authname == 'anonymous':
+            yield 'metanav', 'register', '<a href="%s">Register</a>' \
+                  % (self.env.href.register())
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        return self.is_enabled() and req.path_info == '/register'
+
+    def process_request(self, req):
+        if req.authname != 'anonymous':
+            req.redirect(self.env.href.account())
+        action = req.args.get('action')
+        if req.method == 'POST' and action == 'create':
+            self._do_create(req)
+        return 'register.cs', None
+
+    def is_enabled(self):
+        return self.config.get('account-management',
+                               'enabled').lower() in util.TRUE and \
+               self.config.get('account-management',
+                               'registration_enabled').lower() in util.TRUE
+
+    def _do_create(self, req):
+        mgr = AccountManager(self.env)
+
+        user = req.args.get('user')
+        if mgr.has_user(user):
+            req.hdf['registration.error'] = \
+                'Another account with that name already exists.'
+            return
+
+        password = req.args.get('password')
+        if not password:
+            req.hdf['registration.error'] = 'Password cannot be empty.'
+            return
+
+        if password != req.args.get('password_confirm'):
+            req.hdf['registration.error'] = 'The passwords must match.'
+            return
+
+        mgr.set_password(user, password)
+        req.redirect(self.env.href.login())
+
+class AccountManager(Component):
+
+    stores = ExtensionPoint(IPasswordStore)
+
+    def __init__(self):
+        self.store_map = {}
+        for s in self.stores:
+            self.store_map[s.config_key()] = s
+
+    def _dispatch(self, func):
+        return getattr(self._get_store(), func)
+
+    def _get_store(self):
+        fmt = self.config.get('account-management', 'password_format')
+        return self.store_map[fmt]
+
+class DispatchProperty(object):
+    def __init__(self, name, fget):
+        self.name = name
+        self.fget = fget
+
+    def __get__(self, obj, objtype=None):
+        if obj is None:
+            return self
+        return self.fget(obj, self.name)
+
+# Add the IPasswordStore methods to the AccountManager to dispatch to the
+# active implementation
+for func, v in inspect.getmembers(IPasswordStore, inspect.ismethod):
+    setattr(AccountManager, func, DispatchProperty(func, AccountManager._dispatch))
+
+class AbstractPasswordFileStore(Component):
+    '''
+    Abstract class to use as a base for Apache's htpasswd and htdigest style
+    password file formats
+    '''
+
+    def has_user(self, user):
+        return user in self.get_users()
+
+    def get_users(self):
+        if not os.path.exists(self._get_filename()):
+            return []
+        return self._get_users(self._get_filename())
+
+    def set_password(self, user, password):
+        self._update_file(self.prefix(user), self.userline(user, password))
+
+    def delete_user(self, user):
+        self._update_file(self.prefix(user), None)
+
+    def _get_filename(self):
+        return self.config.get('account-management', 'password_file')
+
+    def _update_file(self, prefix, userline):
+        filename = self._get_filename()
+        written = False
+        if os.path.exists(filename):
+            for line in fileinput.input(filename, inplace=True):
+                if not line.startswith(prefix):
+                    print line,
+                elif not written and userline:
+                    print userline
+                    written = True
+        if not written and userline:
+            f = open(filename, 'a')
+            print >>f, userline
+            f.close()
+
+
+def salt():
+    s = ''
+    v = long(hexlify(urandom(4)), 16)
+    itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
+    for i in range(8):
+        s += itoa64[v & 0x3f]; v >>= 6
+    return s
+
+
+class HtPasswdStore(AbstractPasswordFileStore):
+
+    implements(IPasswordStore)
+
+    def config_key(self):
+        return 'htpasswd'
+
+    def prefix(self, user):
+        return user + ':'
+
+    def userline(self, user, password):
+        return self.prefix(user) + md5crypt(password, salt(),
+                                            '$apr1$')
+
+    def _get_users(self, filename):
+        f = open(filename)
+        for line in f:
+            user = line.split(':', 1)[0]
+            if user:
+                yield user
+
+
+class HtDigestStore(AbstractPasswordFileStore):
+
+    implements(IPasswordStore)
+
+    def __init__(self):
+        self.realm = self.config.get('account-management', 'htdigest_realm')
+
+    def config_key(self):
+        return 'htdigest'
+
+    def prefix(self, user):
+        return '%s:%s:' % (user, self.realm)
+
+    def userline(self, user, password):
+        p = self.prefix(user)
+        return p + md5.new(p + password).hexdigest()
+
+    def _get_users(self, filename):
+        f = open(filename)
+        for line in f:
+            args = line.split(':')[:2]
+            if len(args) == 2:
+                user, realm = args
+                if realm == self.realm and user:
+                    yield user
+
=== trac/md5crypt.py
==================================================================
--- trac/md5crypt.py  (revision 2663)
+++ trac/md5crypt.py  (local)
@@ -0,0 +1,94 @@
+# Based on FreeBSD src/lib/libcrypt/crypt.c 1.2
+# http://www.freebsd.org/cgi/cvsweb.cgi/~checkout~/src/lib/libcrypt/crypt.c?rev=1.2&content-type=text/plain
+
+# Original license:
+# * "THE BEER-WARE LICENSE" (Revision 42):
+# * <phk@login.dknet.dk> wrote this file.  As long as you retain this notice you
+# * can do whatever you want with this stuff. If we meet some day, and you think
+# * this stuff is worth it, you can buy me a beer in return.   Poul-Henning Kamp
+
+# This port adds no further stipulations.  I forfeit any copyright interest.
+
+import md5
+
+def md5crypt(password, salt, magic='$1$'):
+    # /* The password first, since that is what is most unknown */ /* Then our magic string */ /* Then the raw salt */
+    m = md5.new()
+    m.update(password + magic + salt)
+
+    # /* Then just as many characters of the MD5(pw,salt,pw) */
+    mixin = md5.md5(password + salt + password).digest()
+    for i in range(0, len(password)):
+        m.update(mixin[i % 16])
+
+    # /* Then something really weird... */
+    # Also really broken, as far as I can tell.  -m
+    i = len(password)
+    while i:
+        if i & 1:
+            m.update('\x00')
+        else:
+            m.update(password[0])
+        i >>= 1
+
+    final = m.digest()
+
+    # /* and now, just to make sure things don't run too fast */
+    for i in range(1000):
+        m2 = md5.md5()
+        if i & 1:
+            m2.update(password)
+        else:
+            m2.update(final)
+
+        if i % 3:
+            m2.update(salt)
+
+        if i % 7:
+            m2.update(password)
+
+        if i & 1:
+            m2.update(final)
+        else:
+            m2.update(password)
+
+        final = m2.digest()
+
+    # This is the bit that uses to64() in the original code.
+
+    itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
+
+    rearranged = ''
+    for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)):
+        v = ord(final[a]) << 16 | ord(final[b]) << 8 | ord(final[c])
+        for i in range(4):
+            rearranged += itoa64[v & 0x3f]; v >>= 6
+
+    v = ord(final[11])
+    for i in range(2):
+        rearranged += itoa64[v & 0x3f]; v >>= 6
+
+    return magic + salt + '$' + rearranged
+
+if __name__ == '__main__':
+
+    def test(clear_password, the_hash):
+        magic, salt = the_hash[1:].split('$')[:2]
+        magic = '$' + magic + '$'
+        return md5crypt(clear_password, salt, magic) == the_hash
+
+    test_cases = (
+        (' ', '$1$yiiZbNIH$YiCsHZjcTkYd31wkgW8JF.'),
+        ('pass', '$1$YeNsbWdH$wvOF8JdqsoiLix754LTW90'),
+        ('____fifteen____', '$1$s9lUWACI$Kk1jtIVVdmT01p0z3b/hw1'),
+        ('____sixteen_____', '$1$dL3xbVZI$kkgqhCanLdxODGq14g/tW1'),
+        ('____seventeen____', '$1$NaH5na7J$j7y8Iss0hcRbu3kzoJs5V.'),
+        ('__________thirty-three___________', '$1$HO7Q6vzJ$yGwp2wbL5D7eOVzOmxpsy.'),
+        ('apache', '$apr1$J.w5a/..$IW9y6DR0oO/ADuhlMF5/X1')
+    )
+
+    for clearpw, hashpw in test_cases:
+        if test(clearpw, hashpw):
+            print '%s: pass' % clearpw
+        else:
+            print '%s: FAIL' % clearpw

