fu: Close-up of Fu, bringing a scoop of water to her mouth (Default)
fu ([personal profile] fu) wrote in [site community profile] changelog2010-08-19 07:08 am

[dw-free] Implement renames

[commit: http://hg.dwscoalition.org/dw-free/rev/ba6606b9f6c8]

http://bugs.dwscoalition.org/show_bug.cgi?id=216

Renames for personal journals.

Patch by [personal profile] fu.

Files modified:
  • bin/renameuser.pl
  • bin/upgrading/en.dat
  • bin/upgrading/update-db-general.pl
  • bin/worker/paidstatus
  • cgi-bin/DW/Console/Command/RenameOpts.pm
  • cgi-bin/DW/Controller/Rename.pm
  • cgi-bin/DW/RenameToken.pm
  • cgi-bin/DW/Shop.pm
  • cgi-bin/DW/Shop/Cart.pm
  • cgi-bin/DW/Shop/Item.pm
  • cgi-bin/DW/Shop/Item/Rename.pm
  • cgi-bin/DW/User/Edges/WatchTrust.pm
  • cgi-bin/DW/User/Rename.pm
  • cgi-bin/LJ/Event/SecurityAttributeChanged.pm
  • cgi-bin/LJ/Widget/ShopItemOptions.pm
  • doc/config-local.pl.txt
  • htdocs/admin/userlog.bml
  • htdocs/shop/renames.bml
  • htdocs/shop/renames.bml.text
  • htdocs/stc/rename.css
  • htdocs/stc/shop.css
  • t/rename.t
  • views/rename.tt
  • views/rename.tt.text
  • views/shop/index.tt
  • views/shop/index.tt.text
--------------------------------------------------------------------------------
diff -r 9dafe54beefd -r ba6606b9f6c8 bin/renameuser.pl
--- a/bin/renameuser.pl	Wed Aug 18 17:26:51 2010 -0500
+++ b/bin/renameuser.pl	Thu Aug 19 15:07:56 2010 +0800
@@ -55,7 +55,7 @@ unless ($args{force}) {
         print "Both accounts aren't valid.\n";
         exit 1;
     }
-    unless (lc($acct[0]->raw_email) eq lc($acct[1]->raw_email)) {
+    unless (lc($acct[0]->email_raw) eq lc($acct[1]->email_raw)) {
         print "Email addresses don't match.\n";
         print "   " . $acct[0]->raw_email . "\n";
         print "   " . $acct[1]->raw_email . "\n";
@@ -154,7 +154,8 @@ sub rename_user
     LJ::procnotify_add("rename_user", { 'user' => $u->{'user'},
                                         'userid' => $u->{'userid'} });
 
-    $dbh->do("INSERT INTO renames (renid, token, payid, userid, fromuser, touser, rendate) ".
-             "VALUES (NULL,'[manual]',0,$u->{userid},$qfrom,$qto,NOW())");
+    #$dbh->do("INSERT INTO renames (renid, token, payid, userid, fromuser, touser, rendate) ".
+    #         "VALUES (NULL,'[manual]',0,$u->{userid},$qfrom,$qto,NOW())");
+
     return 1;
 }
diff -r 9dafe54beefd -r ba6606b9f6c8 bin/upgrading/en.dat
--- a/bin/upgrading/en.dat	Wed Aug 18 17:26:51 2010 -0500
+++ b/bin/upgrading/en.dat	Thu Aug 19 15:07:56 2010 +0800
@@ -1796,7 +1796,7 @@ On [[date]] at [[time]] your username wa
 
 If you made this change yourself, this email serves as a confirmation of your change.
 
-If you did not change your username, it means that your journal's security was compromised. Please immediately see FAQ 117 [[http://www.livejournal.com/support/faqbrowse.bml?faqid=117]] for steps to resecure your journal.
+If you did not change your username, it means that your journal's security was compromised. Please contact Support [http://www.dreamwidth.org/support] to learn what steps you should take to resecure your journal.
 
 This letter was sent out automatically to help you keep your account secure. You cannot opt-out of receiving these letters.
 .
@@ -2430,6 +2430,32 @@ protocol.parseerror=Your entry has inval
 protocol.parseerror=Your entry has invalid HTML and cannot be displayed properly. It will be hidden behind a cut on your journal and on other people's read pages until you edit your entry and fix it.
 
 protocol.readonly=Your account is temporarily in read-only mode.  Some operations will fail for a few minutes.
+
+rename.error.invalidaccounttype=Only personal journals can own rename tokens.
+
+rename.error.invalidfrom=Tried to rename an invalid journal.
+
+rename.error.invalidstatusfrom=Cannot rename [[from]]: must be an active journal, not deleted or suspended.
+
+rename.error.invalidstatusto=You cannot rename to [[to]]; it must be either an active journal under your control, or else deleted and purged.
+
+rename.error.invalidto=Username was in an invalid format.
+
+rename.error.isself=Cannot rename back to your own username.
+
+rename.error.noto=No username provided to rename to.
+
+rename.error.reserved="[[to]]" is a reserved username.
+
+rename.error.unauthorized=[[to]] is not under your control.
+
+rename.error.unknown=Cannot rename to "[[to]]".
+
+rename.error.tokenapplied=This token has already been used.
+
+rename.error.tokeninvalid=The provided token is not a valid token.
+
+rename.ex.toomanytries=We had some trouble trying to move aside the account you are trying to rename to ([[tousername]]). Please try again.
 
 s2theme.autogenerated.warning<<
 ## Important note: Hand-edited changes to this layer stand a high risk of 
@@ -3318,6 +3344,41 @@ The [[sitename]] Team
 
 shop.email.processed.subject=[[sitename]] Order Processed
 
+shop.email.renametoken.subject=[[sitename]] Rename Token
+
+shop.email.renametoken.anon.body<<
+Dear [[touser]],
+
+Someone has gifted you with a rename token. You can use this token by visiting the following link:
+
+[[tokenurl]]
+
+Regards,
+The [[sitename]] Team
+.
+
+shop.email.renametoken.explicit.body<<
+Dear [[touser]],
+
+[[fromuser]] has gifted you with a rename token. You can use this token by visiting the following link:
+
+[[tokenurl]]
+
+Regards,
+The [[sitename]] Team
+.
+
+shop.email.renametoken.self.body<<
+Dear [[touser]],
+
+The rename token you bought is now available. You can use this token by visiting the following link:
+
+[[tokenurl]]
+
+Regards,
+The [[sitename]] Team
+.
+
 shop.email.user.anon.body<<
 Dear [[touser]],
 
@@ -3554,6 +3615,12 @@ shop.item.points.canbeadded.outofrange=M
 
 shop.item.points.name=[[num]] [[sitename]] Points
 
+shop.item.rename.canbeadded.invalidjournaltype=You can only send a rename token to a personal journal. If you wish to rename a community journal, please <a [[aopts]]>contact site administrators</a>.
+
+shop.item.rename.name.notoken=[[points]] points for a Rename Token
+
+shop.item.rename.name.hastoken=Rename Token <a [[aopts]]>[[token]]</a>
+
 sitescheme.accountlinks.account=Account
 
 sitescheme.accountlinks.btn.login=Log in
@@ -4869,22 +4936,6 @@ widget.shopcart.paymentmethod.paypal=Pay
 widget.shopcart.paymentmethod.paypal=PayPal Account
 
 widget.shopcart.total=Total:
-
-widget.shopitemgroupdisplay.paidaccounts.header=Buy a Paid Account
-
-widget.shopitemgroupdisplay.paidaccounts.item.circleaccount=<a [[aopts]]>For an account in your Circle</a>
-
-widget.shopitemgroupdisplay.paidaccounts.item.differentaccount=<a [[aopts]]>For a different existing account</a>
-
-widget.shopitemgroupdisplay.paidaccounts.item.existingaccount=<a [[aopts]]>For an existing account</a>
-
-widget.shopitemgroupdisplay.paidaccounts.item.newaccount=<a [[aopts]]>For a new account</a>
-
-widget.shopitemgroupdisplay.paidaccounts.item.randomaccount.noshow=<a [[aopts]]>For an anonymous random active free account</a>
-
-widget.shopitemgroupdisplay.paidaccounts.item.randomaccount.show=<a [[aopts]]>For an identified random active free account</a>
-
-widget.shopitemgroupdisplay.paidaccounts.item.self=<a [[aopts]]>For yourself</a> ([[user]])
 
 widget.shopitemoptions.error.banned=You are restricted from making purchases for this journal.
 
diff -r 9dafe54beefd -r ba6606b9f6c8 bin/upgrading/update-db-general.pl
--- a/bin/upgrading/update-db-general.pl	Wed Aug 18 17:26:51 2010 -0500
+++ b/bin/upgrading/update-db-general.pl	Thu Aug 19 15:07:56 2010 +0800
@@ -3036,6 +3036,20 @@ EOC
 
 # NOTE: new table declarations go ABOVE here ;)
 
+register_tablecreate('renames', <<'EOC');
+CREATE TABLE renames (
+    renid INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+    auth CHAR(13) NOT NULL,
+    cartid INT UNSIGNED,
+    ownerid INT UNSIGNED NOT NULL,
+    renuserid INT UNSIGNED NOT NULL,
+    fromuser CHAR(25),
+    touser CHAR(25),
+    rendate INT UNSIGNED,
+    
+    INDEX (ownerid)
+)
+EOC
 
 ### changes
 
diff -r 9dafe54beefd -r ba6606b9f6c8 bin/worker/paidstatus
--- a/bin/worker/paidstatus	Wed Aug 18 17:26:51 2010 -0500
+++ b/bin/worker/paidstatus	Thu Aug 19 15:07:56 2010 +0800
@@ -316,6 +316,8 @@ sub scan_cart {
     my ( $unapplied, %saw_ids ) = ( 0 );
     $log->( 'Iterating over items.' );
     foreach my $item ( @{ $cart->items } ) {
+        next unless $item->apply_automatically;
+
         $log->( 'Found item [%d] %s.', $item->id, $item->short_desc );
 
         # rare case where we've found the cart generating items with the same
diff -r 9dafe54beefd -r ba6606b9f6c8 cgi-bin/DW/Console/Command/RenameOpts.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Console/Command/RenameOpts.pm	Thu Aug 19 15:07:56 2010 +0800
@@ -0,0 +1,85 @@
+#!/usr/bin/perl
+#
+# DW::Console::Command::RenameOpts
+# This module 
+#
+# Authors:
+#      Afuna <coder.dw@afunamatata.com>
+#
+# Copyright (c) 2010 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself.  For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+package DW::Console::Command::RenameOpts;
+use strict;
+
+use base qw/ LJ::Console::Command /;
+use Carp qw/ croak /;
+
+sub cmd { 'rename_opts' }
+sub desc { 'Manage options attached to a rename.' }
+
+sub args_desc {
+    [
+        'command' => 'Subcommand: redirect, break_redirect, break_redirect_email, del_trusted_by, del_watched_by, del_trusted, del_watched, del_communities.',
+        'username' => 'Username to act on.',
+    ]
+}
+sub usage { 'redirect from_nonexistent_user to_existing_user | break_email_redirect from_user to_user | <subcommand> <username>' }
+sub can_execute {
+    my $remote = LJ::get_remote();
+    return $remote && $remote->has_priv( "siteadmin", "rename" );
+}
+
+sub execute {
+    my ( $self, $cmd, $user, $tousername ) = @_;
+
+    return $self->error( 'Invalid command. Usage: ' . usage() )
+        unless $cmd && $cmd =~ /^(?:redirect|break_redirect|break_email_redirect|del_trusted_by|del_watched_by|del_trusted|del_watched|del_communities)$/;
+
+    if ( $cmd eq 'redirect' ) {
+        # "from" is the user we are creating; "to" is an existing user
+        my $from_user = LJ::canonical_username( $user );
+
+        my $to_u = LJ::load_user( $tousername );
+        return $self->error( 'No destination user provided.' )
+            unless $to_u;
+
+        return $self->error( 'Unable to setup redirection' )
+            unless DW::User::Rename->create_redirect_journal( $from_user, $to_u->user );
+
+    } elsif ( $cmd eq 'break_email_redirect' ) {
+        return $self->error( 'Need to provide the user being redirected from and the user being redirected to' )
+            unless $user && $tousername;
+
+        return $self->error( 'Unable to break the email redirect. Note that from_user must redirect to to_user' )
+            unless DW::User::Rename->break_email_redirection( $user, $tousername );
+            
+    } else {
+        my $u = LJ::load_user( $user );
+        return $self->error( 'Invalid user.' )
+            unless $u;
+
+        if ( $cmd eq 'break_redirect' )  {
+            if ( $u->break_redirects ) {
+                $u->set_deleted;
+            } else {
+                $self->error( "Unable to break redirection" );
+            }
+        }
+        elsif ( $cmd eq 'del_trusted_by' )  { $u->delete_relationships( del_trusted_by => 1 ) }
+        elsif ( $cmd eq 'del_watched_by' )  { $u->delete_relationships( del_watched_by => 1 ) }
+        elsif ( $cmd eq 'del_trusted' )     { $u->delete_relationships( del_trusted => 1 ) }
+        elsif ( $cmd eq 'del_watched' )     { $u->delete_relationships( del_watched => 1 ) }
+        elsif ( $cmd eq 'del_communities' ) { $u->delete_relationships( del_communities => 1 ) }
+    }
+
+    $self->print( 'Done.' );
+
+    return 1;
+}
+
+1;
diff -r 9dafe54beefd -r ba6606b9f6c8 cgi-bin/DW/Controller/Rename.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Controller/Rename.pm	Thu Aug 19 15:07:56 2010 +0800
@@ -0,0 +1,146 @@
+#!/usr/bin/perl
+#
+# DW::Controller::Rename
+#
+# This controller is for renames
+#
+# Authors:
+#      Afuna <coder.dw@afunamatata.com>
+#
+# Copyright (c) 2010 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+package DW::Controller::Rename;
+
+use strict;
+use warnings;
+use DW::Controller;
+use DW::Routing;
+use DW::Template;
+
+use DW::RenameToken;
+use DW::Shop;
+
+# be lax in accepting what goes in the URL in case of typos or mis-copy/paste
+# we validate the token inside and return an appropriate message (instead of 404)
+# ideally, should be: /rename, or /rename/(20 character token)
+DW::Routing->register_regex( qr!^/rename(?:/([A-Z0-9]*))?$!i, \&rename_handler, app => 1 );
+
+sub rename_handler {
+    my $r = DW::Request->get;
+
+    my ( $ok, $rv ) = controller();
+    return $rv unless $ok;
+
+    my $remote = LJ::get_remote();
+
+    return error_ml( 'rename.error.invalidaccounttype' ) unless $remote->is_personal;
+
+    my $vars = {};
+
+    my $given_token = $_[0]->subpatterns->[0];
+    my $token = DW::RenameToken->new( token => $given_token );
+    my $post_args = DW::Request->get->post_args || {};
+    my $get_args = DW::Request->get->get_args || {};
+
+    if ( $r->method eq "POST" ) {
+
+        # this is kind of ugly. Basically, it's a rendered template if it's a success, and a list of errors if it failed
+        my ( $post_ok, $rv ) = handle_post( $token, $post_args );
+        return $rv if $post_ok;
+
+        $vars->{error_list} = $rv;
+    }
+
+    $vars->{invalidtoken} = $given_token
+        if $given_token && ! $token;
+
+    my $rename_to_errors = [];
+    if ( $get_args->{checkuser} ) {
+        $vars->{checkusername} = {
+            user => $get_args->{checkuser},
+            status => $remote->can_rename_to( $get_args->{checkuser}, errref => $rename_to_errors ) ? "available" : "unavailable",
+            errors => $rename_to_errors
+        };
+    }
+
+    if ( $token ) {
+        if ( $token->applied ) {
+            $vars->{usedtoken} = $token->token;
+        } else {
+            $vars->{token} = $token;
+
+            # initialize the form based on previous posts (in case of error) or with some default values
+            $vars->{form} = {
+                from        => $remote->user,
+                journalurl  => $remote->journal_base,
+                token       => $token->token,
+                to          => $post_args->{touser} || $get_args->{to} || "",
+                redirect    => $post_args->{redirect} || "disconnect",
+                rel_options => %$post_args ? { map { $_ => 1 } $post_args->get( "rel_options" ) }
+                                            : { map { $_ => 1 } qw( trusted_by watched_by trusted watched communities ) },
+                others      => %$post_args ? { map { $_ => 1 } $post_args->get( "others" ) }
+                                            : { email => 0 },
+            };
+
+        }
+    }
+
+    if ( ! $token || ( $token && $token->applied ) ) {
+        # grab a list of tokens they can use in case they didn't provide a usable token
+        # assume we always have a remote because our controller is registered as requiring a remote (default behavior)
+        $vars->{unused_tokens} = DW::RenameToken->by_owner_unused( userid => $remote->userid );
+    }
+
+    return DW::Template->render_template( 'rename.tt', $vars );
+}
+
+sub handle_post {
+    my ( $token, $post_args ) = @_;
+
+    # FIXME: replace with official tt-implementation
+    return ( 0, [ LJ::Lang::ml( '/rename.tt.error.invalidform' ) ] ) unless LJ::check_form_auth( $post_args->{lj_form_auth} );
+
+    my $errref = [];
+
+    # the journal we are going to rename; yourself or (eventually) a community you maintain
+    my $journal = LJ::get_remote();
+    push @$errref, LJ::Lang::ml( '/rename.tt.error.nojournal' ) unless $journal;
+
+    my $fromusername = $journal ? $journal->user : "";
+
+    my $tousername = $post_args->{touser};
+    my $redirect_journal = $post_args->{redirect} && $post_args->{redirect} eq "disconnect" ? 0 : 1;
+    push @$errref, LJ::Lang::ml( '/rename.tt.error.noredirectopt' ) unless $post_args->{redirect};
+
+    # since you can't recover deleted relationships, but you can delete the relationships later if something was missed
+    # negate the form submission so we're explicitly stating which rels we want to delete, rather than deleting everything not listed
+    my %keep_rel = map { $_ => 1 } $post_args->get( "rel_options" );
+    my %del_rel = map { +"del_$_" => ! $keep_rel{$_} } qw( trusted_by watched_by trusted watched communities );
+
+    my %other_opts = map { $_ => 1 } $post_args->get( "others" );
+    if ( $other_opts{email} ) {
+        if ( $post_args->{redirect} ne "forward" ) {
+            push @$errref, LJ::Lang::ml( '/rename.tt.error.emailnotforward', { emaildomain => "\@$LJ::USER_DOMAIN" } );
+            $other_opts{email} = 0;
+        } 
+
+        unless ( $LJ::USER_EMAIL && $journal->can_have_email_alias ) {
+            push @$errref, LJ::Lang::ml( '/rename.tt.error.emailnoalias' );
+            $other_opts{email} = 0;
+        }
+    }
+
+    # try the rename and see if there are any errors
+    $journal->rename( $tousername, token => $token, redirect => $redirect_journal, redirect_email => $other_opts{email}, %del_rel, errref => $errref );
+
+    return ( 1, success_ml( "/rename.tt.success", { from => $fromusername, to => $journal->user } ) ) unless @$errref;
+
+    # return the list of errors, because we want to print out other things as well...
+    return ( 0, $errref );
+}
+1;
diff -r 9dafe54beefd -r ba6606b9f6c8 cgi-bin/DW/RenameToken.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/RenameToken.pm	Thu Aug 19 15:07:56 2010 +0800
@@ -0,0 +1,364 @@
+#!/usr/bin/perl
+#
+# DW::RenameToken - Token which can be applied to a journal to change the username.
+#
+# Authors:
+#      Afuna <coder.dw@afunamatata.com>
+#
+# Copyright (c) 2010 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+
+package DW::RenameToken;
+
+=head1 NAME
+
+DW::RenameToken - Token which can be applied to a journal to change the username.
+
+=head1 SYNOPSIS
+
+  use DW::Rename;
+
+  # create:
+  # return a DW::RenameToken object
+  my $new_token_obj = DW::RenameToken->create_token( ownerid => $u->id, cartid => $cart->id );
+
+  # convenience method which returns the string representation of the token. Same as $token_obj->token
+  my $new_token_string = DW::RenameToken->create( ownerid => $u->id, cartid => $cart->id );
+
+  # special token for internal use
+  my $internal_token = DW::RenameToken->create_token( systemtoken => 1 );
+
+
+  # try to use...
+  my $token_obj = DW::RenameToken->new( token => $POST{token} );
+  if ( $token_obj->applied ) { print "Already used" }
+  else { $token_obj->apply( userid => $id_of_the_journal_being_renamed, from => $oldname, to => $newname ) }
+
+=cut
+
+use strict;
+use warnings;
+
+use DW::Shop::Cart;
+
+use fields qw(renid auth cartid ownerid renuserid fromuser touser rendate);
+
+use constant { AUTH_LEN => 13, ID_LEN => 7 };
+use constant DIGITS => qw(A B C D E F G H J K L M N P Q R S T U V W X Y Z 2 3 4 5 6 7 8 9);
+use constant { TOKEN_LEN => AUTH_LEN + ID_LEN, DIGITS_LEN => scalar( DIGITS ) };
+
+=head1 API
+
+=head2 C<< $class->create_token >>
+
+Create a new rename token and return the DW::RenameToken object.
+
+=head2 C<< $class->create >>
+
+Create a new rename token and return the string token representation of the rename token
+
+Args
+=item ownerid => id of the user who gets to use the rename token
+=item cartid => id of the cart where this rename token was bought
+=item systemtoken => whether this token is owned by the system instead of a user. Used for automatically generated tokens -- manual renames, moving aside a user to ex_* etc. When this is on, the ownerid is ignored.
+=cut
+
+sub create_token {
+    my ( $class, %opts ) = @_;
+
+    my $dbh = LJ::get_db_writer()
+        or die "Unable to connect to database.\n";
+
+    my $sth = $dbh->prepare(
+        q{INSERT INTO renames (renid, auth, cartid, ownerid)
+          VALUES (NULL, ?, ?, ?)}
+    )
+        or die "Unable to allocate statement handle.\n";
+
+    my $uid = $opts{systemtoken} ? 0 : $opts{ownerid};
+    my $cartid = $opts{cartid};
+    my $authcode = LJ::make_auth_code( AUTH_LEN );
+
+    $sth->execute( $authcode, $cartid, $uid );
+    die "Unable to create rename token: " . $dbh->errstr . "\n"
+        if $dbh->err;
+
+    return bless( {
+        renid   => $dbh->{mysql_insertid},
+        auth    => $authcode,
+        cartid  => $cartid,
+        ownerid => $uid,
+    }, "DW::RenameToken" );
+}
+
+sub create {
+    my ( $class, %opts ) = @_;
+    return $class->create_token( %opts )->token;
+}
+
+=head2 C<< $class->valid_format( string => tokentovalidate ) >>
+
+Verifies if this could be a valid format for the rename token. Checks length and characters.
+
+=cut
+sub valid_format {
+    my ( $class, %opts ) = @_;
+
+    my $string = uc $opts{string};
+    return 0 unless length $string == TOKEN_LEN;
+
+    my %valid_digits = map { $_ => 1 } DIGITS;
+    my @string_array = split( //, $string );
+    foreach my $char ( @string_array ) {
+        return 0 unless $valid_digits{$char};
+    }
+
+    return 1;
+}
+
+=head2 C<< $class->new >>
+
+Returns object for rename token, given the token string, or undef if none exists.
+
+=item userid => userid of the journal being renamed
+=item from   => old username
+=item to     => new username
+=cut
+
+sub new {
+    my ( $class, %opts ) = @_;
+    my $dbr = LJ::get_db_reader();
+
+    return undef unless $class->valid_format( string => $opts{token} );
+
+    my ( $id, $auth ) = $class->decode( $opts{token} );
+    my $renametoken = $dbr->selectrow_hashref( "SELECT renid, auth, cartid, ownerid, renuserid, fromuser, touser, rendate FROM renames ".
+                                      "WHERE renid=? AND auth=?",
+                                      undef, $id, $auth);
+
+    return undef unless defined $renametoken;
+
+    my $ret = fields::new( $class );
+    while ( my ( $k, $v ) = each %$renametoken ) {
+        $ret->{$k} = $v;
+    }
+
+    return $ret;
+
+}
+
+=head2 C<< $class->by_owner_unused( userid => ownerid ) >>
+
+Return a list of unused tokens for this user.
+
+=cut
+sub by_owner_unused {
+    my ( $class, %opts ) = @_;
+
+    my $userid = $opts{userid} + 0;
+    return unless $userid;
+
+    my $dbr = LJ::get_db_reader();
+
+    my $sth = $dbr->prepare( "SELECT renid, auth, cartid, ownerid, renuserid, fromuser, touser, rendate FROM renames " .
+                             "WHERE ownerid=? AND renuserid=0" )
+        or die "Unable to retrieve list of unused rename tokens: " . $dbr->errstr;
+
+    $sth->execute( $userid )
+        or die "Unable to retrieve list of unused rename tokens: " . $sth->errstr;
+
+    my @tokens;
+
+    while (my $token = $sth->fetchrow_hashref) {
+        my $ret = fields::new( $class );
+        while (my ($k, $v) = each %$token) {
+            $ret->{$k} = $v;
+        }
+        push @tokens, $ret;
+    }
+
+    return @tokens ? [ @tokens ] : undef;
+}
+
+=head2 C<< $class->_encode( $id, $auth ) >>
+
+Internal. Given a rename token id and a 13-digit auth code, returns a 20-digit
+all-uppercase rename token.
+
+=cut
+
+sub _encode {
+    my ( $class, $id, $auth ) = @_;
+    return uc( $auth ) . $class->_id_encode( $id );
+}
+
+=head2 C<< $class->decode( $invite ) >>
+
+Internal. Given a rename token, break it down into its component parts: a rename token id and a 13-character auth code.
+
+=cut
+
+sub decode {
+    my ( $class, $token ) = @_;
+    return ( $class->_id_decode( substr( $token, AUTH_LEN, ID_LEN ) ), uc( substr( $token, 0, AUTH_LEN ) ) );
+}
+
+=head2 C<< $class->_id_encode( $num ) >>
+
+Internal. Converts a 32-bit unsigned integer into a fixed-width string
+representation in base DIGITS_LEN, based on an alphabet of letters and numbers
+that are not easily mistaken for each other.
+
+=cut
+
+sub _id_encode {
+    my ( $class, $num ) = @_;
+    my $id = "";
+    while ( $num ) {
+        my $dig = $num % DIGITS_LEN;
+        $id = (DIGITS)[$dig] . $id;
+        $num = ($num - $dig) / DIGITS_LEN;
+    }
+    return ( (DIGITS)[0] x ( ID_LEN - length( $id ) ) . $id );
+}
+
+my %val;
+@val{(DIGITS)} = 0..DIGITS_LEN;
+
+=head2 C<< $class->_id_decode( $id ) >>
+
+Internal. Given an id encoding from C<DW::RenameToken::_id_encode>, returns
+the original decimal number.
+
+=cut
+
+sub _id_decode {
+    my ($class, $id) = @_;
+    $id = uc( $id );
+
+    my $num = 0;
+    my $place = 0;
+    foreach my $d ( split //, $id ) {
+        return 0 unless exists $val{$d};
+        $num = $num * DIGITS_LEN + $val{$d};
+    }
+    return $num;
+}
+
+
+=head2 C<< $self->apply( %opts ) >>
+
+Record information about how this rename token was applied.
+
+=cut
+
+sub apply {
+    my ( $self, %opts ) = @_;
+
+    # modify self
+    my $dbh = LJ::get_db_writer();
+    $dbh->do( "UPDATE renames SET renuserid=?, fromuser=?, touser=?, rendate=NOW() WHERE renid=?",
+        undef, $opts{userid}, $opts{from}, $opts{to}, $self->id );
+
+    # modify status in the cart
+    if ( $self->cartid ) {
+        my $cart = DW::Shop::Cart->get_from_cartid( $self->cartid );
+        foreach my $item ( @{ $cart->items } ) {
+            next unless $item->isa( "DW::Shop::Item::Rename" ) && $item->token eq $self->token;
+            $item->apply;
+        }
+
+        $cart->save;
+    }
+
+    return 1;
+}
+
+# accessors
+=head2 C<< $self->token >>
+
+The string representation of the token (formed by a combination of the auth code and the id)
+
+=head2 C<< $self->applied >>
+
+Whether this token has been used.
+
+=head2 C<< $self->auth >>
+
+The auth code, randomly generated characters. Not necesarily unique.
+
+=head2 C<< $self->id >>
+
+Unique id for the rename token.
+
+=head2 C<< $self->cartid( [ $cartid ] ) >>
+
+Gets / sets cart where we can look up payment information. May be 0, if the rename token did not pass through the payment system.
+
+=head2 C<< $self->ownerid >>
+
+Owner of the rename token; the one who actually did the applying.  May be different from the user who owns/bought the rename token in case  of gifts, or of renaming of communities, or a system admin doing the rename
+
+=head2 C<< $self->renuserid >>
+
+User id that the rename token was applied to.
+
+=head2 C<< $self->fromuser >>
+
+Original username.
+
+=head2 C<< $self->touser >>
+
+New username.
+
+=head2 C<< $self->rendate >>
+
+UNIX timestamp the token was used.
+
+=cut
+
+sub token {
+    my $self = $_[0];
+
+    # _encode is a class method
+    return (ref $self)->_encode( $self->{renid}, $self->{auth} );
+}
+
+sub applied {
+    my $self = $_[0];
+    return $self->{renuserid} ? 1 : 0;
+}
+
+sub cartid {
+    return $_[0]->{cartid} unless defined $_[1];
+    return $_[0]->{cartid} = $_[1];
+}
+
+sub auth { return $_[0]->{auth} }
+sub id { return $_[0]->{renid} }
+sub ownerid { return $_[0]->{ownerid} }
+sub renuserid { return $_[0]->{renuserid} }
+sub fromuser { return $_[0]->{fromuser} }
+sub touser { return $_[0]->{touser} }
+sub rendate { return $_[0]->{rendate} }
+
+=head1 BUGS
+
+=head1 AUTHORS
+
+Afuna <coder.dw@afunamatata.com>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (c) 2010 by Dreamwidth Studios, LLC.
+
+This program is free software; you may redistribute it and/or modify it under
+the same terms as Perl itself. For a copy of the license, please reference
+'perldoc perlartistic' or 'perldoc perlgpl'.
+
+=cut
+
+1;
diff -r 9dafe54beefd -r ba6606b9f6c8 cgi-bin/DW/Shop.pm
--- a/cgi-bin/DW/Shop.pm	Wed Aug 18 17:26:51 2010 -0500
+++ b/cgi-bin/DW/Shop.pm	Thu Aug 19 15:07:56 2010 +0800
@@ -24,6 +24,7 @@ use DW::Shop::Engine;
 use DW::Shop::Engine;
 use DW::Shop::Item::Account;
 use DW::Shop::Item::Points;
+use DW::Shop::Item::Rename;
 
 # constants across the site
 our $MIN_ORDER_COST = 3.00; # cost in USD minimum.  this only comes into affect if
diff -r 9dafe54beefd -r ba6606b9f6c8 cgi-bin/DW/Shop/Cart.pm
--- a/cgi-bin/DW/Shop/Cart.pm	Wed Aug 18 17:26:51 2010 -0500
+++ b/cgi-bin/DW/Shop/Cart.pm	Thu Aug 19 15:07:56 2010 +0800
@@ -434,9 +434,12 @@ sub get_item {
 # get/set state
 sub state {
     my ( $self, $newstate ) = @_;
+    return $self->{state} unless defined $newstate;
+    return $self->{state} if $self->{state} == $newstate;
 
-    return $self->{state}
-        unless defined $newstate;
+    # alert the items that the cart's state has changed, this allows items to do things
+    # that happen when the state changes.
+    $_->cart_state_changed( $newstate ) foreach @{$self->items};
 
     LJ::Hooks::run_hooks( 'shop_cart_state_change', $self, $newstate );
 
diff -r 9dafe54beefd -r ba6606b9f6c8 cgi-bin/DW/Shop/Item.pm
--- a/cgi-bin/DW/Shop/Item.pm	Wed Aug 18 17:26:51 2010 -0500
+++ b/cgi-bin/DW/Shop/Item.pm	Thu Aug 19 15:07:56 2010 +0800
@@ -108,6 +108,17 @@ sub new {
     }, $class;
 }
 
+
+=head2 C<< $self->apply_automatically >>
+
+True if you want the item to be applied via the paidstatus worker, and false 
+if you wish to apply the item yourself (usually triggered by a user action).
+
+Subclasses may override.
+
+=cut
+
+sub apply_automatically { 1 }
 
 =head2 C<< $self->apply >>
 
diff -r 9dafe54beefd -r ba6606b9f6c8 cgi-bin/DW/Shop/Item/Rename.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Shop/Item/Rename.pm	Thu Aug 19 15:07:56 2010 +0800
@@ -0,0 +1,144 @@
+#!/usr/bin/perl
+#
+# DW::Shop::Item::Rename
+#
+# Represents a rename token that someone is purchasing.
+#
+# Authors:
+#      Afuna <coder.dw@afunamatata.com>
+#
+# Copyright (c) 2010 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself.  For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+package DW::Shop::Item::Rename;
+
+use base 'DW::Shop::Item';
+
+use strict;
+use DW::RenameToken;
+use DW::User::Rename;
+use DW::Shop::Cart;
+
+=head1 NAME
+
+DW::Shop::Item::Rename - Represents a rename token that someone is purchasing. See 
+the documentation for DW::Shop::Item for usage examples and description of methods
+inherited from that base class.
+
+=head1 API
+
+=cut 
+
+=head2 C<< $class->new( [ %args ] ) >>
+
+Instantiates a rename token to be purchased.
+
+=cut
+sub new {
+    my ( $class, %args ) = @_;
+
+    # must have been sent to a user
+    return undef unless $args{target_userid};
+
+    my $self = $class->SUPER::new( %args, type => "rename" );
+    return undef unless $self;
+
+    return $self;
+}
+
+
+# override
+sub name_html {
+    return $_[0]->token && $_[0]->from_userid == $_[0]->t_userid
+        ? LJ::Lang::ml( 'shop.item.rename.name.hastoken', { token => $_[0]->token, aopts => 'href="/rename/' . $_[0]->token . '"' } )
+        : LJ::Lang::ml( 'shop.item.rename.name.notoken', { points => $_[0]->cost_points } );
+}
+
+# override
+sub apply_automatically { 0 }
+
+# override
+sub _apply {
+    my ( $self, %opts ) = @_;
+
+    # very simple (the actual logic for applying is in the rename token object)
+    $self->{applied} = 1;
+
+    return 1;
+}
+
+
+# override
+sub can_be_added {
+    my ( $self, %opts ) = @_;
+
+    my $errref = $opts{errref};
+    my $target_u = LJ::load_userid( $self->t_userid );
+
+    # the receiving user must be a personal journal
+    if ( LJ::isu( $target_u ) && ! $target_u->is_personal ) {
+        $$errref = LJ::Lang::ml( 'shop.item.rename.canbeadded.invalidjournaltype' );
+        return 0;
+    }
+
+    return 1;
+}
+
+# override
+sub cart_state_changed {
+    my ( $self, $newstate ) = @_;
+
+    # create a new rename token once the cart has been paid for
+    # but only do so if we haven't created one before (just checking in case we manage to set the cart to 
+    #    paid status multiple times -- but that had better not happen!)
+    if ( $newstate == $DW::Shop::STATE_PAID  && ! $self->{token} ) {
+        my $token = DW::RenameToken->create( ownerid => $self->t_userid, cartid => $self->cartid );
+        return undef unless $token;
+
+        $self->{token} = $token;
+
+        # now let's tell the user about this token
+        my $fu = LJ::load_userid( $self->from_userid );
+        my $u = LJ::load_userid( $self->t_userid )
+            or return 0;
+
+        my $from;
+        my $vars = {
+            sitename => $LJ::SITENAME,
+            touser   => $u->user,
+            tokenurl => "$LJ::SITEROOT/rename/$token",
+        };
+
+        if ( $u->equals( $fu ) ) {
+            $from = "self";
+        } elsif ( $fu ) {
+            $from = "explicit";
+            $vars->{fromuser} = $fu->user;
+        } else {
+            $from = "anon";
+        }
+
+        LJ::send_mail( {
+            to => $u->email_raw,
+            from => $LJ::ACCOUNTS_EMAIL,
+            fromname => $LJ::SITENAME,
+            subject => LJ::Lang::ml( 'shop.email.renametoken.subject', { sitename => $LJ::SITENAME } ),
+            body => LJ::Lang::ml( "shop.email.renametoken.$from.body", $vars ),
+        } );
+    }
+}
+
+
+=head2 C<< $self->token >>
+
+Returns the usable encoded representation of the rename token.
+
+=cut
+
+sub token { return $_[0]->{token} }
+
+1;
diff -r 9dafe54beefd -r ba6606b9f6c8 cgi-bin/DW/User/Edges/WatchTrust.pm
--- a/cgi-bin/DW/User/Edges/WatchTrust.pm	Wed Aug 18 17:26:51 2010 -0500
+++ b/cgi-bin/DW/User/Edges/WatchTrust.pm	Thu Aug 19 15:07:56 2010 +0800
@@ -438,6 +438,27 @@ sub circle_users {
 *LJ::User::circle_users = \&circle_users;
 
 
+# return users who trust you
+sub trusted_by_users {
+    my $u = shift;
+    my @trustedbyids = $u->trusted_by_userids;
+    my $users = LJ::load_userids(@trustedbyids);
+    return values %$users if wantarray;
+    return $users;
+}
+*LJ::User::trusted_by_users = \&trusted_by_users;
+
+
+# return users who watch you
+sub watched_by_users {
+    my $u = shift;
+    my @watchedbyids = $u->watched_by_userids;
+    my $users = LJ::load_userids(@watchedbyids);
+    return values %$users if wantarray;
+    return $users;
+}
+*LJ::User::watched_by_users = \&watched_by_users;
+
 # returns array of trusted by uids.  by default, limited at 50,000 items.
 sub trusted_by_userids {
     my ( $u, %args ) = @_;
diff -r 9dafe54beefd -r ba6606b9f6c8 cgi-bin/DW/User/Rename.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/User/Rename.pm	Thu Aug 19 15:07:56 2010 +0800
@@ -0,0 +1,468 @@
+#!/usr/bin/perl
+#
+# DW::User::Rename - Contains logic to handle account renaming.
+#
+# Authors:
+#      Afuna <coder.dw@afunamatata.com>
+#
+# Copyright (c) 2010 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+
+package DW::User::Rename;
+
+=head1 NAME
+
+DW::User::Rename - Contains logic to handle account renaming. Based on bin/renameuser.pl, from the LiveJournal code
+
+=head1 SYNOPSIS
+
+  use DW::User::Rename;
+
+  # on a user object
+  my $u = LJ::load_user( "exampleusername" );
+  if ( $u->can_rename_to( "to_username" ) ) {
+    # print message, whatever...
+
+    # do rename
+    $u->rename( "to_username", token => $token_object );
+
+    # this user object retains old name
+    # but all caches should have been cleared after the rename, so you can get
+    # an updated copy of the user when you do LJ::load_userid
+    $u = LJ::load_userid( $u->userid );
+  }
+
+  my $user_a = LJ::load_user( "swap_a" );
+  my $user_b = LJ::load_user( "swap_b" );
+  $user_a->swap_usernames( $user_b ) if $user_a->can_rename_to( $user_b->user );
+
+  # can also force a rename, which doesn't take into consideration any of the 
+  # safeguards. Only call this from an admin page:
+  $u->rename( "to_username", token => $token, force => 1 )
+=cut
+
+use strict;
+use warnings;
+
+use DW::RenameToken;
+
+=head1 API
+
+=head2 C<< $self->can_rename_to( $tousername [, %opts ] ) >>
+
+Return true if this user can be renamed to the given username
+
+=cut
+sub can_rename_to {
+    my ( $self, $tousername, %opts ) = @_;
+
+    my $errref = $opts{errref} || [];
+
+    unless ( $tousername ) {
+        push @$errref, LJ::Lang::ml( 'rename.error.noto' );
+        return 0;
+    }
+
+    # make sure both from and to are present and, the to is a valid username form
+    $tousername = LJ::canonical_username( $tousername );
+    unless ( $tousername ) {
+        push @$errref, LJ::Lang::ml( 'rename.error.invalidto' );
+        return 0;
+    }
+
+    unless ( LJ::isu( $self ) ) {
+        push @$errref, LJ::Lang::ml( 'rename.error.invalidfrom' );
+        return 0;
+    }
+
+    # make sure we don't try to rename to ourself
+    if ( $self->user eq $tousername ) {
+        push @$errref, LJ::Lang::ml( 'rename.error.isself' );
+        return 0;
+    }
+
+    # force, but only if to and from are valid
+    return 1 if $opts{force};
+
+    # can't rename to a reserved username 
+    if ( LJ::User->is_protected_username( $tousername ) ) {
+        push @$errref, LJ::Lang::ml( 'rename.error.reserved', { to => LJ::ehtml( $tousername ) } );
+        return 0;
+    }
+
+    # suspended journals can't be renamed. So can't these other ones.
+    if ( $self->is_suspended || $self->is_readonly || $self->is_locked || $self->is_memorial || $self->is_renamed ) {
+        push @$errref, LJ::Lang::ml( 'rename.error.invalidstatusfrom', { from => $self->ljuser_display } );
+        return 0;
+    }
+
+    # only personal accounts can be renamed
+    if ( $self->is_personal ) {
+
+        # able to rename to unregistered accounts
+        my $tou = LJ::load_user( $tousername );
+        return 1 unless $tou;
+
+        # some journals can not be renamed to
+        if ( $tou->is_suspended || $tou->is_readonly || $tou->is_locked || $tou->is_memorial || $tou->is_renamed ) {
+            push @$errref, LJ::Lang::ml( 'rename.error.invalidstatusto', { to => $self->ljuser_display } );
+            return 0;
+        }
+
+        # expunged users can always be renamed to
+        return 1 if $tou->is_expunged;
+
+
+        # deleted and visible journals have extra safeguards:
+        # person-to-person
+        return 1 if DW::User::Rename::_are_same_person( $self, $tou );
+
+        push @$errref, LJ::Lang::ml( 'rename.error.unauthorized', { to => $tou->ljuser_display } );
+        return 0;
+    }
+
+    # be strict in what we accept
+    push @$errref, LJ::Lang::ml( 'rename.error.unknown', { to => LJ::ehtml( $tousername ) } );
+    return 0;
+}
+
+=head2 C<< $self->rename( $tousername, token => $rename_token_obj [, %opts] ) >>
+
+Rename the given user to the provided username. Requires a user name to rename to, and a token object to store the rename action data. If the username we're returning to is of an existing user then it shall be moved aside to a username of the form "ex_oldusernam123". Returns 1 on success, 0 on failure
+
+Optional arguments are:
+=item force     => bool, default false
+=item redirect  => bool, default false
+=item errref    => array ref of errors
+=item del_watched_by/del_trusted_by/del_trusted/del_watched/del_communities => bool, default false
+=item redirect_email => bool, default false (also forced to false if redirect is false)
+
+=cut
+
+sub rename {
+    my ( $self, $tousername, %opts ) = @_;
+
+    my $errref = $opts{errref} || [];
+
+    push @$errref, LJ::Lang::ml( 'rename.error.tokeninvalid' ) unless $opts{token} && $opts{token}->isa( "DW::RenameToken" )
+            && $opts{token}->ownerid == $self->userid;
+    push @$errref, LJ::Lang::ml( 'rename.error.tokenapplied' ) if $opts{token} && $opts{token}->applied;
+
+    my $can_rename_to = $self->can_rename_to( $tousername, %opts );
+
+    return 0 if @$errref || ! $can_rename_to;
+
+    $tousername = LJ::canonical_username( $tousername );
+    if ( my $tou = LJ::load_user( $tousername ) ) {
+        return 0 unless DW::User::Rename::_rename_to_ex( $tou, errref => $opts{errref} );
+    }
+
+    return DW::User::Rename::_rename( $self, $tousername, %opts );
+}
+
+=head2 C<< $self->swap_usernames( $touser [, %opts ] ) >>
+
+Swap the usernames of these two users. Currently unimplemented.
+
+=cut
+
+sub swap_usernames {
+    my ( $self, $touser, %opts ) = @_;
+}
+
+=head2 C<< $self->_clear_from_cache >>
+
+Internal function to clear a user from various caches.
+
+=cut
+
+sub _clear_from_cache {
+    my ( $self, $fromusername, $tousername ) = @_;
+
+    # $fromusername should be the same as $self->user, but we use the passed in value
+    # to be safe, since $self has been renamed at this point.
+    LJ::MemCache::delete( "uidof:$fromusername" );
+    LJ::MemCache::delete( "uidof:$tousername" );
+    LJ::memcache_kill( $self->userid, "userid" );
+
+    delete $LJ::CACHE_USERNAME{$self->userid};
+    delete $LJ::REQ_CACHE_USER_NAME{$fromusername};
+    delete $LJ::REQ_CACHE_USER_ID{$self->userid};
+}
+
+=head2 C<< $self->_are_same_person >>
+
+Internal function to determine whether two personal accounts are controlled by the same person
+
+=cut
+sub _are_same_person {
+    my ( $p1, $p2 ) = @_;
+
+    return 0 unless $p1->is_person && $p2->is_person;
+
+    # able to rename to registered accounts, where both accounts can be identified as the same person
+    # may be able to do this more elegantly once we are able to associate accounts
+    # right now: two valid accounts, same email address, same password, and at least one must be validated
+    return 0 unless lc( $p1->email_raw ) eq lc( $p2->email_raw );
+    return 0 unless $p1->password eq $p2->password;
+    return 0 unless $p1->is_validated || $p2->is_validated;
+
+    return 1;
+}
+
+=head2 C<< $self->_rename( $tousername, %opts ) >>
+
+Internal function to do renames. Low-level, no error-checking on inputs. Only call
+this when you are sure that all conditions for a rename are satisfied. Returns 1 on 
+success, 0 on failure.
+
+=cut
+
+sub _rename {
+    my ( $self, $tousername, %opts ) = @_;
+
+    my $errref = $opts{errref} || [];
+    my $token = $opts{token};
+
+    my $fromusername = $self->user;
+
+    my $dbh = LJ::get_db_writer() or die "Could not get DB handle";
+
+    # FIXME: transactions possible?
+    foreach my $table ( qw( user useridmap ) )
+    {
+        $dbh->do( "UPDATE $table SET user=? WHERE user=?", 
+            undef, $tousername, $fromusername );
+
+        if ( $dbh->err ) {
+            push @$errref, $dbh->errstr;
+            return 0;
+        }
+    }
+
+    # invalidate
+    DW::User::Rename::_clear_from_cache( $self, $fromusername, $tousername );
+
+    # tell everything else that we renamed
+    LJ::procnotify_add( "rename_user", { user => $fromusername,
+                                         userid => $self->userid });
+
+    $token->apply( userid => $self->userid, from => $fromusername, to => $tousername );
+
+    $self->break_redirects;
+    DW::User::Rename->create_redirect_journal( $fromusername, $tousername ) if $opts{redirect};
+    my $del = $self->delete_relationships( del_trusted_by => $opts{del_trusted_by}, del_watched_by => $opts{del_watched_by}, del_trusted => $opts{del_trusted}, del_watched => $opts{del_watched}, del_communities => $opts{del_communities} );
+
+    # this deletes the email under the old username
+    DW::User::Rename->break_email_redirection( $fromusername, $tousername ) unless $opts{redirect} && $opts{redirect_email};
+
+    # update current object to new username, and update the email under the new username
+    $self->{user} = $tousername;
+    $self->update_email_alias;
+
+    my @redir;
+    push @redir, "J" if $opts{redirect};
+    push @redir, "E" if $opts{redirect} && $opts{redirect_email};
+
+    my $remote = LJ::isu( $opts{user} ) ? $opts{user} : $self;
+    $self->log_event( 'rename', { from => $fromusername, to => $tousername, remote => $remote, del => $del, redir => join( ":", @redir ) } );
+    
+    # infohistory
+    LJ::infohistory_add( $self, "username", $fromusername );
+
+    # notification
+    LJ::Event::SecurityAttributeChanged->new( $self, { 
+        action   => 'account_renamed',
+        ip       => eval { BML::get_remote_ip() } || "[unknown]",
+        old_username => $fromusername,
+    })->fire;
+
+    return 1;
+}
+
+=head2 C<< $self->break_redirects >>
+
+Break outgoing redirects.
+
+=cut
+sub break_redirects {
+    my $self = $_[0];
+
+    if ( my $renamedto = $self->prop( "renamedto" ) ) {
+        $self->set_prop( renamedto => undef );
+        $self->log_event( 'redirect', { renamedto => $renamedto, action => 'remove' } );
+    }
+}
+
+=head2 C<< DW::User::Rename->create_redirect_journal >>
+
+Set up a new user which will redirect to an existing one. Don't allow to set redirects for existing users.
+
+=cut
+sub create_redirect_journal {
+    my ( $class, $fromusername, $tousername ) = @_;
+
+    # we can only create a redirect journal for a nonexistent, a purged user, or a redirecting user
+    my $fromu = LJ::load_user( $fromusername ); 
+    return 0 if $fromu && ! ( $fromu->is_expunged || $fromu->is_redirect );
+
+    return 0 unless LJ::load_user( $tousername );
+
+    # unable to login as this user, because they have an empty password, which is just fine
+    $fromu = LJ::User->create(
+        user => $fromusername,
+        journaltype => "R",     # redirect
+    ) unless $fromu;
+
+    $fromu->set_renamed;
+    $fromu->set_prop( renamedto => $tousername );
+    $fromu->log_event( 'redirect', { renamedto => $tousername, action => "add" } );
+
+    return 1;
+
+}
+
+=head2 C<< DW::User::Rename->break_email_redirection( $from_user, $to_user ) >>
+
+Break email redirection from one user which redirects to another user
+
+=cut
+sub break_email_redirection {
+    my ( $class, $from_user, $to_user ) = @_;
+
+    my $to_u = LJ::load_user( $to_user );
+    my $from_u = LJ::load_user( $from_user );
+    return unless $to_u && $from_u;
+
+    return unless $from_u->is_redirect && $from_u->prop( "renamedto" ) eq $to_u->user;
+
+    return $from_u->delete_email_alias;
+}
+
+=head2 C<< $self->delete_relationships >>
+
+Delete a list of relationships. Returns a string representation of which relationships were deleted.
+
+=cut
+sub delete_relationships {
+    my ( $self, %opts ) = @_;
+
+    if ( $opts{del_trusted_by} ) {
+        foreach ( $self->trusted_by_users ) {
+            $_->remove_edge( $self, trust => {} );
+        }
+    }
+
+    if ( $opts{del_watched_by} ) {
+        foreach ( $self->watched_by_users ) {
+            $_->remove_edge( $self, watch => {} );
+        }
+    }
+
+    my @watched_comms;
+    if ( $opts{del_watched} ) {
+        foreach ( $self->watched_users ) {
+            if ( $_->is_community ) {
+                push @watched_comms, $_ if $opts{del_communities};
+                next;
+            } 
+
+            $self->remove_edge( $_, watch => {} );
+        }
+    }
+
+    if ( $opts{del_trusted} ) {
+        foreach ( $self->trusted_users ) {
+            $self->remove_edge( $_, trust => {} );
+        }
+    }
+
+    # remove admin and community membership edges
+    if ( $opts{del_communities} ) {
+
+        # we already have a list of watched communities if we'd fetched the list of journals we watch
+        unless ( $opts{del_watched} ) {
+            foreach ( $self->watched_users ) {
+                push @watched_comms, $_ if $_->is_community;
+            }
+        }
+
+        foreach ( @watched_comms ) {
+            $self->remove_edge( $_, watch => {} );
+        }
+
+
+        my @ids = $self->member_of_userids;
+        my $memberships = LJ::load_userids( @ids ) || {};
+        foreach ( values %$memberships ) {
+            LJ::leave_community( $self, $_, 0 );
+        }
+    }
+
+    my @del;
+    push @del, "TB" if $opts{del_trusted_by};
+    push @del, "WB" if $opts{del_watched_by};
+    push @del, "T" if $opts{del_trusted};
+    push @del, "W" if $opts{del_watched};
+    push @del, "C"  if $opts{del_communities};
+
+    return join ":", @del;
+}
+
+=head2 C<< $self->_rename_to_ex( $tousername ) >>
+
+Internal function to do renames away from the current username. Low-level, no error-checking on inputs. Accepts a username, renames the user to a form of ex_oldusernam123.
+
+=cut
+sub _rename_to_ex {
+    my ( $u, %opts ) = @_;
+
+    my $errref = $opts{errref} || [];
+
+    my $dbh = LJ::get_db_writer() or die "Could not get DB handle";
+
+    # move the current username out of the way, if it's an existing user
+    my $tries = 0;
+
+    while ( $tries < 10 ) {
+        # take the first ten characters of the old username + a random number
+        my $ex_user = substr( $u->user, 0, 10 ) . int( rand( 999 ) );
+
+        # do the rename if the user doesn't already exist
+        return DW::User::Rename::_rename( $u, "ex_$ex_user", redirect => 0, token => DW::RenameToken->create_token( systemtoken => 1 ) )
+            unless $dbh->selectrow_array( "SELECT COUNT(*) from user WHERE user=?", undef, $ex_user );
+
+        $tries++;
+    }
+
+    push @$errref, LJ::Lang::ml( "rename.ex.toomanytries", { tousername => $u->user } );
+    return 0;
+}
+
+*LJ::User::can_rename_to = \&can_rename_to;
+*LJ::User::rename = \&rename;
+*LJ::User::swap_usernames = \&swap_usernames;
+
+*LJ::User::break_redirects = \&break_redirects;
+*LJ::User::delete_relationships = \&delete_relationships;
+
+=head1 BUGS
+
+=head1 AUTHORS
+
+Afuna <coder.dw@afunamatata.com>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (c) 2010 by Dreamwidth Studios, LLC.
+
+This program is free software; you may redistribute it and/or modify it under
+the same terms as Perl itself. For a copy of the license, please reference
+'perldoc perlartistic' or 'perldoc perlgpl'.
+
+=cut
+
+1;
diff -r 9dafe54beefd -r ba6606b9f6c8 cgi-bin/LJ/Event/SecurityAttributeChanged.pm
--- a/cgi-bin/LJ/Event/SecurityAttributeChanged.pm	Wed Aug 18 17:26:51 2010 -0500
+++ b/cgi-bin/LJ/Event/SecurityAttributeChanged.pm	Thu Aug 19 15:07:56 2010 +0800
@@ -82,8 +82,7 @@ sub new {
         die "This event (uid=$userid, what=username) was not found in logs"
             unless $timechange;
 
-        die "Event (uid=$userid, what=username) was not found in logs".
-            " has wrong old username: $oldvalue instead of $old_username"
+        die "Event (uid=$userid, what=username) has wrong old username: $oldvalue instead of $old_username"
                 if $oldvalue ne $old_username;
 
         my ($timechange2, $oldvalue2) = $sth->fetchrow_array;
diff -r 9dafe54beefd -r ba6606b9f6c8 cgi-bin/LJ/Widget/ShopItemOptions.pm
--- a/cgi-bin/LJ/Widget/ShopItemOptions.pm	Wed Aug 18 17:26:51 2010 -0500
+++ b/cgi-bin/LJ/Widget/ShopItemOptions.pm	Thu Aug 19 15:07:56 2010 +0800
@@ -152,6 +152,12 @@ sub handle_post {
             DW::Shop::Item::Account->new( type => $post->{accttype}, %item_data )
         );
         return ( error => $err ) unless $rv;
+    } elsif ( $post->{item} eq "rename" ) {
+        my ( $rv, $err ) = $cart->add_item(
+            DW::Shop::Item::Rename->new( cannot_conflict => 1, %item_data )
+        );
+
+        return ( error => $err ) unless $rv;
     }
 
     return;
diff -r 9dafe54beefd -r ba6606b9f6c8 doc/config-local.pl.txt
--- a/doc/config-local.pl.txt	Wed Aug 18 17:26:51 2010 -0500
+++ b/doc/config-local.pl.txt	Thu Aug 19 15:07:56 2010 +0800
@@ -104,7 +104,8 @@
     #    paid6  => [  13,  6, 'paid', 130   ],
     #    paid12 => [  25, 12, 'paid', 250   ],
     #    seed   => [ 200, 99, 'seed', 2000   ],
-    #    points => [],
+    #    points => [],     # if present, sell points
+    #    rename => [ 15, undef, undef, 150 ],
     #);
 
 }
diff -r 9dafe54beefd -r ba6606b9f6c8 htdocs/admin/userlog.bml
--- a/htdocs/admin/userlog.bml	Wed Aug 18 17:26:51 2010 -0500
+++ b/htdocs/admin/userlog.bml	Thu Aug 19 15:07:56 2010 +0800
@@ -128,6 +128,12 @@ FORM
         } elsif ( $row->{action} eq 'impersonator' ) {
             my $u = LJ::load_userid( $row->{actiontarget} );
             $action = "Did impersonate on " . ( $u ? $u->ljuser_display : "(no target)" ) . ": " .  LJ::ehtml( $extra->{reason} );
+        } elsif ( $row->{action} eq 'rename' ) {
+            $action = "Renamed from '$extra->{from}' to '$extra->{to}'.";
+            $action .= "<br />Deleted: $extra->{del}" if $extra->{del};
+            $action .= "<br />Redirected: $extra->{redir}" if $extra->{redir};
+        } elsif ( $row->{action} eq 'redirect' ) {
+            $action = $extra->{action} eq "add" ? "Added redirect: $extra->{renamedto}" : "Removed redirect: $extra->{renamedto}";
         } elsif (my $info = LJ::Hooks::run_hook('userlog_rows', $row)) {
             $action = $info;
         } else {
diff -r 9dafe54beefd -r ba6606b9f6c8 htdocs/shop/renames.bml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/shop/renames.bml	Thu Aug 19 15:07:56 2010 +0800
@@ -0,0 +1,113 @@
+<?_c
+#
+# /shop/renames.bml
+#
+# This is the page where a person can choose to buy a rename token for themselves or for another user.
+#
+# Authors:
+#      Afuna <coder.dw@afunamatata.com>
+#
+# Copyright (c) 2010 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+_c?><?page
+body<=
+<?_code
+{
+    use strict;
+    use vars qw/ %GET %POST $title /;
+
+    return BML::redirect( "$LJ::SITEROOT/" )
+        unless LJ::is_enabled( 'payments' );
+
+    # this page uses new style JS
+    LJ::need_res( 'stc/shop.css' );
+    LJ::set_active_resource_group( 'jquery' );
+
+    # let's see what they're trying to do
+    my $for = $GET{for};
+    return BML::redirect( "$LJ::SITEROOT/shop" )
+        unless $for && $for =~ /^(?:self|gift)$/;
+
+    $title = $ML{'.title'};
+
+    # ensure they have a user if it's for self
+    my $remote = LJ::get_remote();
+    return $ML{'.error.invalidself'}
+        if $for eq 'self' && ( !$remote || !$remote->is_personal );
+
+    my $err = DW::Shop->remote_sysban_check;
+    return $err if $err;
+
+    my $ret = "<p><a href='$LJ::SITEROOT/shop'>&lt;&lt; " . BML::ml( '.backlink', { sitename => $LJ::SITENAMESHORT } ) . "</a></p>";
+
+    # hack in cart status here ... will be easy once this page is TTd
+    {
+        my $shop = DW::Shop->get;
+        $ret .= DW::Template->template_string( 'shop/cartdisplay.tt', { shop => $shop, cart => $shop->cart, remote => $remote } );
+    }
+
+    $ret .= "<p>" . BML::ml( ".intro.$for" ) . "</p>";
+    $ret .= "<p>" . BML::ml( '.action', { aopts => "href='/rename'" } ) . "</p>";
+
+    if ( LJ::did_post() ) {
+        return "<?h1 $ML{'Error'} h1?><?p $ML{'error.invalidform'} p?>"
+            unless LJ::check_form_auth();
+
+        my $error;
+        my $post_fields = LJ::Widget::ShopItemOptions->post_fields( \%POST );
+        # need to do this because all of these form fields are in the BML page instead of in the widget
+        LJ::Widget->use_specific_form_fields( post => \%POST, widget => "ShopItemOptions", fields => [ qw( item for username deliverydate_mm deliverydate_dd deliverydate_yyyy anonymous ) ] );
+        my %from_post = LJ::Widget->handle_post( \%POST, ( 'ShopItemOptions' ) );
+        $error = $from_post{error} if $from_post{error};
+
+        if ( $error ) {
+            $ret .= qq{<div class="shop-error">$error</div>};
+        } else {
+            return BML::redirect( "$LJ::SITEROOT/shop" );
+        }
+    }
+
+    $ret .= "<div style='clear: both;'></div>";
+    $ret .= "<form method='post'>";
+    $ret .= LJ::form_auth();
+
+    if ( $for eq "gift" ) {
+        $ret .= "<table class='shop-table-gift'>";
+
+        if ( $for eq 'gift' ) {
+            $ret .= "<tr><td>$ML{'.giftfor.username'}</td><td>" . LJ::html_text( { name => 'username', value => LJ::ehtml( $GET{user} ) } ) . "</td></tr>";
+        }
+
+        $ret .= "<tr><td>$ML{'.giftfor.deliverydate'}</td>";
+        $ret .= "<td>" . LJ::html_datetime( {
+            name => 'deliverydate',
+            default => DateTime->today->date,
+            notime => 1,
+        } ) . "</td></tr>";
+        $ret .= "<tr><td>$ML{'.giftfor.anonymous'}</td>";
+        $ret .= "<td>" . LJ::html_check( {
+            name => 'anonymous',
+            value => 1,
+            selected => $remote ? 0 : 1,
+            disabled => $remote ? 0 : 1,
+        } ) . "</td></tr>";
+
+        $ret .= "</table>";
+    }
+
+    $ret .= LJ::html_hidden( for => $GET{for}, item => "rename" );
+    $ret .= "<p>" . LJ::html_submit( $ML{'.btn.addtocart'} ) . "</p>";
+    $ret .= "</form>";
+
+    $ret .= "<p><a href='$LJ::SITEROOT/shop'>&lt;&lt; " . BML::ml( '.backlink', { sitename => $LJ::SITENAMESHORT } ) . "</a></p>";
+
+    return $ret;
+}
+_code?>
+<=body
+title=><?_code return $title; _code?>
+page?>
diff -r 9dafe54beefd -r ba6606b9f6c8 htdocs/shop/renames.bml.text
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/shop/renames.bml.text	Thu Aug 19 15:07:56 2010 +0800
@@ -0,0 +1,21 @@
+;; -*- coding: utf-8 -*-
+
+.backlink=Back to [[sitename]] Shop
+
+.btn.addtocart=Add to Order
+
+.error.invalidself=You must be logged in as a personal account in order to purchase a rename token for yourself.
+
+.giftfor.anonymous=Anonymous gift?
+
+.giftfor.deliverydate=Delivery date:
+
+.giftfor.username=Username to receive this rename token:
+
+.intro.gift=Buy a rename token for someone else to allow them to change their journal's username.
+
+.intro.self=Buy a rename token for your journal to change your username.
+
+.action=Once your payment has gone through, the token will be listed on <a [[aopts]]>the rename page</a>.
+
+.title=Buy a Rename Token
diff -r 9dafe54beefd -r ba6606b9f6c8 htdocs/stc/rename.css
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/stc/rename.css	Thu Aug 19 15:07:56 2010 +0800
@@ -0,0 +1,39 @@
+#renameform fieldset {
+    display: block;
+}
+
+#renameform fieldset legend {
+    font-size: 1.2em;
+    padding: 0.8em 0 0.2em 0;
+    font-weight: bold;
+}
+
+#renameform .formfield fieldset legend {
+    font-size: 1em;
+}
+
+#renameform .rename label {
+    padding-right: 1em;
+}
+
+#renameform label {
+    display: inline-block;
+    min-width: 8em;
+}
+
+#renameform .formfield {
+    padding: 0.2em 0;
+}
+
+ul.error-list {
+    margin-left: 2em;
+    margin-bottom: 2em;
+}
+
+ul.error-list li { 
+    list-style: disc outside;
+}
+
+p.detail {
+    margin-left: 2em !important;
+}
diff -r 9dafe54beefd -r ba6606b9f6c8 htdocs/stc/shop.css
--- a/htdocs/stc/shop.css	Wed Aug 18 17:26:51 2010 -0500
+++ b/htdocs/stc/shop.css	Thu Aug 19 15:07:56 2010 +0800
@@ -15,7 +15,7 @@
 */
 
 
-.shopbox, .appwidget-shopitemgroupdisplay {
+.shopbox, div.appwidget-shopitemgroupdisplay {
     border: 1px solid #c1272d;
     margin: 10px;
     padding: 5px;
diff -r 9dafe54beefd -r ba6606b9f6c8 t/rename.t
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/t/rename.t	Thu Aug 19 15:07:56 2010 +0800
@@ -0,0 +1,424 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use Test::More;
+plan tests => 96;
+
+use lib "$ENV{LJHOME}/cgi-bin";
+require 'ljlib.pl';
+use LJ::Test qw( temp_user temp_comm );
+use DW::User::Rename;
+use DW::RenameToken;
+
+my $create_users = sub {
+    my %opts = @_;
+
+    my $fromu = temp_user();
+    my $tou = temp_user();
+
+    unless( $opts{match} ) {
+        my %from_defaults = (
+            status => 'N',
+            email => 'from@testemail',
+            password => 'from',
+        );
+
+        LJ::update_user( $fromu, { %from_defaults, %{$opts{from_details} || {}} } );
+
+        my %to_defaults = (
+            status => 'N',
+            email => 'to@testemail',
+            password => 'to',
+        );
+
+        LJ::update_user( $tou, { %to_defaults, %{$opts{to_details} || {}} } );
+    }
+
+    $fromu = LJ::load_userid( $fromu->userid ) if $opts{from_details};
+    $tou = LJ::load_userid( $tou->userid ) if $opts{to_details};
+
+    if ( $opts{validated} ) {
+        LJ::update_user( $fromu, { status => 'A' } );
+        LJ::update_user( $tou, { status => 'A' } );
+    }
+
+    return ( $fromu, $tou );
+};
+
+sub new_token { return DW::RenameToken->create_token( ownerid => $_[0]->id ) }
+
+note( "-- personal-to-unregistered, no redirect" );
+{
+
+    my $u = temp_user();
+
+    my $fromuid = $u->userid;
+    my $fromusername = $u->username;
+    my $tousername = $fromusername . "_renameto";
+
+    ok( ! LJ::load_user( $tousername ), "Username '$tousername' is unregistered" );
+    ok( $u->can_rename_to( $tousername ), "'" . $u->user . "' can rename to '$tousername'" );
+
+    ok( $u->rename( $tousername, token => new_token( $u ), redirect => 0 ), "Rename fromu to a valid unregistered username, no redirect" );
+
+    $u = LJ::load_userid( $u->userid );
+    is( $u->userid, $fromuid, "Id '#$fromuid' remains the same after rename." );
+    is( $u->user, $tousername, "fromu is now named '$tousername'" ); 
+}
+
+note( "-- personal-to-unregistered, with redirect" );
+{
+
+    my $u = temp_user();
+
+    my $fromuid = $u->userid;
+    my $fromusername = $u->user;
+    my $tousername =  $fromusername . "_renameto";
+
+    ok( ! LJ::load_user( $tousername ), "Username '$tousername' is unregistered" );
+    ok( $u->can_rename_to( $tousername ), "'" . $u->user . "' can rename to '$tousername'" );
+
+    ok( $u->rename( $tousername, token => new_token( $u ), redirect => 1 ), "Rename fromu to a valid unregistered username, with redirect" );
+
+    $u = LJ::load_userid( $u->userid );
+    is( $u->userid, $fromuid, "Id '#$fromuid' remains the same after rename." );
+    is( $u->user, $tousername, "fromu is now named '$tousername'" ); 
+
+    my $orig_u = LJ::load_user( $fromusername );
+    ok( $orig_u->is_renamed, "Yup, renamed" );
+    ok( $orig_u->is_redirect, "Chose to redirect this rename" );
+    is( $orig_u->get_renamed_user->user, $tousername, "Confirm redirect from $fromusername to $tousername" );
+}
+
+note( "-- user-to-user, no redirect" );
+{
+    my ( $fromu, $tou ) = $create_users->( match => 1, validated => 1 );
+
+    my $fromuid = $fromu->userid;
+    my $touid = $tou->userid;
+    my $tousername = $tou->user;
+
+    ok( $fromu->rename( $tousername, token => new_token( $fromu ), redirect => 0 ), "Rename fromu to existing user $tousername" );
+
+    $fromu = LJ::load_userid( $fromu->userid );
+    $tou = LJ::load_userid( $tou->userid );
+    is( $fromu->user, $tousername, "Rename fromu to tou, which is under the control of fromu" );
+    my $ex_user = substr( $tousername, 0, 10 );
+    like( $tou->user, qr/^ex_$ex_user/ , "Moved out of the way." );
+    is( $fromu->userid, $fromuid, "Id of fromu remains the same after rename." );
+    is( $tou->userid, $touid, "Id of tou remains the same after rename." );
+}
+
+note( "-- user-to-user, with redirect" );
+{
+    my ( $fromu, $tou ) = $create_users->( match => 1, validated => 1 );
+
+    my $fromuid = $fromu->userid;
+    my $fromusername = $fromu->username;
+    my $touid = $tou->userid;
+    my $tousername = $tou->user;
+
+    ok( $fromu->rename( $tousername, token => new_token( $fromu ), redirect => 1 ), "Rename fromu to existing user $tousername" );
+
+    $fromu = LJ::load_userid( $fromu->userid );
+    $tou = LJ::load_userid( $tou->userid );
+    is( $fromu->user, $tousername, "Rename fromu to tou, which is under the control of fromu" );
+    my $ex_user = substr( $tousername, 0, 10 );
+    like( $tou->user, qr/^ex_$ex_user/ , "Moved out of the way." );
+    is( $fromu->userid, $fromuid, "Id of fromu remains the same after rename." );
+    is( $tou->userid, $touid, "Id of tou remains the same after rename." );
+
+    my $orig_u = LJ::load_user( $fromusername );
+    ok( $orig_u->is_renamed, "Yup, renamed" );
+    ok( $orig_u->is_redirect, "Chose to redirect this rename" );
+    is( $orig_u->get_renamed_user->user, $tousername, "Confirm redirect from $fromusername to $tousername" );
+
+}
+
+note( "-- personal-to-personal, authorization" );
+{
+    my ( $fromu, $tou, $tousername );
+    my %rename_cond = (
+        status => 'A',
+        password => 'rename',
+        email => 'rename@testemail',
+    );
+    ( $fromu, $tou ) = $create_users->( 
+        from_details => { %rename_cond, email => 'from@testemail' },
+        to_details   => { %rename_cond, email => 'to@testemail'   }  );
+    $tousername = $tou->user;
+    ok( ! $fromu->can_rename_to( $tousername ), "Cannot rename fromu to existing user $tousername (because: email)" );
+
+
+    ( $fromu, $tou ) = $create_users->( 
+        from_details => { %rename_cond, password => 'from' },
+        to_details   => { %rename_cond, password => 'to'   }  );
+    $tousername = $tou->user;
+    ok( ! $fromu->can_rename_to( $tousername ), "Cannot rename fromu to existing user $tousername (because: password)" );
+
+    ( $fromu, $tou ) = $create_users->( 
+        from_details => { %rename_cond, status => 'N' },
+        to_details   => { %rename_cond, status => 'N' }  );
+    $tousername = $tou->user;
+    ok( ! $fromu->can_rename_to( $tousername ), "Cannot rename fromu to existing user $tousername (because: validation)" );
+
+
+    ( $fromu, $tou ) = $create_users->( 
+        from_details => { %rename_cond },
+        to_details   => { %rename_cond, status => 'N' }  );
+    $tousername = $tou->user;
+    ok( $fromu->can_rename_to( $tousername ), "Can rename fromu to existing user $tousername (at least one user is validated)" );
+    ok( $fromu->rename( $tousername, token => new_token( $fromu ) ), "Renamed fromu to existing user $tousername" );
+}
+
+{
+    my ( $fromu, $tou ) = $create_users->();
+    my $tousername = $tou->user;
+
+    ok( $fromu->can_rename_to( $tousername, force => 1 ), "Can force rename fromu to existing user $tousername not under their control" );
+    ok( $fromu->rename( $tousername, token => new_token( $fromu ), force => 1 ), "Renamed fromu to existing user $tousername" );
+}
+
+TODO: {
+    local $TODO = "rename to linked usernames, once we allow one account to control multiple usernames";
+}
+
+note( "-- user status special casing" );
+{
+    my ( $fromu, $tou ) = $create_users->();
+
+    my $fromusername = $fromu->username;
+    my $tousername = $tou->user;
+
+    $tou->set_statusvis( "X" );
+
+    ok( $fromu->can_rename_to( $tousername ), "Can always rename to expunged users." );
+    ok( $fromu->rename( $tousername, token => new_token( $fromu ) ), "Rename to expunged user $tousername" );
+
+    $fromu = LJ::load_userid( $fromu->userid );
+    $tou = LJ::load_userid( $tou->userid );
+    is( $fromu->user, $tousername, "Rename fromu to tou, which is under the control of fromu" );
+    my $ex_user = substr( $tousername, 0, 10 );
+    like( $tou->user, qr/^ex_$ex_user/ , "Moved out of the way." );
+}
+
+
+{
+    my ( $fromu, $tou ) = $create_users->();
+
+    my $fromusername = $fromu->username;
+    my $tousername = $tou->user;
+
+    $tou->set_statusvis( "D" );
+
+    ok( ! $fromu->can_rename_to( $tousername ), "Cannot rename to (nonmatching) deleted users." );
+}
+
+{
+    my ( $fromu, $tou, $tousername );
+
+    ( $fromu, $tou ) = $create_users->( validated => 1 );
+    $tousername = $tou->user;
+
+    $tou->set_statusvis( "S" );
+    ok( ! $fromu->can_rename_to( $tousername ), "Cannot rename to nonmatching suspended users." );
+
+
+    ( $fromu, $tou ) = $create_users->( match => 1, validated => 1 );   
+    $tousername = $tou->user;
+
+    $tou->set_statusvis( "S" );
+    ok( ! $fromu->can_rename_to( $tousername ), "Cannot rename to matching suspended users." );
+
+    $tou->set_statusvis( "L" );
+    ok( ! $fromu->can_rename_to( $tousername ), "Cannot rename to matching locked users." );
+
+    $tou->set_statusvis( "M" );
+    ok( ! $fromu->can_rename_to( $tousername ), "Cannot rename to matching memorial users." );
+
+    $tou->set_statusvis( "O" );
+    ok( ! $fromu->can_rename_to( $tousername ), "Cannot rename to matching read-only users." );
+
+    $tou->set_statusvis( "R" );
+    ok( ! $fromu->can_rename_to( $tousername ), "Cannot rename to matching renamed and redirecting users." );
+
+
+    $tou->set_statusvis( "V" );
+    ok( $fromu->can_rename_to( $tousername ), "(reset status)" );
+
+    $fromu->set_statusvis( "S" );
+    ok( ! $fromu->can_rename_to( $tousername ), "Cannot rename from suspended users." );
+}
+
+note( "-- username issues" );
+{
+    my $fromu = temp_user();
+
+    my $fromusername = $fromu->user;
+    # taken from htdocs/inc/reserved-usernames. Production site may have more
+    # but these are good enough for testing
+    my @reserved_names = qw( dw_test ex_test ext_test s_test _test test__test );
+
+    foreach my $name ( @reserved_names ) {
+        ok( ! $fromu->can_rename_to( $name . $fromusername, token => new_token( $fromu ) ), "Cannot rename to reserved username '$name'" );
+    }
+
+    # reserved usernames can be force-renamed to
+    foreach my $name ( @reserved_names ) {
+        ok( $fromu->can_rename_to( $name . $fromusername, token => new_token( $fromu ), force => 1 ), "Forced rename to reserved username '$name$fromusername'" );
+    }
+
+    ok( ! $fromu->can_rename_to( $fromu->username, token => new_token( $fromu ) ), "Cannot rename to own name" );
+}
+
+{
+    my $fromu = temp_user();
+
+    my $fromusername = $fromu->user;
+    my @invalid_usernames = qw( a.b a!b a\x{123}b );
+    push @invalid_usernames, "x" x 30;
+
+    foreach my $name ( @invalid_usernames ) {
+        ok( ! $fromu->rename( $name, token => new_token( $fromu ) ), "Cannot rename to invalid username '$name'" );
+    }
+
+    # invalid usernames cannot be force-renamed to
+    foreach my $name ( @invalid_usernames ) {
+        ok( ! $fromu->rename( $name, token => new_token( $fromu ), force => 1 ), "Cannot force rename to invalid username '$name'" );
+    }
+}
+
+{
+    my $fromu = temp_user();
+
+    my $tousername = $fromu->user . "-abc";
+    ok( $fromu->rename( $tousername, token => new_token( $fromu ) ), "Rename does canonicalization" );
+    $fromu = LJ::load_userid( $fromu->userid );
+    is( $fromu->user, LJ::canonical_username( $tousername ), "Canonicalize away hyphens" );
+}
+
+note( "-- community-to-unregistered" );
+TODO: {
+    local $TODO = "community to unregistered";
+    my $admin = temp_user();
+    my $fromu = temp_comm();
+    my $tousername = $fromu->username . "_renameto";
+
+    ok( ! $admin->can_manage( $fromu ), "User cannot manage community fromu." );
+    ok( ! $fromu->can_rename_to( $tousername, user => $admin ), "Cannot rename community to  $tousername (not admin)" );
+
+    LJ::set_rel( $fromu, $admin, "A" );
+    # FIXME: we shouldn't need to do this!
+    delete $LJ::REQ_CACHE_REL{$fromu->userid."-".$admin->userid."-A"};
+    ok( $admin->can_manage( $fromu ), "User can manage fromu." );
+
+    ok( ! LJ::load_user( $tousername ), "Username '$tousername' is unregistered" );
+    ok( $fromu->can_rename_to( $tousername, user => $admin ), "Can rename to $tousername" );
+    ok( $fromu->rename( $tousername, token => new_token( $fromu ), user => $admin ), "Renamed community to $tousername" );
+
+    ok( $admin->is_validated, "Admin was validated so could rename.");
+    LJ::update_user( $admin, { status => 'N' } );
+    ok( ! $admin->is_validated && ! $fromu->can_rename_to( $tousername . "_rename", user => $admin ), "Admin no longer validated; can no longer rename" );
+
+    ok( ! $fromu->can_rename_to( $tousername ), "Cannot rename community without providing a user doing the renaming" );
+}
+
+note( "-- community-to-community" );
+TODO: {
+    local $TODO = "community to community";
+    my $admin = temp_user();
+    my $fromu = temp_comm();
+    my $tou = temp_comm();
+    my $tousername = $tou->user;
+
+    ok( ! $admin->can_manage( $fromu ), "User cannot manage community fromu" );
+    ok( ! $admin->can_manage( $tou ), "User cannot manage community tou" );
+    ok( ! $fromu->can_rename_to( $tousername, user => $admin ), $admin->user . " cannot rename community fromu to existing community $tousername (because: not admin)" );
+
+    # make admin of fromu
+    LJ::set_rel( $fromu, $admin, "A" );
+    delete $LJ::REQ_CACHE_REL{$fromu->userid."-".$admin->userid."-A"};
+    ok( $admin->can_manage( $fromu ), "User can manage community fromu" );
+    ok( ! $admin->can_manage( $tou ), "User cannot manage community tou" );
+
+    ok( ! $fromu->can_rename_to( $tousername, user => $admin ), $admin->user . " cannot rename community fromu to existing community $tousername (because: not admin of tou)" );
+
+    # make admin of tou
+    LJ::set_rel( $tou, $admin, "A" );
+    delete $LJ::REQ_CACHE_REL{$tou->userid."-".$admin->userid."-A"};
+    ok( $admin->can_manage( $fromu ), "User can manage community fromu" );
+    ok( $admin->can_manage( $tou ), "User can manage community tou" );
+
+    ok( $fromu->can_rename_to( $tousername, user => $admin ), $admin->user . " can rename community fromu to existing community $tousername (is admin of both)" );
+    ok( $fromu->rename( $tousername, token => new_token( $fromu ), user => $admin ), $admin->user . " renamed community fromu to existing community $tousername" );
+}
+
+note( "-- community-to-personal" );
+TODO: {
+    local $TODO = "community to personal";
+    my ( $admin, $tou ) = $create_users->(); 
+    my $fromu = temp_comm();
+    my $tousername = $tou->user;
+
+    # make admin of fromu
+    LJ::set_rel( $fromu, $admin, "A" );
+    delete $LJ::REQ_CACHE_REL{$fromu->userid."-".$admin->userid."-A"};
+
+    ok( ! $fromu->can_rename_to( $tousername, user => $admin ), "Cannot rename fromu to existing user $tousername (tou is a personal journal not under admin's control)" );
+
+    ( $admin, $tou ) = $create_users->( match => 1 ); 
+    $tousername = $tou->user;
+
+    # make admin of fromu
+    LJ::set_rel( $fromu, $admin, "A" );
+    delete $LJ::REQ_CACHE_REL{$fromu->userid."-".$admin->userid."-A"};
+    ok( $fromu->can_rename_to( $tousername, user => $admin ), $admin->user . " can rename community fromu to existing user $tousername (tou is a personal journal under admin's control)" );
+    ok( $fromu->rename( $tousername, token => new_token( $fromu ), user => $admin ), $admin->user . " renamed community fromu to existing community $tousername" );
+}
+
+note( "-- personal-to-community" );
+TODO: {
+    local $TODO = "personal to community";
+    my $fromu = temp_user();
+    my $tou = temp_comm();
+    my $tousername = $tou->user;
+
+    # make admin of tou
+    LJ::set_rel( $tou, $fromu, "A" );
+    delete $LJ::REQ_CACHE_REL{$tou->userid."-".$fromu->userid."-A"};
+
+    ok( $fromu->can_rename_to( $tousername ), "Can rename to a community under your own control." );
+    ok( $fromu->rename( $tousername, token => new_token( $fromu ) ), "Renamed personal journal fromu to existing community $tousername" );
+}
+
+TODO: {
+    local $TODO = "community with multiple admins";
+    my $admin1 = temp_user();
+    my $admin2 = temp_user();
+    my $tou = temp_comm();
+    my $tousername = $tou->user;
+
+    # make admins of tou
+    LJ::set_rel_multi( [ $tou, $admin1, "A" ], [ $tou, $admin2, "A"] );
+    delete $LJ::REQ_CACHE_REL{$tou->userid."-".$admin1->userid."-A"};
+    delete $LJ::REQ_CACHE_REL{$tou->userid."-".$admin2->userid."-A"};
+
+    ok( ! $admin1->can_rename_to( $tousername ), "Cannot rename to a community under your own control if there are multiple admins." );
+}
+
+note( "-- openid and feeds" );
+{
+    my $u = temp_user();
+    LJ::update_user( $u, { journaltype => 'I' } );
+
+    ok( ! $u->can_rename_to( $u->user . "_rename" ), "Cannot rename OpenID accounts" );
+
+    LJ::update_user( $u, { journaltype => 'F' } );
+    ok( ! $u->can_rename_to( $u->user . "_rename" ), "Cannot rename feed accounts" );
+}
+
+TODO: {
+    local $TODO = "two username swap";
+}
+
diff -r 9dafe54beefd -r ba6606b9f6c8 views/rename.tt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/views/rename.tt	Thu Aug 19 15:07:56 2010 +0800
@@ -0,0 +1,122 @@
+[%# rename.tt
+
+Page where you can use a rename token.
+
+Authors:
+    Afuna <coder.dw@afunamatata.com>
+
+This program is free software; you may redistribute it and/or modify it under
+the same terms as Perl itself.  For a copy of the license, please reference
+'perldoc perlartistic' or 'perldoc perlgpl'.
+%]
+
+[%- dw.need_res( 'stc/rename.css' ) -%]
+
+[%- sections.title = '.title' | ml -%]
+
+[% IF error_list %]
+    <p>[% '.error.header' | ml %]</p>
+    <ul class='error-list'>
+        [% FOREACH error = error_list %]
+            <li>[% error %] </li>
+        [% END %]
+    </ul>
+[% END %]
+
+[% IF token %]
+    <form method="POST" id="renameform">
+        [%- dw.form_auth # hidden input field HTML -%]
+
+
+        <fieldset class="rename">
+            <legend>[%- '.form.rename.header' | ml %]</legend>
+            <div class="formfield">
+                <label for="token">[%- '.form.rename.token.label' | ml -%]</label> [%- form.token | html -%]
+            </div>
+            <div class="formfield">
+                <label>[%- '.form.rename.fromuser.label' | ml -%]</label> [% form.from | html %]
+            </div>
+            <div class="formfield">
+                <label for="touser">[%- '.form.rename.touser.label' | ml -%]</label><input type="text" id="touser" name="touser" value="[%- form.to | html -%]" />
+            </div>
+            <div class="formfield">
+                <fieldset>
+                <legend>What do you want to do with your old username?</legend>
+                <input type="radio" name="redirect" value="forward" id="redirect_forward" [% 'checked="checked"' IF form.redirect == "forward" %]/><label for="redirect_forward">[%- '.form.rename.forward.label' | ml %]</label>
+                <p class='detail'>[%- '.form.rename.forward.note' | ml( journalurl = form.journalurl ) %]</p>
+                <input type="radio" name="redirect" value="disconnect" id="redirect_disconnect" [% 'checked="checked"' IF form.redirect == 'disconnect' %]/><label for="redirect_disconnect">[%- '.form.rename.disconnect.label' | ml %]</label>
+                <p class='detail'>[%- '.form.rename.disconnect.note' | ml( journalurl = form.journalurl ) %]</p>
+                </fieldset>
+            </div>
+        </fieldset>
+
+
+        <fieldset class="relationships">
+            <legend>[% '.form.relationships.header' | ml %]</legend>
+            [% FOREACH rel IN [ "watched_by", "trusted_by", "watched", "trusted", "communities" ] %]
+                <div class="formfield">
+                    <input type="checkbox" name="rel_options" value="[% rel %]" id="rel_[% rel %]" [%- 'checked="checked"' IF form.rel_options.$rel -%] /><label for="rel_[% rel %]">[% ".form.relationships.$rel" | ml %]</label>
+                </div>
+            [% END %]
+        </fieldset>
+
+        <fieldset class="others">
+            <legend>[% '.form.others.header' | ml %]</legend>
+            <div class="formfield">
+                <input type="checkbox" name="others" value="email" id="others_email" [%- 'checked="checked"' IF form.others.email -%]/><label for="others_email">[% '.form.others.email' | ml( sitename = site.nameshort ) %] <span id="others_email_note">([% '.form.others.email.note' | ml %])</span></label>
+            </div>
+        </fieldset>
+
+        <input type="submit" value="Rename Journal" />
+    </form>
+
+[% ELSE %]
+    [% IF invalidtoken %]
+        <p>
+        [% '.token.invalid' | ml( token = invalidtoken )  | html %]
+        </p>
+    [% ELSIF usedtoken %]
+        <p>
+        [% '.token.used' | ml( token = usedtoken ) | html %]
+        </p>
+    [% ELSE %]
+        <p>[% '.checkusername.intro' | ml %]</p>
+
+        [% IF checkusername.status %]
+            <p>[% ".checkusername.status.${checkusername.status}"  | ml( user = checkusername.user ) | html %]</p>
+            [% IF checkusername.errors %]
+                <ul class='error-list'>
+                [% FOREACH error = checkusername.errors %]
+                    <li>[% error %] </li>
+                [% END %]
+                </ul>
+            [% END %]
+        [% END %]
+
+        <form method="GET" id="checkusername"> 
+            <label for="username_for_rename">[% '.checkusername.label' | ml %]: <input id="username_for_rename" name="checkuser" value="[% checkusername.user | html %]" />
+            <input type="submit" value="[% '.checkusername.submit' | ml %]" />
+        </form>
+    [% END %]
+
+    <hr />
+
+    [% IF unused_tokens %]
+        <p>[% '.token.list.header' | ml %]</p>
+        <ul>
+        [% FOREACH token = unused_tokens %]
+            <li>[% token.token | html %] - <a href="/rename/[%- token.token | url %]
+            [%- IF checkusername.user AND checkusername.status == 'available' ~%]
+                ?to=[% checkusername.user | url ~%]
+                ">[% '.token.list.item.withname' | ml( username = checkusername.user ) %]
+            [%- ELSE ~%]
+                ">[% '.token.list.item' | ml %]
+            [% END ~%]
+            </a></li>
+        [% END %]
+        </ul>
+    [% ELSE %]
+        [%- '.token.notoken' | ml(aopts = "href='/shop/renames?for=self'") -%]
+    [% END %]
+[% END %]
+
diff -r 9dafe54beefd -r ba6606b9f6c8 views/rename.tt.text
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/views/rename.tt.text	Thu Aug 19 15:07:56 2010 +0800
@@ -0,0 +1,76 @@
+;; -*- coding: utf-8 -*-
+
+.checkusername.intro=Input your desired username, to check whether it is available for renaming to.
+
+.checkusername.label=Desired username
+
+.checkusername.status.available="[[user]]" is available for renaming.
+
+.checkusername.status.unavailable="[[user]]" is not available.
+
+.checkusername.submit=Check Username Availability
+
+.error.header=Unable to perform rename. Please correct the following and try again:
+
+.error.invalidform=There was a problem with processing the form: the page may have been left open too long. Please try submitting the form again.
+
+.error.nojournal=You did not provide a journal to rename.
+
+.error.noredirectopt=You need to choose whether to forward your old username to your new username, or else to drop the connection.
+
+.error.emailnoalias=The email alias feature is not available to your current account level.
+
+.error.emailnotforward=You cannot forward emails sent to your [[emaildomain]] address to your new username, because you have not chosen to redirect your old username to your new one.
+
+.form.relationships.header=Relationships
+
+.form.relationships.communities=Keep community memberships and administration roles
+
+.form.relationships.trusted=Keep outgoing access (people you have granted access to)
+
+.form.relationships.trusted_by=Keep incoming access (people who have granted you access)
+
+.form.relationships.watched=Keep outgoing subscriptions (people you are subscribed to)
+
+.form.relationships.watched_by=Keep incoming subscriptions (people who are subscribed to you)
+
+
+.form.others.header=Others
+
+.form.others.email=Redirect emails sent to your old [[sitename]] email address
+
+.form.others.email.note=your old username must also redirect to your desired username
+
+
+.form.rename.header=Rename
+
+.form.rename.disconnect.label=Disconnect your old username from your new username, leaving the old username free to use
+
+.form.rename.disconnect.note=[[journalurl]] will not point to your new username; comments and community entries that you posted will still show your new username.
+
+.form.rename.forward.label=Forward your old username to your new username
+
+.form.rename.forward.note=[[journalurl]] will automatically redirect to your new username.
+
+.form.rename.fromuser.label=Rename from
+
+.form.rename.token.label=Rename token
+
+.form.rename.touser.label=Rename to
+
+
+.success=Successfully renamed journal from [[from]] to [[to]].
+
+.title=Rename Journal
+
+.token.invalid=Invalid token: [[token]]
+
+.token.list.header=You have these unused tokens:
+
+.token.list.item=Use token to rename journal
+
+.token.list.item.withname=Use token to rename journal to [[username]]
+
+.token.notoken=<a [[aopts]]>Purchase a rename token</a> to perform a rename.
+
+.token.used=Token has been used: [[token]]
diff -r 9dafe54beefd -r ba6606b9f6c8 views/shop/index.tt
--- a/views/shop/index.tt	Wed Aug 18 17:26:51 2010 -0500
+++ b/views/shop/index.tt	Thu Aug 19 15:07:56 2010 +0800
@@ -33,6 +33,17 @@
     </div>
 </div>
 
+<div class='shop-category'>
+    <div class='shop-category-title'>[% '.title.renames' | ml %]</div>
+    <div class='shop-category-items'>
+[% IF remote AND remote.is_personal %]
+        <span class='shop-category-item'><a href="[% roots.site %]/shop/renames?for=self">[% '.for.self' | ml %]</a> ([% remote.ljuser_display %])</span>
+        <span class='shop-category-item'><a href="[% roots.site %]/shop/renames?for=gift">[% '.for.different' | ml %]</a></span>
+[% ELSE %]
+        <span class='shop-category-item'>[% '.renames.login' | ml %]</span>
+[% END %]
+    </div>
+</div>
 
 [%#
 
@@ -48,11 +59,4 @@ Here for future expansion ...
 </div>
 
 
-<div class='shop-category'>
-    <div class='shop-category-title'>Rename Tokens for ...</div>
-    <div class='shop-category-items'>
-        <span class='shop-category-item'><a href="...">yourself</a> (username)</span>
-        <span class='shop-category-item'><a href="...">your circle</a></span>
-    </div>
-</div>
 %]
diff -r 9dafe54beefd -r ba6606b9f6c8 views/shop/index.tt.text
--- a/views/shop/index.tt.text	Wed Aug 18 17:26:51 2010 -0500
+++ b/views/shop/index.tt.text	Thu Aug 19 15:07:56 2010 +0800
@@ -14,8 +14,12 @@
 
 .points.login=You must log in to a personal account to buy points.
 
+.renames.login=You must log in to a personal account to buy a rename token.
+
 .title=[[sitename]] Shop
 
 .title.paidacc=Buy a Paid Account for...
 
 .title.points=Buy [[site]] Points for...
+
+.title.renames=Buy a Rename Token for...
--------------------------------------------------------------------------------
ninetydegrees: Text: I came to dance (dance)

[personal profile] ninetydegrees 2010-08-19 11:42 am (UTC)(link)
YES! Thank you, thank you, thank you!