[dw-free] Implement two-token swap of two usernames
[commit: http://hg.dwscoalition.org/dw-free/rev/c0efdc40d201]
http://bugs.dwscoalition.org/show_bug.cgi?id=2317
Two token swap of usernames; new page linked to from /rename, changes to
community renames so that you can only rename a community (or rename to a
community, because it then gets renamed to ex_*), when you are the only
community member. This is to avoid surprises.
Patch by
fu.
Files modified:
http://bugs.dwscoalition.org/show_bug.cgi?id=2317
Two token swap of usernames; new page linked to from /rename, changes to
community renames so that you can only rename a community (or rename to a
community, because it then gets renamed to ex_*), when you are the only
community member. This is to avoid surprises.
Patch by
Files modified:
- bin/upgrading/en.dat
- cgi-bin/DW/Controller/Rename.pm
- cgi-bin/DW/User/Rename.pm
- t/rename.t
- views/rename.tt
- views/rename/swap.tt
- views/rename/swap.tt.text
--------------------------------------------------------------------------------
diff -r 485f2b7ff657 -r c0efdc40d201 bin/upgrading/en.dat
--- a/bin/upgrading/en.dat Tue Nov 02 18:49:58 2010 +0800
+++ b/bin/upgrading/en.dat Wed Nov 03 10:55:19 2010 +0800
@@ -2485,11 +2485,15 @@ rename.error.reserved="[[to]]" is a rese
rename.error.unauthorized=[[to]] is not under your control.
+rename.error.unauthorized.forcomm=The community [[comm]] must have no members, and must be 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.error.tokentoofew=Too few tokens; you need two to perform a swap.
rename.ex.toomanytries=We had some trouble trying to move aside the account you are trying to rename to ([[tousername]]). Please try again.
diff -r 485f2b7ff657 -r c0efdc40d201 cgi-bin/DW/Controller/Rename.pm
--- a/cgi-bin/DW/Controller/Rename.pm Tue Nov 02 18:49:58 2010 +0800
+++ b/cgi-bin/DW/Controller/Rename.pm Wed Nov 03 10:55:19 2010 +0800
@@ -25,6 +25,8 @@ use DW::Controller::Admin;
use DW::RenameToken;
use DW::Shop;
+
+DW::Routing->register_string( "/rename/swap", \&swap_handler, app => 1 );
# 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)
@@ -174,6 +176,74 @@ sub handle_post {
return ( 0, $errref );
}
+
+sub swap_handler {
+ my $r = DW::Request->get;
+
+ my ( $ok, $rv ) = controller( authas => 1 );
+ return $rv unless $ok;
+
+ my $remote = $rv->{remote};
+ return error_ml( 'rename.error.invalidaccounttype' ) unless $remote->is_personal;
+
+ my $vars = {};
+
+ my $post_args = DW::Request->get->post_args || {};
+
+ my @unused_tokens = @{DW::RenameToken->by_owner_unused( userid => $remote->userid ) || []};
+ $vars->{numtokens} = scalar @unused_tokens;
+
+ 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_swap_post( $post_args, tokens => \@unused_tokens, user => $remote );
+ return $rv if $post_ok;
+
+ $vars->{error_list} = $rv;
+ }
+
+ my $authas = LJ::make_authas_select( $remote,
+ { selectonly => 1,
+ authas => $post_args->{authas},
+ } );
+
+ $vars->{form} = {
+ authas => $authas,
+ };
+
+ return DW::Template->render_template( 'rename/swap.tt', $vars );
+}
+
+sub handle_swap_post {
+ my ( $post_args, %opts ) = @_;
+
+ my @errors = ();
+ push @errors, LJ::Lang::ml( '/rename.tt.error.invalidform' ) unless LJ::check_form_auth( $post_args->{lj_form_auth} );
+
+ my $journal = LJ::get_authas_user( $post_args->{authas} );
+ push @errors, LJ::Lang::ml( '/rename/swap.tt.error.nojournal' ) unless $journal;
+
+ my $swapjournal = LJ::load_user( $post_args->{swapjournal} );
+ push @errors, LJ::Lang::ml( '/rename/swap.tt.error.invalidswapjournal' ) unless $swapjournal;
+
+ return ( 0, \@errors ) if @errors;
+
+
+ ( $journal, $swapjournal ) = ( $swapjournal, $journal )
+ if $journal->is_community && $swapjournal->is_personal;
+
+ # let's do this
+ my $errref = [];
+ $journal->swap_usernames( $swapjournal, user => $opts{user}, tokens => $opts{tokens}, errref => $errref );
+
+ return ( 1, success_ml( "/rename/swap.tt.success", {
+ journal => $journal->ljuser_display,
+ swapjournal => $swapjournal->ljuser_display,
+ } ) ) unless @$errref;
+
+ # return the list of errors, because we want to print out other things as well...
+ return ( 0, $errref );
+}
+
sub rename_admin_handler {
my ( $ok, $rv ) = controller( privcheck => [ "siteadmin:rename" ] );
return $rv unless $ok;
diff -r 485f2b7ff657 -r c0efdc40d201 cgi-bin/DW/User/Rename.pm
--- a/cgi-bin/DW/User/Rename.pm Tue Nov 02 18:49:58 2010 +0800
+++ b/cgi-bin/DW/User/Rename.pm Wed Nov 03 10:55:19 2010 +0800
@@ -114,8 +114,8 @@ sub can_rename_to {
# expunged users can always be renamed to
return { ret => 1 } if $tou->is_expunged;
- # communities cannot be renamed to
- if ( ! $tou->is_personal ) {
+ # only personal journals and communities can be renamed
+ if ( ! ( $tou->is_personal || $tou->is_community ) ) {
push @$errref, LJ::Lang::ml( 'rename.error.invalidjournaltypeto' );
return { ret => 0 };
}
@@ -135,13 +135,20 @@ sub can_rename_to {
# person-to-person
return 1 if DW::User::Rename::_are_same_person( $self, $tou );
+ # person-to-community (only under restricted circumstances for the community)
+ return 1 if DW::User::Rename::_is_authorized_for_comm( $self, $tou );
+
push @$errref, LJ::Lang::ml( 'rename.error.unauthorized', { to => $tou->ljuser_display } );
return 0;
} elsif ( $self->is_community && LJ::isu( $opts{user} ) ) {
my $admin = $opts{user};
- # user must be able to control (be an admin of) community
- return 0 unless $admin->can_manage_other( $self );
+ # make sure that the community journal is under the admin's control
+ # and satisfies all other conditions that ensure we don't leave members hanging
+ unless ( DW::User::Rename::_is_authorized_for_comm( $admin, $self ) ) {
+ push @$errref, LJ::Lang::ml( 'rename.error.unauthorized.forcomm', { comm => $self->ljuser_display } );
+ return 0;
+ }
my $tou = LJ::load_user( $tousername );
@@ -151,7 +158,12 @@ sub can_rename_to {
# community-to-person
# able to rename to another personal journal under admin's control
- return 1 if DW::User::Rename::_are_same_person( $admin, $tou );
+ return 1 if $tou->is_person && DW::User::Rename::_are_same_person( $admin, $tou );
+
+ # community-to-community
+ # we checked early on that the admin is authorized to rename this community
+ # so we don't need to check again here
+ return 1 if $tou->is_community;
}
# be strict in what we accept
@@ -198,12 +210,55 @@ sub rename {
=head2 C<< $self->swap_usernames( $touser [, %opts ] ) >>
-Swap the usernames of these two users. Currently unimplemented.
+Swap the usernames of these two users.
=cut
sub swap_usernames {
- my ( $self, $touser, %opts ) = @_;
+ my ( $u1, $u2, %opts ) = @_;
+
+ my $errref = $opts{errref} || [];
+
+ my $admin = LJ::isu( $opts{user} ) ? $opts{user} : $u1;
+ my @tokens = @{ $opts{tokens} || [] };
+
+ foreach my $token ( @tokens ) {
+ push @$errref, LJ::Lang::ml( 'rename.error.tokeninvalid' )
+ unless $token && $token->isa( "DW::RenameToken" )
+ && $token->ownerid == $admin->userid;
+
+ push @$errref, LJ::Lang::ml( 'rename.error.tokenapplied' )
+ if $token->applied;
+ }
+
+ if ( scalar @tokens >= 2 ) {
+ push @$errref, LJ::Lang::ml( 'rename.error.tokeninvalid' )
+ if ( $tokens[0]->token eq $tokens[1]->token );
+ } else {
+ push @$errref, LJ::Lang::ml( 'rename.error.tokentoofew' );
+ }
+
+ my %admin_opts = $u2->is_community ? ( user => $admin ) : ();
+
+ my $can_rename = $u1->can_rename_to( $u2->username, %opts, %admin_opts )
+ && $u2->can_rename_to( $u1->username, %opts, %admin_opts );
+ return 0 if @$errref || ! $can_rename;
+
+ my $u1name = $u1->user;
+ my $u2name = $u2->user;
+
+ my $did_rename = 1;
+ $did_rename &&= DW::User::Rename::_rename_to_ex( $u2, errref => $opts{errref} );
+ return 0 unless $did_rename;
+
+ # ugh, but need it to avoid duplicate timestamps in infohistory
+ sleep( 1 );
+
+ $did_rename &&= DW::User::Rename::_rename( $u1, $u2name, %opts, %admin_opts, token => $tokens[0] );
+ return 0 unless $did_rename;
+
+ $did_rename &&= DW::User::Rename::_rename( $u2, $u1name, %opts, %admin_opts, token => $tokens[1] );
+ return $did_rename;
}
=head2 C<< $self->_clear_from_cache >>
@@ -242,6 +297,26 @@ sub _are_same_person {
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->_is_authorized_for_comm >>
+
+Internal function to determine whether an account can control / manage another account
+
+=cut
+
+sub _is_authorized_for_comm {
+ my ( $admin, $journal ) = @_;
+
+ return 0 unless $admin->is_person && $journal->is_community;
+ return 0 unless $admin->can_manage_other( $journal );
+
+ # community must have no users, to avoid confusion
+ my @member_userids = $journal->member_userids;
+ return 0 if scalar @member_userids > 1;
+ return 0 if scalar @member_userids == 1 && $member_userids[0] != $admin->userid;
return 1;
}
diff -r 485f2b7ff657 -r c0efdc40d201 t/rename.t
--- a/t/rename.t Tue Nov 02 18:49:58 2010 +0800
+++ b/t/rename.t Wed Nov 03 10:55:19 2010 +0800
@@ -3,7 +3,7 @@ use warnings;
use warnings;
use Test::More;
-plan tests => 137;
+plan tests => 152;
use lib "$ENV{LJHOME}/cgi-bin";
require 'ljlib.pl';
@@ -385,6 +385,7 @@ note( "-- community-to-unregistered" );
{
my $admin = temp_user();
my $fromu = temp_comm();
+ my $oldusername = $fromu->username;
my $tousername = $fromu->username . "_renameto";
ok( ! $admin->can_manage( $fromu ), "User cannot manage community fromu." );
@@ -405,36 +406,13 @@ note( "-- community-to-unregistered" );
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;
+ my $member = temp_user();
+ $member->join_community( $fromu );
+ ok( ! $fromu->can_rename_to( $oldusername, user => $admin ), "Cannot rename a community with members" );
- 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( $admin ), user => $admin ), $admin->user . " renamed community fromu to existing community $tousername" );
+ $member->leave_community( $fromu );
+ ok( $fromu->can_rename_to( $oldusername, user => $admin ), "Can rename community again, no members." );
}
note( "-- community-to-personal" );
@@ -460,9 +438,10 @@ note( "-- community-to-personal" );
}
note( "-- personal-to-community" );
-TODO: {
- local $TODO = "personal to community";
+{
my $fromu = temp_user();
+ LJ::update_user( $fromu, { status => 'A' } );
+
my $tou = temp_comm();
my $tousername = $tou->user;
@@ -470,23 +449,15 @@ TODO: {
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" );
-}
+ my $member = temp_user();
+ $member->join_community( $tou );
+ ok( ! $fromu->can_rename_to( $tousername, user => $fromu, verbose => 1 ), "Cannot rename a community with members" );
-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." );
+ $member->leave_community( $tou );
+ ok( $fromu->can_rename_to( $tousername ),
+ "Can rename to a community under your own control if it has no members.");
+ ok( $fromu->rename( $tousername, token => new_token( $fromu ) ),
+ "Renamed personal journal fromu to existing community." );
}
note( "-- openid and feeds" );
@@ -500,7 +471,165 @@ note( "-- openid and feeds" );
ok( ! $u->can_rename_to( $u->user . "_rename" ), "Cannot rename feed accounts" );
}
-TODO: {
- local $TODO = "two username swap";
+note( "-- two username swap (personal to personal)" );
+{
+ my ( $u1, $u2 ) = $create_users->( match => 1, validated => 1 );
+
+ my $u1id = $u1->userid;
+ my $u2id = $u2->userid;
+ my $u1sername = $u1->user;
+ my $u2sername = $u2->user;
+
+ ok( $u1sername ne $u2sername, "Not the same username" );
+
+ my $token = new_token( $u1 );
+ ok( ! $u1->swap_usernames(
+ $u2,
+ tokens => [ $token, $token ]
+ ), "Can't swap, token is the same" );
+
+
+ ok( $u1->swap_usernames(
+ $u2,
+ tokens => [ new_token( $u1 ), new_token( $u1 ) ]
+ ), "Swap usernames" );
+
+ $u1 = LJ::load_userid( $u1->userid );
+ $u2 = LJ::load_userid( $u2->userid );
+
+ is( $u1->user, $u2sername, "Swap usernames of u1 and u2" );
+ is( $u2->user, $u1sername, "Swap usernames of u2 and u1" );
+
+ is( $u1->userid, $u1id, "Id of u1 remains the same after rename." );
+ is( $u2->userid, $u2id, "Id of u2 remains the same after rename." );
}
+note( "-- two username swap (one user is suspended)" );
+{
+ my ( $u1, $u2 ) = $create_users->( match => 1, validated => 1 );
+ $u2->set_statusvis( "S" );
+
+ my $u1id = $u1->userid;
+ my $u2id = $u2->userid;
+ my $u1sername = $u1->user;
+ my $u2sername = $u2->user;
+
+ ok( $u1sername ne $u2sername, "Not the same username" );
+
+ ok( ! $u1->swap_usernames(
+ $u2,
+ tokens => [ new_token( $u1 ), new_token( $u1 ) ]
+ ), "Cannot swap usernames." );
+
+ $u1 = LJ::load_userid( $u1->userid );
+ $u2 = LJ::load_userid( $u2->userid );
+
+ is( $u1->user, $u1sername, "No swap" );
+ is( $u2->user, $u2sername, "No swap" );
+}
+
+note( "-- two username swap personal <=> community " );
+{
+ my $u = temp_user();
+ LJ::update_user( $u, { status => 'A' } );
+
+ my $comm = temp_comm();
+ my $uname = $u->user;
+ my $commname = $comm->user;
+
+ ok( ! $u->swap_usernames(
+ $comm,
+ tokens => [ new_token( $u ), new_token( $u ) ]
+ ), "Cannot swap personal and community usernames (not an admin)" );
+
+ # make admin of u
+ LJ::set_rel( $comm, $u, "A" );
+ delete $LJ::REQ_CACHE_REL{$comm->userid."-".$u->userid."-A"};
+
+ ok( $u->swap_usernames(
+ $comm,
+ tokens => [ new_token( $u ), new_token( $u ) ],
+ ), "Swap personal and community usernames" );
+
+ is( $u->user, $commname, "Swap usernames u => comm" );
+ is( $comm->user, $uname, "Swap usernames comm => u" );
+}
+
+note( "-- two username swap personal <=> community (with malice)" );
+{
+ my ( $u, $u2 ) = $create_users->( validate => 1 );
+
+ my $comm = temp_comm();
+ my $uname = $u->user;
+ my $commname = $comm->user;
+
+ # make admin of u
+ LJ::set_rel( $comm, $u, "A" );
+ delete $LJ::REQ_CACHE_REL{$comm->userid."-".$u->userid."-A"};
+
+ LJ::set_rel( $comm, $u2, "A" );
+ delete $LJ::REQ_CACHE_REL{$comm->userid."-".$u2->userid."-A"};
+
+ ok( ! $u->swap_usernames(
+ $u2,
+ tokens => [ new_token( $u ), new_token( $u ) ],
+ ), "Swap usernames with someone not under your control" );
+
+ ok ( ! $comm->swap_usernames(
+ $u2,
+ user => $u,
+ tokens => [ new_token( $u ), new_token( $u )],
+ ), "Swapping the username of a co-admin");
+}
+
+note( "-- two username swap community <=> personal" );
+{
+ my $u = temp_user();
+ LJ::update_user( $u, { status => 'A' } );
+
+ my $comm = temp_comm();
+ my $uname = $u->user;
+ my $commname = $comm->user;
+
+ ok( ! $comm->swap_usernames(
+ $u,
+ tokens => [ new_token( $u ), new_token( $u ) ]
+ ), "Cannot swap personal and community usernames (not an admin)" );
+
+ # make admin of u
+ LJ::set_rel( $comm, $u, "A" );
+ delete $LJ::REQ_CACHE_REL{$comm->userid."-".$u->userid."-A"};
+
+ ok( ! $comm->swap_usernames(
+ $u,
+ tokens => [ new_token( $u ), new_token( $u ) ]
+ ), "Cannot swap community and personal when acting on the community" );
+}
+
+note( "-- two username swap (community and community)" );
+{
+ my $admin = temp_user();
+ LJ::update_user( $admin, { status => 'A' } );
+
+ my $c1 = temp_comm();
+ my $c2 = temp_comm();
+
+ LJ::set_rel( $c1, $admin, "A" );
+ delete $LJ::REQ_CACHE_REL{$c1->userid."-".$admin->userid."-A"};
+ LJ::set_rel( $c2, $admin, "A" );
+ delete $LJ::REQ_CACHE_REL{$c2->userid."-".$admin->userid."-A"};
+
+ my $c1sername = $c1->user;
+ my $c2sername = $c2->user;
+
+ ok( $c1sername ne $c2sername, "Not the same username" );
+
+ ok( $c1->swap_usernames(
+ $c2,
+ user => $admin,
+ tokens => [ new_token( $admin ), new_token( $admin ) ],
+ ), "Swap community usernames" );
+
+ is( $c1->user, $c2sername, "Swap usernames of c1 and c2" );
+ is( $c2->user, $c1sername, "Swap usernames of c2 and c1" );
+}
diff -r 485f2b7ff657 -r c0efdc40d201 views/rename.tt
--- a/views/rename.tt Tue Nov 02 18:49:58 2010 +0800
+++ b/views/rename.tt Wed Nov 03 10:55:19 2010 +0800
@@ -104,6 +104,11 @@ the same terms as Perl itself. For a co
<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>
+
+ <hr />
+
+ <a href="[%dw.site|url%]/rename/swap">Swap usernames with a journal under your control</a>
+ <p class='note'>You will require two tokens</p>
[% END %]
<hr />
diff -r 485f2b7ff657 -r c0efdc40d201 views/rename/swap.tt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/views/rename/swap.tt Wed Nov 03 10:55:19 2010 +0800
@@ -0,0 +1,45 @@
+[%# rename/swap.tt
+
+Page where you can swap the usernames of two journals under your control.
+
+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 %]
+
+<p>[% '.intro' | ml %]</p>
+[% IF numtokens >= 2 %]
+ <form id="renameform" method="POST">
+ [% dw.form_auth %]
+ <div class='formfield'>
+ <label for='authas'>[% '.form.journal' | ml %]</label>
+ [% form.authas %]
+ </div>
+ <div class='formfield'>
+ <label for='swapjournal'>[% '.form.swapjournal' | ml %]</label>
+ <input type='text' name='swapjournal' id='swapjournal' />
+ </div>
+
+ <input type='submit' value='Swap' />
+ </form>
+[% ELSE %]
+ [%- '.numtokens.toofew' | ml(aopts = "href='/shop/renames?for=self'") -%]
+[% END %]
+
+
diff -r 485f2b7ff657 -r c0efdc40d201 views/rename/swap.tt.text
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/views/rename/swap.tt.text Wed Nov 03 10:55:19 2010 +0800
@@ -0,0 +1,17 @@
+.error.header=Unable to perform username swap. Please correct the following and try again:
+
+.error.invalidswapjournal=Invalid username was provided to swap with.
+
+.error.nojournal=No journal selected to swap usernames.
+
+.form.journal=Select Journal
+
+.form.swapjournal=Swap With
+
+.intro=You may swap the usernames of two personal journals, or of your journal and a community under your control. None of your settings will change. This will use two of your rename tokens.
+
+.numtokens.toofew=You do not have enough tokens to do a swap. You can <a [[aopts]]>purchase rename tokens</a> in the shop.
+
+.success=Successfully swapped [[journal]] and [[swapjournal]].
+
+.title=Swap Journal Usernames
--------------------------------------------------------------------------------
