[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
fu.
Files modified:
http://bugs.dwscoalition.org/show_bug.cgi?id=216
Renames for personal journals.
Patch by
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'><< " . 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'><< " . 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...
--------------------------------------------------------------------------------

no subject