#!/usr/bin/perl # # Authors: # Afuna # # Copyright (c) 2013-2018 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::Community; use strict; use DW::Controller; use DW::Routing; use DW::Template; use DW::FormErrors; use POSIX; use List::MoreUtils qw/ uniq /; use DW::Entry::Moderated; =head1 NAME DW::Controller::Community - Community management pages =cut DW::Routing->register_string( "/communities/index", \&index_handler, app => 1 ); DW::Routing->register_string( "/communities/list", \&list_handler, app => 1 ); DW::Routing->register_string( "/communities/new", \&new_handler, app => 1 ); DW::Routing->register_string( "/communities/convert", \&convert_handler, app => 1 ); DW::Routing->register_regex( '^/communities/([^/]+)/members/new$', \&members_new_handler, app => 1 ); DW::Routing->register_regex( '^/communities/([^/]+)/members/edit$', \&members_edit_handler, app => 1 ); DW::Routing->register_string( "/communities/members/purge", \&members_purge_handler, app => 1, methods => { POST => 1 } ); DW::Routing->register_regex( '^/communities/([^/]+)/queue/entries$', \&entry_queue_handler, app => 1 ); DW::Routing->register_regex( '^/communities/([^/]+)/queue/entries/([0-9]+)$', \&entry_queue_edit_handler, app => 1 ); DW::Routing->register_regex( '^/communities/([^/]+)/queue/members$', \&members_queue_handler, app => 1 ); DW::Routing->register_regex( '^/approve/(\d+)\.(.+)$', \&member_approve_handler, app => 1, no_cache => 1 ); DW::Routing->register_regex( '^/reject/(\d+)\.(.+)$', \&member_reject_handler, app => 1, no_cache => 1 ); # redirects DW::Routing->register_redirect( "/community/index", "/communities/index" ); DW::Routing->register_redirect( "/community/manage", "/communities/list" ); DW::Routing->register_redirect( "/community/create", "/communities/new" ); DW::Routing->register_redirect( "/community/sentinvites", "/communities/members/new", keep_args => ["authas"] ); DW::Routing->register_string( "/communities/members/new", \&members_new_redirect_handler, app => 1 ); DW::Routing->register_redirect( "/community/members", "/communities/members/edit", keep_args => ["authas"] ); DW::Routing->register_string( "/communities/members/edit", \&members_redirect_handler, app => 1 ); DW::Routing->register_redirect( "/community/moderate", "/communities/queue/entries", keep_args => ["authas"] ); DW::Routing->register_string( "/communities/queue/entries", \&entry_queue_redirect_handler, app => 1 ); DW::Routing->register_redirect( "/community/pending", "/communities/queue/members", keep_args => ["authas"] ); DW::Routing->register_string( "/communities/queue/members", \&member_queue_redirect_handler, app => 1 ); sub _redirect_authas { my $redirect_path = $_[0]; my $r = DW::Request->get; my $get = $r->get_args; my $authas = LJ::eurl( $get->{authas} ); if ($authas) { return $r->redirect("$LJ::SITEROOT/communities/$authas/$redirect_path"); } else { return $r->redirect("$LJ::SITEROOT/communities/list"); } } sub members_new_redirect_handler { return _redirect_authas("members/new"); } sub members_redirect_handler { return _redirect_authas("members/edit"); } sub entry_queue_redirect_handler { return _redirect_authas("queue/entries"); } sub member_queue_redirect_handler { return _redirect_authas("queue/members"); } sub index_handler { my ( $ok, $rv ) = controller( anonymous => 1 ); return $rv unless $ok; my $vars = { remote => $rv->{remote}, remote_admins_communities => @{ LJ::load_rel_target( $rv->{remote}, 'A' ) || [] } ? 1 : 0, community_manage_links => LJ::Hooks::run_hook('community_manage_links') || "", # implemented as a hook because most/all the links are to # dreamwidth.org-specific FAQs. see cgi-bin/DW/Hooks/Community.pm # in dw-nonfree as an example to create your own. faq_links => LJ::Hooks::run_hook('community_faqs') || "", # hook is to list dw-community-promo; # define your own in a hook if you have a similar community or want to # add other links to the list. community_search_links => LJ::Hooks::run_hook('community_search_links') || "", recently_active_comms => DW::Widget::RecentlyActiveComms->render, newly_created_comms => DW::Widget::NewlyCreatedComms->render, official_comms => LJ::Hooks::run_hook('official_comms') || "", }; return DW::Template->render_template( 'communities/index.tt', $vars ); } sub list_handler { my ( $ok, $rv ) = controller(); return $rv unless $ok; my $remote = $rv->{remote}; my @comms_managed = $remote->communities_managed_list; my @comms_moderated = $remote->communities_moderated_list; # 'foo' => { # user => 'foo' # ljuser => '<.... user=foo>' # title => 'Community for Foo Enthusiasts', # mod_queue_count => 123, # pending_members_count => 456, # } my %communities; foreach my $cu ( @comms_managed, @comms_moderated ) { $communities{ $cu->user } = { user => $cu->user, ljuser => $cu->ljuser_display, title => $cu->name_raw, moderation_queue_url => $cu->moderation_queue_url, member_queue_url => $cu->member_queue_url, }; } foreach my $cu (@comms_managed) { my $comm_representation = $communities{ $cu->user }; $comm_representation->{admin} = 1; my $pending_members = $cu->is_moderated_membership ? $cu->get_pending_members_count : 0; $comm_representation->{pending_members_count} = $pending_members; } foreach my $cu (@comms_moderated) { my $comm_representation = $communities{ $cu->user }; $comm_representation->{moderator} = 1; # we don't rely on $cu->has_moderated_posting # because we may still have posts in the queue # e.g., after a switch from moderated posting to non-moderated posting my $mod_queue = $cu->get_mod_queue_count; my $should_show_queue = $cu->has_moderated_posting || $mod_queue; $comm_representation->{show_mod_queue_count} = $should_show_queue; $comm_representation->{mod_queue_count} = $cu->get_mod_queue_count if $should_show_queue; } my @sorted_communities = sort { $a cmp $b } keys %communities; my $vars = { community_list => [ @communities{@sorted_communities} ], }; return DW::Template->render_template( 'communities/list.tt', $vars ); } sub _enforce_valid_settings { my ( $post, $default_options, $validate_age_restriction ) = @_; # checks that the POSTed option is valid # if not, force it to the default option my $validate = sub { my ( $key, $regex ) = @_; $post->set( $key, $default_options->{$key} ) unless defined $post->{$key} && $post->{$key} =~ $regex; }; $validate->( "membership", qr/^(?:open|moderated|closed)$/ ); $validate->( "postlevel", qr/^(?:members|select)$/ ); $validate->( "nonmember_posting", qr/^[01]$/ ); $validate->( "moderated", qr/^[01]$/ ); $validate->( "age_restriction", qr/^(?:none|concepts|explicit)$/ ) if $validate_age_restriction; return $post; } sub new_handler { my ( $ok, $rv ) = controller( form_auth => 1 ); return $rv unless $ok; my $remote = $rv->{remote}; my $r = $rv->{r}; my $post; my $get; return error_ml('bml.badinput.body1') unless LJ::text_in($post); return error_ml('/communities/new.tt.error.notactive') unless $remote->is_visible; return error_ml( '/communities/new.tt.error.notconfirmed', { confirm_url => "$LJ::SITEROOT/register", } ) unless $remote->is_validated; my %default_options = ( membership => 'open', postlevel => 'members', moderated => '0', nonmember_posting => '0', age_restriction => 'none' ); my $errors = DW::FormErrors->new; if ( $r->did_post ) { $post = _enforce_valid_settings( $r->post_args, \%default_options, 1 ); my $new_user = LJ::canonical_username( $post->{user} ); my $title = $post->{title} || $new_user; if ( LJ::sysban_check( 'email', $remote->email_raw ) ) { LJ::Sysban::block( 0, "Create user blocked based on email", { new_user => $new_user, email => $remote->email_raw, name => $new_user } ); return $r->HTTP_SERVICE_UNAVAILABLE; } if ( !$post->{user} ) { $errors->add( "user", ".error.user.mustenter" ); } elsif ( !$new_user ) { $errors->add( "user", "error.usernameinvalid" ); } elsif ( length $new_user > 25 ) { $errors->add( "user", "error.usernamelong" ); } # disallow creating communities matched against the deny list $errors->add( "user", ".error.user.reserved" ) if LJ::User->is_protected_username($new_user); # now try to actually create the community my $second_submit; my $cu = LJ::load_user($new_user); if ( $cu && $cu->is_expunged ) { $errors->add( "user", "widget.createaccount.error.username.purged", { aopts => "href='$LJ::SITEROOT/rename/'" } ); } elsif ($cu) { # community was created in the last 10 minutes? my $recent_create = ( $cu->timecreate > ( time() - ( 10 * 60 ) ) ) ? 1 : 0; $second_submit = ( $cu->is_community && $recent_create && $remote->can_manage_other($cu) ) ? 1 : 0; $errors->add( "user", ".error.user.inuse" ) unless $second_submit; } unless ( $errors->exist ) { # rate limit return error_ml("/communities/new.tt.error.ratelimited") unless $remote->rate_log( 'commcreate', 1 ); $cu = LJ::User->create_community( user => $new_user, status => $remote->email_status, name => $title, email => $remote->email_raw, membership => $post->{membership}, postlevel => $post->{postlevel}, moderated => $post->{moderated}, nonmember_posting => $post->{nonmember_posting}, journal_adult_settings => $post->{age_restriction}, ) unless $second_submit; return DW::Controller->render_success( 'communities/new.tt', { user => $cu->ljuser_display }, [ { text_ml => ".success.link.settings", url => LJ::create_url( "/manage/settings/", args => { authas => $cu->user, cat => "community" } ), }, { text_ml => ".success.link.profile", url => LJ::create_url( "/manage/profile/", args => { authas => $cu->user } ), }, { text_ml => ".success.link.customize", url => LJ::create_url( "/customize/", args => { authas => $cu->user } ), }, ] ) if $cu; } } else { $get = $r->get_args; } my $vars = { age_restriction_enabled => LJ::is_enabled('adult_content'), errors => $errors, }; $vars->{formdata} = $post || { user => $get->{user}, title => $get->{title}, # initial radio button selection %default_options }; return DW::Template->render_template( 'communities/new.tt', $vars ); } sub convert_handler { my ( $ok, $rv ) = controller( form_auth => 1 ); return $rv unless $ok; my $remote = $rv->{remote}; my $r = $rv->{r}; my $post; # more private / restricted than when we create a new community from scratch my %default_options = ( membership => 'closed', postlevel => 'select', moderated => '0', nonmember_posting => '0', age_restriction => 'none', ); my $errors = DW::FormErrors->new; if ( $r->did_post ) { $post = _enforce_valid_settings( $r->post_args, \%default_options ); my $cuser = LJ::canonical_username( $post->{cuser} ); my $cu = LJ::load_user($cuser); if ($cu) { $errors->add( "cuser", ".error.alreadycomm", { comm => $cu->ljuser_display } ) if $cu->is_community; $errors->add( "cuser", ".error.samenames" ) if $cu->equals($remote); } else { $errors->add( "cuser", ".error.notfound" ) unless $cu; } # only check the password if we have no errors so far unless ( $errors->exist ) { $errors->add( "cpassword", ".error.badpassword" ) if !LJ::auth_okay( $cu, $post->{cpassword} ); } # disallow changing the journal type if the journal has entries if ( !$errors->exist && !$remote->has_priv( "changejournaltype", "" ) ) { my $count; my $userid = $cu->{'userid'} + 0; my $dbcr = LJ::get_cluster_reader($cu); $count = $dbcr->selectrow_array( "SELECT COUNT(*) FROM log2 WHERE journalid=$userid AND posterid=journalid"); $errors->add( "cuser", ".error.hasentries", { user => $cu->ljuser_display } ) if $count; } # disallow changing the journal type if the journal administers any communities unless ( $errors->exist ) { my $admin_of_count = scalar( $cu->communities_managed_list ) || 0; $errors->add( "cuser", ".error.hascommadmin", { count => $admin_of_count } ) if $admin_of_count; } unless ( $errors->exist ) { $cu->update_self( { journaltype => 'C', password => '' } ); $cu->invalidate_directory_record; # handle admin edges LJ::set_rel( $cu, $remote, 'A' ); LJ::set_rel( $cu->userid, $remote->userid, 'M' ) if $post->{moderated} && !LJ::load_rel_user( $cu->userid, 'M' )->[0]; # set community settings $cu->set_comm_settings( $remote, { membership => $post->{membership}, postlevel => $post->{postlevel}, } ); $cu->set_prop( { nonmember_posting => $post->{nonmember_posting}, moderated => $post->{moderated}, } ); # delete existing watchlist & trustlist foreach ( $cu->watched_users ) { $cu->remove_edge( $_, watch => {} ); } foreach ( $cu->trusted_users ) { $cu->remove_edge( $_, trust => {} ); } # log this to statushistory my $msg = "account '" . $cu->user . "' converted to community"; $msg .= " (maintainer is '" . $remote->user . "')"; LJ::statushistory_add( $cu, $remote, "change_journal_type", $msg ); # lazy-cleanup: if a community has subscriptions (most likely # due to a personal->comm conversion), nuke those subs. # (since they can't manage them anyway!) $cu->delete_all_subscriptions; # ... and migrate their interests to the right table $cu->lazy_interests_cleanup; LJ::Hooks::run_hook( "change_journal_type", $cu ); return DW::Controller->render_success( 'communities/convert.tt', { comm => $cu->ljuser_display }, [ { text_ml => ".success.link.settings", url => LJ::create_url( "/manage/settings/", args => { authas => $cu->user, cat => "community" } ), }, { text_ml => ".success.link.profile", url => LJ::create_url( "/manage/profile/", args => { authas => $cu->user } ), }, { text_ml => ".success.link.customize", url => LJ::create_url( "/customize/", args => { authas => $cu->user } ), }, ] ); } } my $vars = { errors => $errors, formdata => $post || \%default_options, admin_user => $remote->ljuser_display, }; return DW::Template->render_template( 'communities/convert.tt', $vars ); } sub _revoke_invitation { my ( $cu, $post, $errors ) = @_; my $target_uid = $post->{revoke} + 0; my $target_u = LJ::load_userid($target_uid); $errors->add( undef, "error.nojournal" ) and return unless $target_u; $cu->revoke_invites( $target_u->userid ); } sub _invite_new_member { my ( $cu, $post, $errors, %opts ) = @_; my $remote = $opts{remote}; my $num_rows = $opts{rows}; my $default_checked_roles = $opts{default_roles}; my $form_to_invite_attrib = $opts{form_to_invite_attrib}; foreach my $num ( 1 .. $num_rows ) { my $user_field = "user_$num"; my $role_field = "user_role_$num"; my $given_user = LJ::ehtml( LJ::trim( $post->{$user_field} ) ); next unless $given_user; my $invited_u = LJ::load_user_or_identity($given_user); $errors->add( $user_field, ".error.no_user", { user => $given_user } ) and next unless $invited_u; $errors->add( $user_field, ".error.not_active", { user => $invited_u->ljuser_display } ) and next unless $invited_u->is_visible; my @roles_for_user = $post->get_all($role_field); $errors->add( $user_field, ".error.no_role", { user => $invited_u->ljuser_display } ) and next unless @roles_for_user; $errors->add( $user_field, ".error.invalid_journaltype", { user => $invited_u->ljuser_display, type => $invited_u->journaltype_readable } ) and next unless $invited_u->is_individual; $errors->add( $user_field, ".error.already_added", { user => $invited_u->ljuser_display } ) and next if $invited_u->member_of($cu); my $adult_content; unless ( $invited_u->can_join_adult_comm( comm => $cu, adultref => \$adult_content ) ) { $errors->add( $user_field, ".error.is_minor", { user => $invited_u->ljuser_display } ) and next if $adult_content eq "explicit"; } # turn on posting access according to community settings my $post_level = $cu->post_level; push @roles_for_user, "poster" if $post_level eq 'members'; # all good, let's extend an invite to this person # these map the form field POSTed to the form expected in send_comm_invite my @attribs = map { $form_to_invite_attrib->{$_} } uniq @roles_for_user; if ( $invited_u->send_comm_invite( $cu, $remote, \@attribs ) ) { # succeeded, clear from the form so they don't display again $post->remove($user_field); $post->set( "user_role_$num", @$default_checked_roles ); } else { my $error_ml = { "comm_user_has_banned" => '.error.banned', "comm_invite_limit" => '.error.limit' }->{ LJ::last_error_code() }; $error_ml ||= ".error.unknown"; $errors->add( $user_field, $error_ml, { user => $invited_u->ljuser_display } ); } } } sub members_new_handler { my ( $opts, $community ) = @_; my ( $ok, $rv ) = controller( form_auth => 1 ); return $rv unless $ok; my $cu = LJ::load_user($community); return error_ml("error.nocomm") unless $cu; return error_ml( "error.communities.notcomm", { user => $cu->ljuser_display, } ) unless $cu->is_comm; my $remote = $rv->{remote}; return error_ml( "error.communities.noaccess", { comm => $cu->ljuser_display, } ) unless $remote->can_manage_other($cu); my $r = $rv->{r}; my $get = $r->get_args; my $num_rows = 5; my @default_checked_roles = qw( member poster ); my %form_to_invite_attrib = ( admin => 'admin', poster => 'post', member => 'member', moderator => 'moderate', unmoderated => 'preapprove' ); my %invite_attrib_to_form = reverse %form_to_invite_attrib; my $errors = DW::FormErrors->new; my $post; if ( $r->did_post ) { $post = $r->post_args; if ( $post->{revoke} ) { _revoke_invitation( $cu, $post, $errors, ); } else { _invite_new_member( $cu, $post, $errors, remote => $remote, rows => $num_rows, form_to_invite_attrib => \%form_to_invite_attrib, default_roles => \@default_checked_roles, ); } } # figure out what member roles are relevant my @available_roles = ('member'); push @available_roles, 'poster' if $cu->post_level eq 'select'; push @available_roles, qw( unmoderated moderator ) if $cu->has_moderated_posting; push @available_roles, 'admin'; # now get sent invites and the users involved my $sent = $cu->get_sent_invites || []; my @ids; push @ids, ( $_->{userid}, $_->{maintid} ) foreach @$sent; my $us = LJ::load_userids(@ids); # filter by status if desired my @valid_statuses = qw( outstanding accepted rejected ); my %valid_statuses = map { $_ => 1 } @valid_statuses; my $status_filter = $get->{status} || ''; $status_filter = "all" unless $valid_statuses{$status_filter}; my @filters = ( { text => ".invite.filter.all", url => LJ::create_url(undef), active => $status_filter eq "all", } ); push @filters, { text => ".invite.filter.$_", url => LJ::create_url( undef, args => { status => $_ } ), active => $status_filter eq $_, } foreach @valid_statuses; # populate %users hash my %users = (); my %usernames; my @available_roles_for_invite = map { $form_to_invite_attrib{$_} } @available_roles; foreach my $invite (@$sent) { my $id = $invite->{userid}; next unless $status_filter eq 'all' || $status_filter eq $invite->{status}; my $name = $us->{$id}{user}; $users{$id}{userid} = $id; $users{$id}{invited_by} = LJ::ljuser( $us->{ $invite->{maintid} }{user} ); $users{$id}{user} = LJ::ljuser($name); $users{$id}{roles} = [ map { $invite_attrib_to_form{$_} } grep { $invite->{args}->{$_} } @available_roles_for_invite ]; $users{$id}{status} = $invite->{status}; $users{$id}{date} = LJ::diff_ago_text( $invite->{recvtime} ); } my $page = int( $get->{page} || 0 ) || 1; my $page_size = 100; my @users = sort { $a->{user} cmp $b->{user} } values %users; my $total_pages = ceil( scalar @users / $page_size ); @users = _items_for_this_page( $page, $page_size, @users ); # if we invited new members, respect the checkboxes from the form submission # if we revoked an invitation, use the default roles # if we just loaeded the page, use the default roles my $formdata = $post && !$post->{revoke} ? $post : { map { "user_role_" . $_ => \@default_checked_roles } ( 1 .. $num_rows ) }; my $vars = { roles => \@available_roles, rows => $num_rows, has_active_filter => $status_filter eq "all" ? 0 : 1, sentinvite_filters => \@filters, sentinvite_pages => { current => $page, total_pages => $total_pages }, sentinvite_list => \@users, formdata => $formdata, errors => $errors, form_invite_action_url => LJ::create_url(undef), form_revoke_action_url => LJ::create_url( undef, keep_args => [qw( status page )] ), linkbar => _community_menu( $remote, $cu, current_page => 'invites', path => "/communities/members/new" ), }; return DW::Template->render_template( 'communities/members/new.tt', $vars ); } sub members_edit_handler { my ( $opts, $cuser ) = @_; my ( $ok, $rv ) = controller( form_auth => 1 ); return $rv unless $ok; my $r = $rv->{r}; my $remote = $rv->{remote}; my $get = $r->get_args; # now get lists of: members, admins, able to post, moderators my %roletype_to_readable = _roletype_map(); my %readable_to_roletype = reverse %roletype_to_readable; my @roles = keys %readable_to_roletype; my $cu = LJ::load_user($cuser); return error_ml("error.nocomm") unless $cu; return error_ml( "error.communities.notcomm", { user => $cu->ljuser_display, } ) unless $cu->is_comm; return error_ml( "/communities/members/edit.tt.error.noaccess", { comm => $cu->ljuser_display, } ) unless $remote->can_manage_other($cu); # handle post my @messages; my @roles_changed; my $errors = DW::FormErrors->new; if ( $r->did_post ) { my $post = $r->post_args; my %was; my %current; foreach my $role (@roles) { # quick lookup for checkboxes that were checked on page load (old values{}) foreach my $uid ( $post->get_all( $role . "_old" ) ) { $was{$uid}->{$role} = 1; } # and same for current values foreach my $uid ( $post->get_all($role) ) { $current{$uid}->{$role} = 1; } } # preload the users we're dealing with # assumes that every user has at least one checked checkbox... # but that seems to be a fair assumption my @preload_userids = grep { $_ } map { $_ + 0 } keys %was; my %us = %{ LJ::load_userids(@preload_userids) }; # now compare userids in %current to %was # to determine which to add and which to delete my ( %add, %delete ); # role -> userid mapping my ( %add_user_to_role, %delete_user_to_role ); # userid -> role mappings foreach my $uid (@preload_userids) { foreach my $role (@roles) { if ( $current{$uid}->{$role} && !$was{$uid}->{$role} ) { $add{$role}->{$uid} = 1; $add_user_to_role{$uid}->{$role} = 1; } elsif ( $was{$uid}->{$role} && !$current{$uid}->{$role} ) { $delete{$role}->{$uid} = 1; $delete_user_to_role{$uid}->{$role} = 1; } } } ######## ## ADD # members are a special-case, because we need to ask permission first foreach my $uid ( keys %{ $add{member} || {} } ) { my $add_u = $us{$uid}; next unless $add_u; if ( $remote->equals($add_u) ) { # you're allowed to add yourself as member $remote->join_community($cu); } else { if ( $add_u && $add_u->send_comm_invite( $cu, $remote, ['member'] ) ) { push @messages, [ ".msg.invite", { user => $add_u->ljuser_display, invite_url => "$LJ::SITEROOT/manage/invites" } ]; } } } # admins also need special handling: they should be notified that they've been added foreach my $uid ( keys %{ $add{admin} || {} } ) { my $add_u = $us{$uid}; next unless $add_u; $cu->notify_administrator_add( $add_u, $remote ); } # go ahead and add poster (P), unmoderated (N), moderator (M), admin (A) edges unconditionally my $cid = $cu->userid; LJ::set_rel_multi( ( map { [ $cid, $_, 'A' ] } keys %{ $add{admin} || {} } ), ( map { [ $cid, $_, 'P' ] } keys %{ $add{poster} || {} } ), ( map { [ $cid, $_, 'M' ] } keys %{ $add{moderator} || {} } ), ( map { [ $cid, $_, 'N' ] } keys %{ $add{unmoderated} || {} } ), ); ########## ## DELETE # delete members foreach my $uid ( keys %{ $delete{member} || {} } ) { my $del_u = $us{$uid}; next unless $del_u; $del_u->remove_edge( $cid, member => {} ); } # admins are a special case: we need to make sure we don't remove all admins from the community # we load the admin_users in bulk separately, because this list might include admins that weren't available on this page # (but we still want to be able to load them up to check their visibility status) my %admin_users = %{ LJ::load_userids( $cu->maintainer_userids ) }; my %admins_to_delete = %{ $delete{admin} || {} }; my @remaining_admins = grep { !$admins_to_delete{$_} # admins we want to delete on this page load && $admin_users{$_} # is an existing user && !$admin_users{$_}->is_expunged # that is not expunged } $cu->maintainer_userids; unless (@remaining_admins) { $errors->add( "admin", ".error.no_admin", { comm => $cu->ljuser_display } ); # refuse to delete any admins $delete{admin} = {}; } # now notify admins that we're deleting foreach my $uid ( keys %{ $delete{admin} || {} } ) { my $del_u = $us{$uid}; next if !$del_u || $del_u->is_expunged; $cu->notify_administrator_remove( $del_u, $remote ); } # go ahead and delete poster (P), unmoderated (N), moderator (M), admin (A) edges unconditionally LJ::clear_rel_multi( ( map { [ $cid, $_, 'A' ] } keys %{ $delete{admin} || {} } ), ( map { [ $cid, $_, 'P' ] } keys %{ $delete{poster} || {} } ), ( map { [ $cid, $_, 'M' ] } keys %{ $delete{moderator} || {} } ), ( map { [ $cid, $_, 'N' ] } keys %{ $delete{unmoderated} || {} } ), ); ############### ## CLEAR CACHE # delete reluser memcache key LJ::MemCache::delete( [ $cid, "reluser:$cid:A" ] ); LJ::MemCache::delete( [ $cid, "reluser:$cid:P" ] ); LJ::MemCache::delete( [ $cid, "reluser:$cid:M" ] ); LJ::MemCache::delete( [ $cid, "reluser:$cid:N" ] ); #################### ## SUCCESS MESSAGES # now show messages for each succesful change we did my %done; my %role_strings = map { $_ => LJ::Lang::ml("/communities/members/edit.tt.role.$_") } %readable_to_roletype; my $remote_uid = $remote->userid; foreach my $uid ( keys %add_user_to_role, keys %delete_user_to_role ) { next if $done{$uid}++; my $u = $us{$uid}; next unless $u; if ( $post->{"action:purge"} ) { push @roles_changed, { user => $u->ljuser_display, purged => 1 }; } else { my ( $changed_roles_msg, @added_roles, @removed_roles ); push @added_roles, $role_strings{$_} foreach grep { $_ ne "member" # reinvited members need to confirm, so we don't want a success message || $uid == $remote_uid # but if you're adding yourself as a member, that's fine } keys %{ $add_user_to_role{$uid} || {} }; push @removed_roles, $role_strings{$_} foreach keys %{ $delete_user_to_role{$uid} || {} }; push @roles_changed, { user => $u->ljuser_display, added => \@added_roles, removed => \@removed_roles } if @added_roles || @removed_roles; } } } my @role_filters = split ",", $get->{role} || ""; @role_filters = grep { $_ } # make sure it's a valid role map { $readable_to_roletype{$_} } @role_filters; my %active_role_filters = map { $roletype_to_readable{$_} => 1 } @role_filters; my ( $users, $role_count ); if ( $get->{q} ) { # we return results for just this user my $query_u = LJ::load_user( $get->{q} ); ( $users, $role_count ) = $cu->get_member($query_u); } else { # grab the list of users (optionally by role) ( $users, $role_count ) = $cu->get_members_by_role( \@role_filters ); } my $page = int( $get->{page} || 0 ) || 1; my $page_size = 100; my @users = sort { $a->{name} cmp $b->{name} } values %$users; # pagination: # calculate the number of pages # take the results and choose only a slice for display my $total_pages = ceil( scalar @users / $page_size ); @users = _items_for_this_page( $page, $page_size, @users ); # populate with the ljuser tag for display $_->{ljuser} = LJ::ljuser( $_->{name} ) foreach @users; # figure out what member roles are relevant my @available_roles = ('member'); push @available_roles, 'poster' if $cu->post_level eq 'select' || $role_count->{P}; my $has_moderated_posting = $cu->has_moderated_posting; push @available_roles, 'unmoderated' if $has_moderated_posting || $role_count->{N}; push @available_roles, 'moderator' if $has_moderated_posting || $role_count->{M}; push @available_roles, 'admin'; # create a data structure for the links to filter members my $filter_link = sub { my $filter = $_[0]; return { text => ".role.$filter", url => LJ::create_url( undef, args => { role => "$filter" } ), active => $active_role_filters{$filter} ? 1 : 0, }, ; }; my @filter_links = ( { text => ".role.all", url => LJ::create_url(undef), active => ( scalar keys %active_role_filters ) || $get->{q} ? 0 : 1, } ); push @filter_links, $filter_link->($_) foreach @available_roles; # data for the checkboxes in the form of: # { # role => [ userids ], ... # } my $membership_statuses = Hash::MultiValue->new; my @roletype_keys = keys %roletype_to_readable; foreach my $user ( values %$users ) { foreach my $roletype (@roletype_keys) { $membership_statuses->add( $roletype_to_readable{$roletype}, $user->{userid} ) if $user->{$roletype}; } } my $vars = { community => $cu, user_list => \@users, roles => \@available_roles, filter_links => \@filter_links, pages => { current => $page, total_pages => $total_pages }, has_active_filter => keys %active_role_filters ? 1 : 0, formdata => $membership_statuses, messages => \@messages, roles_changed => \@roles_changed, errors => $errors, form_edit_action_url => LJ::create_url( undef, keep_args => [qw( role page )] ), form_search_action_url => LJ::create_url(undef), form_purge_action_url => LJ::create_url("/communities/members/purge"), linkbar => _community_menu( $remote, $cu, current_page => 'members', path => '/communities/members/edit' ), }; return DW::Template->render_template( 'communities/members/edit.tt', $vars ); } sub members_purge_handler { my ($opts) = @_; my ( $ok, $rv ) = controller( form_auth => 0 ); return $rv unless $ok; my $r = $rv->{r}; my $post = $r->post_args; my $remote = $rv->{remote}; my $cu = LJ::load_user( $post->{authas} ); return error_ml("error.nocomm") unless $cu; return error_ml( "error.communities.notcomm", { user => $cu->ljuser_display, } ) unless $cu->is_comm; return error_ml( "error.communities.noaccess", { comm => $cu->ljuser_display, } ) unless $remote->can_manage_other($cu); my $members = LJ::load_userids( $cu->member_userids ); my @purged = map { name => $_->ljuser_display, id => $_->userid }, sort { $a->user cmp $b->user } grep { $_ && $_->is_expunged } values %$members; my $members_url = LJ::create_url( "/communities/" . $cu->user . "/members/edit" ); my $vars = { user_list => \@purged, community => $cu, roles => [qw( admin poster member moderator unmoderated )], form_action => $members_url, members_url => $members_url, linkbar => _community_menu( $remote, $cu, path => '/communities/members/edit' ), }; return DW::Template->render_template( 'communities/members/purge.tt', $vars ); } sub entry_queue_handler { my ( $opts, $community ) = @_; my ( $ok, $rv ) = controller( form_auth => 0 ); return $rv unless $ok; my $cu = LJ::load_user($community); my ( $can_moderate, @error ) = _check_entry_queue_auth( $cu, $rv->{remote} ); return error_ml(@error) unless $can_moderate; my $r = $rv->{r}; my @queue = $cu->get_mod_queue; my %users; LJ::load_userids_multiple( [ map { $_->{posterid}, \$users{ $_->{posterid} } } @queue ] ); my @entries = map { { time => LJ::diff_ago_text( LJ::mysqldate_to_time( $_->{logtime} ) ), poster => $users{ $_->{posterid} }->ljuser_display, subject => $_->{subject}, subject_maxlength => ( length( $_->{subject} ) >= 29 ) ? 1 : 0, url => $cu->moderation_queue_url( $_->{modid} ), } } @queue; my $vars = { entries => \@entries, linkbar => _community_menu( LJ::get_remote(), $cu, current_page => 'queue', path => '/communities/queue/entries' ), community => $cu, }; return DW::Template->render_template( 'communities/queue/entries.tt', $vars ); } sub entry_queue_edit_handler { my ( $opts, $community, $modid ) = @_; $modid = int $modid; my ( $ok, $rv ) = controller( form_auth => 1 ); return $rv unless $ok; my $cu = LJ::load_user($community); my ( $can_moderate, @error ) = _check_entry_queue_auth( $cu, $rv->{remote} ); return error_ml(@error) unless $can_moderate; my $moderated_entry = DW::Entry::Moderated->new( $cu, $modid ); return error_ml("/communities/queue/entries/edit.tt.error.no_entry") unless $moderated_entry; my $r = $rv->{r}; my $errors = DW::FormErrors->new; if ( $r->did_post ) { my $post = $r->post_args; my $status_vars = { queue_url => $cu->moderation_queue_url }; return error_ml("/communities/queue/entries/edit.tt.error.no_entry") unless $moderated_entry->auth eq $post->{auth}; if ( $post->{"action:approve"} || $post->{"action:preapprove"} ) { my ( $approve_ok, $approve_rv ) = $moderated_entry->approve; if ($approve_ok) { $moderated_entry->notify_poster( "approved", entry_url => $approve_rv, message => $post->{message} ); $status_vars->{status} = "approved"; $status_vars->{entry_url} = $approve_rv; } else { $moderated_entry->notify_poster( "error", error => $approve_rv ); $errors->add( undef, "/communities/queue/entries/edit.tt.error.post", { error => $approve_rv } ); } if ( $post->{"action:preapprove"} ) { LJ::set_rel( $moderated_entry->journal, $moderated_entry->poster, 'N' ); $status_vars->{preapproved} = 1; $status_vars->{user} = $moderated_entry->poster->ljuser_display; $status_vars->{community} = $moderated_entry->journal->ljuser_display; } } if ( $post->{"action:reject"} || $post->{"action:spam"} ) { my $reject_ok = $post->{"action:spam"} ? $moderated_entry->reject_as_spam : $moderated_entry->reject; $moderated_entry->notify_poster( "rejected", message => $post->{message}, ) if $reject_ok; $status_vars->{status} = "rejected"; $errors->add( undef, ".error.cant_spam" ) unless $reject_ok; } return DW::Template->render_template( 'communities/queue/entries/edit-status.tt', $status_vars ) unless $errors->exist; } my $vars = { entry => { icon => $moderated_entry->icon, poster => $moderated_entry->poster, journal => $moderated_entry->journal, time => $moderated_entry->time( linkify => 1 ), subject => $moderated_entry->subject, event => $moderated_entry->event, age_restriction_reason => $moderated_entry->age_restriction_reason, auth => $moderated_entry->auth, currents_html => $moderated_entry->currents_html, security_html => $moderated_entry->security_html, age_restriction_html => $moderated_entry->age_restriction_html, }, moderate_url => LJ::create_url(undef), can_report_spam => LJ::sysban_check( 'spamreport', $cu->user ) ? 0 : 1, errors => $errors, linkbar => _community_menu( LJ::get_remote(), $cu, path => '/communities/queue/entries' ), }; return DW::Template->render_template( 'communities/queue/entries/edit.tt', $vars ); } sub members_queue_handler { my ( $opts, $community ) = @_; my ( $ok, $rv ) = controller( form_auth => 1 ); return $rv unless $ok; my $r = $rv->{r}; my $remote = $rv->{remote}; my $get = $r->get_args; my $cu = LJ::load_user($community); return error_ml("error.nocomm") unless $cu; return error_ml( "/communities/queue/members.tt.error.noaccess", { comm => $cu->ljuser_display, } ) unless $remote->can_manage_other($cu); # now load all users with a pending membership request my $pendids = $cu->get_pending_members || []; my $us = LJ::load_userids(@$pendids); my @success_msgs; if ( $r->did_post ) { my $post = $r->post_args; my @statuses = qw( approve reject ban ban_skip previously_handled ); my %status_count = map { $_ => 0 } @statuses; $post->each( sub { my ( $key, $action ) = @_; my $uid; return unless ($uid) = $key =~ m/user_(\d+)/; my $pending_u = $us->{$uid}; if ( !$pending_u ) { # POSTed but not in pending users. Looks like it was handled by someone else $status_count{previously_handled}++; } elsif ( $action eq "approve" ) { $cu->approve_pending_member($pending_u); $status_count{approve}++; } elsif ( $action eq "reject" ) { $cu->reject_pending_member($pending_u); $status_count{reject}++; } elsif ( $action eq "ban" ) { my $banlist = LJ::load_rel_user( $cu, 'B' ) || []; if ( scalar(@$banlist) >= ( $LJ::MAX_BANS || 5000 ) ) { $status_count{ban_skip}++; } else { $cu->ban_user($pending_u); $status_count{ban}++; # ban is successful, reject member $cu->reject_pending_member($pending_u); # only in case of successful ban } } } ); foreach my $status (@statuses) { push @success_msgs, { ml => ".success.$status", num => $status_count{$status} } if $status_count{$status}; } # get the list of pending members again; may have changed $pendids = $cu->get_pending_members || []; $us = LJ::load_userids(@$pendids); } my $page = int( $get->{page} || 0 ) || 1; my $page_size = 100; my @users = sort { $a->{user} cmp $b->{user} } values %$us; # pagination: # calculate the number of pages # take the results and choose only a slice for display my $total_pages = ceil( scalar @users / $page_size ); @users = _items_for_this_page( $page, $page_size, @users ); my $vars = { user_list => [ map { { userid => $_->userid, ljuser => $_->ljuser_display, } } @users ], pages => { current => $page, total_pages => $total_pages }, messages => \@success_msgs, form_queue_action_url => LJ::create_url( undef, keep_args => [qw( page )] ), linkbar => _community_menu( $remote, $cu, path => '/communities/queue/members' ), }; return DW::Template->render_template( 'communities/queue/members.tt', $vars ); } sub member_approve_handler { my ( $opts, $aaid, $auth ) = @_; my ( $ok, $rv ) = controller( anonymous => 1 ); return $rv unless $ok; return _member_action_handler( $rv, aaid => $aaid, auth => $auth, action => "approve" ); } sub member_reject_handler { my ( $opts, $aaid, $auth ) = @_; my ( $ok, $rv ) = controller( anonymous => 1 ); return $rv unless $ok; return _member_action_handler( $rv, aaid => $aaid, auth => $auth, action => "reject" ); } sub _member_action_handler { my ( $rv, %opts ) = @_; my $r = $rv->{r}; my $aaid = $opts{aaid}; my $auth = $opts{auth}; my $action = $opts{action}; my $ml_scope = '/communities/members/action.tt'; my $aa = LJ::is_valid_authaction( $aaid, $auth ); return error_ml( $ml_scope . '.error.invalidargument' ) unless $aa; return error_ml( $ml_scope . '.error.actionperformed' ) if $aa->{used} eq 'Y'; my $arg = {}; LJ::decode_url_string( $aa->{arg1}, $arg ); my $dbh = LJ::get_db_writer(); # get user we're adding my $targetu = LJ::load_userid( $arg->{targetid} ); return error_ml( $ml_scope . '.error.internerr.invalidaction' ) unless $targetu; if ( $aa->{action} eq 'comm_join_request' ) { # add to community my $cu = LJ::load_userid( $aa->{userid} ); return error_ml( $ml_scope . '.error.' . $action ) unless $cu; my $did_succeed; if ( $action eq "approve" ) { $did_succeed = $cu->approve_pending_member($targetu); } else { $did_succeed = $cu->reject_pending_member($targetu); } return error_ml( $ml_scope . '.error.' . $action ) unless $did_succeed; return DW::Controller->render_success( 'communities/members/action.tt.' . $action, { user => $targetu->ljuser_display, comm => $cu->ljuser_display, url => $cu->community_manage_members_url, } ); } } # convenience methods # return the appropriate slice from the full array for this page # ideally we'd do this when fetching from the DB sub _items_for_this_page { my ( $page, $page_size, @items ) = @_; my $first = ( $page - 1 ) * $page_size; my $num_items = scalar @items; my $last = $page * $page_size; $last = $num_items if $last > $num_items; $last = $last - 1; return @items[ $first ... $last ]; } # returns ( $can_moderate, ".error_ml", { error_ml_args => foo } ) sub _check_entry_queue_auth { my ( $cu, $remote ) = @_; my $ml_scope = "/communities/queue/entries.tt"; return ( 0, "error.nocomm" ) unless $cu; unless ( $remote->can_moderate($cu) ) { return ( 0, "$ml_scope.error.noaccess", { comm => $cu->ljuser_display } ) if $cu->has_moderated_posting; return ( 0, "$ml_scope.error.notmoderated" ); } } sub _roletype_map { return ( A => 'admin', P => 'poster', E => 'member', M => 'moderator', N => 'unmoderated' ); } sub _community_menu { my ( $u, $cu, %opts ) = @_; my $current_page = $opts{current_page} || ''; my $path = $opts{path}; return "
" . "
" . LJ::make_authas_select( $u, { type => 'C', authas => $cu->user, foundation => 1 } ) . "
" . $cu->maintainer_linkbar( $current_page, 1 ) . "
"; } 1;