[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
![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
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 --------------------------------------------------------------------------------