package DW::Controller::Talk;

use strict;
use LJ::JSON;
use DW::Controller;
use DW::Routing;
use DW::Template;
use DW::Formats;

DW::Routing->register_string( '/talkpost_do', \&talkpost_do_handler, app => 1 );
DW::Routing->register_string( '/talkmulti',   \&talkmulti_handler,   app => 1 );
DW::Routing->register_string( '/talkscreen',  \&talkscreen_handler,  app => 1 );
DW::Routing->register_string( '/delcomment',  \&delcomment_handler,  app => 1 );
DW::Routing->register_rpc(
    "delcomment", \&delcomment_handler,
    format  => 'json',
    methods => { POST => 1 }
);
DW::Routing->register_rpc(
    "talkscreen", \&talkscreen_handler,
    format  => 'json',
    methods => { POST => 1 }
);

# I think this canon isn't written down anywhere else, so HWAET: I sing a record
# of every damn form field that comes through in reply form POSTs. -NF
#
# Form names:
# - qrform
#     The quick-reply form, built by the LJ::create_qr_div function, in
#     LJ/Web.pm.
# - postform
#     The talkform, built by the LJ::Talk::talkform function.
# - There used to also be a previewform, but it was a menace and I killed it.
#
# Actual comment content (which gets saved to the DB):
# - subject
# - body
# - prop_picture_keyword
#     The user icon for this comment.
# - prop_opt_preformatted
#     The "don't autoformat" checkbox, which switches a comment from casual HTML
#     to raw HTML. Deprecated in favor of prop_editor and named formats. No
#     longer included in the forms, but can potentially come in via Protocol.
# - prop_editor
#     The markup format the comment's body text uses. See DW::Formats for more info.
# - prop_admin_post
#     Whether this comment is from a community admin (and should thus be displayed
#     specially).
# - subjecticon
#     The "subject icon" for this comment, chosen from a hardcoded list of ~32.
#     Only available in talkform.
# - editreason
#     An explanation for the edit; only present if editing.
#
# Sort of like comment content, but not quite:
# - unscreen_parent
#     Whether to unscreen the screened comment they're replying to. Only available
#     if this is a journal where the commenter can do that, and only shown in the
#     talkform. I think this is meant as a convenience for the no-javascript case,
#     because otherwise the AJAX unscreen button is faster and more intuitive.
#
# Identity stuff:
# - usertype
#     The type of user submitting the comment. In the quick-reply, this is locked
#     to `cookieuser`. In the talkform, it's a group of radio buttons so you can
#     switch user/go anon, and the value will be one of the following:
#         - anonymous
#         - openid
#         - openid_cookie
#         - cookieuser
#         - user
#     All of the identity fields mostly get consumed by the Talk controller, but
#     they can also influence the initial state of the talkform if it gets
#     regenerated partway through a comment submission (when previewing or when
#     there's an error that needs fixing). These fields are documented more fully
#     down in authenticate_user_and_mutate_form.
# - oidurl
# - oiddo_login
# - cookieuser
# - userpost
# - password
# - do_login
#
# Hidden fields that determine what the comment is replying to:
# - journal
#     The journal name for the entry they're replying to.
# - itemid
#     The ditemid (obfuscated display ID) of the entry they're replying to.
#     See comments in the implementation of LJ::Talk::talkform for more info about
#     the different ID formats.
# - parenttalkid
#     The jtalkid (raw ID) of the comment they're replying to. If they're replying
#     directly to the entry, this is 0.
# - replyto
#     An exact duplicate of parenttalkid;
#     LJ::Talk::Post::prepare_and_validate_comment will fall back to this if
#     parenttalkid isn't present, which should never be the case. Nothing else
#     uses this, but jquery.quickreply.js confirms that it's present before
#     allowing you to continue. LJ::Comment->create omits replyto in the mock form
#     it sends to prep/validate.
# - editid
#     The dtalkid (obfuscated display ID) of the existing comment to be edited. If
#     posting a new comment, this is 0.
#
# Consistency checks and metadata:
# - lj_form_auth
#     Consistency check for CSRF prevention. Similar idea to chrp1, but checks more
#     stuff. See LJ::check_form_auth in Web.pm for more.
# - chrp1
#     A time-expiring server-provided token used to make spam posts inconvenient.
# - captcha_type (and other captcha fields)
#     Which captcha implementation to use, if a captcha is deemed necessary;
#     absent if not. Captchas can bring in additional form fields. These get
#     consumed by the captcha implementations, which get invoked down near the
#     bottom of LJ::Talk::Post::prepare_and_validate_comment.
# - qr
#     Hardcoded to 1 in the quick-reply form; absent in talkform. As far as I can
#     tell, nothing ever consumes this. Maybe was for some ancient server log
#     metrics?
#
# Things that affect the return link after replying:
# - viewing_thread
#     The filtered thread view (`?thread=12345`) they were in when they hit the
#     "reply" link, and which they should be returned to once they finish posting.
#     Consumed by Talk controller to build the return link.
# - style/format/s2id/fallback
#     The "viewing style" options (`?style=light`) that were in effect when they
#     hit the "reply" link, which should be re-instated once they finish
#     posting. You get one hidden input for each of these that was present,
#     although usually there's only one and the effects of mixing them seem
#     obscure. See LJ::viewing_style_opts (in Web.pm) for more info.
#
# Enablers for quick-reply's "more options" button:
# - dtid
#     The dtalkid (obfuscated display ID) of the comment they're replying to (in
#     other words, the display version of `parenttalkid`). Only used by the
#     quick-reply JS for building the "more options" URL. Nothing on the backend
#     ever consumes this.
# - basepath
#     The path to the journal entry they're replying to, with any viewing style
#     options included. The quick-reply JS uses this for building the "more
#     options" URL. Nothing on the backend ever consumes this.
#
# Submit buttons and their friends:
# - submitpost
#     The post button. Nothing on the backend listens for this by name; it's just
#     the button that does a "vanilla" form submission.
# - submitpreview
#     The actual signal to the backend that we need to build a preview for this
#     comment instead of posting it. Has indirect behavior sometimes because we
#     disable the post buttons with JS to prevent double-submits, and browsers
#     don't send disabled inputs. In the quick-reply, this is a hidden input whose
#     value gets set to 1 if they click `submitpview`. In the talkform, this is
#     the name of the preview button, so it gets sent normally if JS is disabled,
#     but see also `previewplaceholder`.
# - submitpview
#     The "preview" submit button in the quick-reply form. Nothing on the backend
#     listens for this by name; it just sets the value of the hidden
#     `submitpreview` input.
# - previewplaceholder
#     A hidden input in the talkform whose value is 1. If JS is enabled, we change
#     its name to `submitpreview` when they click the preview button, so the
#     signal to preview will reach the backend despite the submit buttons being
#     disabled.
# - submitmoreopts
#     The "more options" button in the quick-reply. Changes the action of the form
#     to point to the ReplyPage instead of to talkpost_do (so they can continue
#     with a partially-filled talkform), then submits. Nothing on the backend
#     listens for this by name.

sub talkpost_do_handler {

    # This route handles form_auth manually; we want to get the post args, fail
    # gently, regenerate the comment form, and not lose your text.
    my ( $ok, $rv ) = controller( form_auth => 0, anonymous => 1 );
    return $rv unless $ok;

    my $r      = $rv->{r};
    my $POST   = $r->post_args;
    my $remote = $rv->{remote};
    my $GET    = $r->get_args;
    my $title  = '.success.title';

    my $vars;

    # Like error_ml but for when we don't control the error string.
    my $err_raw = sub {
        return DW::Template->render_template( 'error.tt', { message => $_[0] } );
    };

    # For errors that aren't immediately fatal, collect them as we go and let
    # the user fix them all at once.
    my @errors;

    # Track form_auth specially, since we want to bail on some things if it fails.
    my $form_auth_ok = 0;

    # First, make sure we've got POST data and confirm that it's from a legit
    # local form (and not some CSRF shenanigans). Usually that's a real POST,
    # but sometimes it's a saved POST that we set aside while an OpenID
    # commenter sorted out their identity.
    if ( $r->did_post ) {
        $form_auth_ok = LJ::check_form_auth( $POST->{lj_form_auth} );
        push @errors, LJ::Lang::ml('/talkpost_do.tt.error.invalidform') unless $form_auth_ok;
    }
    elsif ( my $mode = $GET->{'openid.mode'} ) {

        # If they're coming back from an OpenID identity server, restore the
        # POST hash we saved before they left.

        unless ( $GET->{jid}
            && $GET->{pendcid}
            && ( $mode eq 'id_res' || $mode eq 'cancel' ) )
        {
            return error_ml('/talkpost_do.tt.error.badrequest');
        }

        # We only check form_auth for real POSTs; if they're coming back from
        # OpenID, we already checked earlier.
        $form_auth_ok = 1;

        my $csr = LJ::OpenID::consumer( $GET->mixed );

        if ( $mode eq 'id_res' ) {    # Verify their identity

            unless ( LJ::check_referer( '/talkpost_do', $GET->{'openid.return_to'} ) ) {
                return error_ml( '/openid/login.tt.error.invalidparameter',
                    { item => "return_to" } );
            }

            my $errmsg;
            my $uo = LJ::User::load_from_consumer( $csr, \$errmsg );
            return $err_raw->($errmsg) unless $uo;

            # Change who we think we are. NB: don't use set_remote to ACTUALLY
            # change the remote, or you'll cause a glitch in the matrix. We just
            # want to use this OpenID user below to check auth, etc.
            $remote = $uo;
        }

        # Restore their data to reset state where they were
        my $pendcid = $GET->{pendcid} + 0;

        my $journalu = LJ::load_userid( $GET->{jid} );
        return error_ml("/talkpost_do.tt.error.openid.nodb") unless $journalu && $journalu->writer;

        my $pending =
            $journalu->selectrow_array( "SELECT data FROM pendcomments WHERE jid=? AND pendcid=?",
            undef, $journalu->{userid}, $pendcid );

        return error_ml("/talkpost_do.tt.error.openid.nopending") unless $pending;

        my $penddata = eval { Storable::thaw($pending) };

        $POST = $penddata;

        # Not fatal, maybe just decided to be someone else for this comment:
        push @errors, "You chose to cancel your identity verification"
            if $csr->user_cancel;
    }
    else {

        # You didn't post, you aren't coming back from OpenID, wyd??
        return error_ml('/talkpost_do.tt.error.badrequest');
    }

    # We don't call LJ::text_in() here; instead, we call it during
    # LJ::Talk::Post::prepare_and_validate_comment.
    # Old talkpost_do.bml comments said they did this because of non-UTF-8 in
    # "replies coming from mail clients," but nobody in 2020 knew what that
    # meant. (Maybe they just wanted to call it only once, so they put it in a
    # spot where it would also hit replies that come through LJ::Protocol
    # instead of talkpost_do? But you'd think encoding checks should be the
    # concern of the request handler, whether it's Protocol or a controller.)
    # Anyway... the point is it gets called eventually. -NF

    my $journalu = LJ::load_user( $POST->{journal} );
    return error_ml('/talkpost_do.tt.error.nojournal') unless $journalu;

    # This launches some garbage into the void of the Apache "notes" system, and
    # it's impossible to know for sure what ends up reading it as an implicit
    # argument. Obviously everyone hates this. It dates back to the old
    # talkpost_do.bml we inherited from LJ. NF's best guess is that S2.pm
    # expects this, but it might also be irrelevant. Who knows.
    $r->note( 'journalid', $journalu->userid ) if $r;

    unless ( $POST->{itemid} ) {
        return error_ml('talk.error.noentry');
    }
    my $entry   = LJ::Entry->new( $journalu, ditemid => $POST->{itemid} + 0 );
    my $talkurl = $entry->url;

    # validate the challenge/response value (anti-spammer)
    my ( $chrp_ok, $chrp_err ) = LJ::Talk::validate_chrp1( $POST->{'chrp1'} );
    unless ($chrp_ok) {
        if ($LJ::REQUIRE_TALKHASH) {
            push @errors, "Sorry, form expired. Please re-submit."
                if $chrp_err eq "too_old";
            push @errors, "Missing parameters";
        }
    }

    ## Sort out who's posting for real.
    my ( $commenter, $didlogin, $authok, $auth );

    if ($form_auth_ok) {
        ( $authok, $auth ) = authenticate_user_and_mutate_form( $POST, $remote, $journalu );
        if ($authok) {
            if ( $auth->{check_url} ) {

                # openid thing. Round and round we go.
                return $r->redirect( $auth->{check_url} );
            }
            else {
                $commenter = $auth->{user};
                $didlogin  = $auth->{didlogin};
            }
        }
        else {
            push @errors, $auth;
        }
    }

    # Now that we've given them a chance to log in, set a resource group.
    # FIXME: You're supposed to set this in the template, so if someone ever
    # rewrites DW::Captcha::TextCAPTCHA to stop snooping around in
    # $LJ::ACTIVE_RES_GROUP, definitely do that. In the meantime, this needs to
    # happen before LJ::Talk::talkform gets called or things might break. -NF
    my $real_remote = LJ::get_remote();
    if ( !LJ::BetaFeatures->user_in_beta( $real_remote => "nos2foundation" ) ) {
        LJ::set_active_resource_group("foundation");
    }
    else {
        LJ::set_active_resource_group("jquery");
    }

    my $need_captcha = 0;
    my $comment;

    if ($form_auth_ok) {

        # Prepare the comment (or wipe out on the permissions/consistency checks)
        $comment =
            LJ::Talk::Post::prepare_and_validate_comment( $POST, $commenter, $entry,
            \$need_captcha, \@errors );
    }

    # If there's anything in @errors at the end of prepare_and_validate_comment,
    # it returns undef instead of a $comment, even if it wasn't the one to
    # add those errors. So all the "non-fatal" errors we've been logging above
    # become suddenly fatal right... about... NOW.

    # At this point, there's three main paths:
    # 1. Stop and show the requested preview (and report errors inline)
    # 2. Stop and ask user to fix errors
    # 3. Continue and post the comment for real.

    # 1. We're previewing!
    # For most errors, we preview anyway; they can fix it while they edit
    # their text. But we DO need to know who they think they are, so let the
    # error path handle auth failures.
    if ( $authok && $POST->{submitpreview} ) {

        # yer a reply page, Harry. (keep consistent behavior by loading same
        # JS/CSS as journal pages.)
        LJ::Talk::init_s2journal_js(
            iconbrowser => 1,
            siteskin    => 1,
            noqr        => 1
        );

        # Plus we're displaying entry/comment content, so the legacy site skins
        # need CSS for that.
        LJ::need_res( { group => 'jquery' }, 'stc/siteviews/layout.css', 'stc/entrypage.css', );

        # If validation failed, just use what we have on hand to build the preview.
        # Might theoretically have borked non-UTF-8, shrug.
        unless ($comment) {
            $comment = {
                u      => $commenter,
                entry  => $entry,
                parent => {
                    talkid => $POST->{replyto} || $POST->{parenttalkid},
                },
                subject         => $POST->{subject},
                body            => $POST->{body},
                subjecticon     => $POST->{subjecticon} eq 'none' ? '' : $POST->{subjecticon},
                preformat       => $POST->{'prop_opt_preformatted'},
                admin_post      => $POST->{'prop_admin_post'},
                picture_keyword => $POST->{'prop_picture_keyword'},
                editor          => $POST->{'prop_editor'},
            };
        }

        my $talkform = LJ::Talk::talkform(
            {
                journalu   => $journalu,
                parpost    => $comment->{parent},
                replyto    => $comment->{parent}->{talkid},
                ditemid    => $comment->{entry}->ditemid,
                do_captcha => $need_captcha,
                errors     => @errors ? \@errors : undef,
                form       => $POST,
            }
        );

        $vars->{title}   = '.title.preview';
        $vars->{preview} = 1;
        $vars->{comment} = preview_comment_args($comment);
        $vars->{parent}  = preview_parent_args($comment);
        $vars->{html}    = $talkform;
        return DW::Template->render_template( 'talkpost_do.tt', $vars );
    }

    # 2. Validation failed!
    # Don't continue; report errors, ask for help, and regenerate the
    # form. We repopulate what we can via hidden fields, but the objects
    # (journalu & parpost) must be recreated here.
    unless ($comment) {
        my ( $sth, $parpost );
        my $dbcr = LJ::get_cluster_def_reader($journalu);
        return error_ml('/talkpost_do.tt.error.nodb') unless $dbcr;

        $sth = $dbcr->prepare(
            "SELECT posterid, state FROM talk2 " . "WHERE journalid=? AND jtalkid=?" );
        $sth->execute( $journalu->{userid}, $POST->{itemid} + 0 );
        $parpost = $sth->fetchrow_hashref;

        # yer a reply page, Harry. (keep consistent behavior by loading same
        # JS/CSS as journal pages.)
        LJ::Talk::init_s2journal_js(
            iconbrowser => 1,
            siteskin    => 1,
            noqr        => 1
        );

        my $talkform = LJ::Talk::talkform(
            {
                journalu   => $journalu,
                parpost    => $parpost,
                replyto    => $POST->{replyto} || $POST->{parenttalkid},
                ditemid    => $POST->{itemid},
                do_captcha => $need_captcha,
                errors     => \@errors,
                form       => $POST,
            }
        );
        $vars->{title} = '.title.error';
        $vars->{html}  = $talkform;
        return DW::Template->render_template( 'talkpost_do.tt', $vars );
    }

    # 3. It's go time!!
    # We might show a page at the end anyway (for example, if the user logged
    # in), but the most common path is to silently redirect back to the thread
    # after posting.

    my $parent = $comment->{parent};

    my $unscreen_parent = $POST->{unscreen_parent} ? 1 : 0;

    # ACTUALLY POST IT
    my $editid      = $POST->{editid};
    my $wasscreened = ( $parent->{state} eq 'S' );
    my $talkid;

    # check for spam domains
    LJ::Hooks::run_hooks( 'spam_check', $comment->{u}, $comment, 'comment' );

    if ($editid) {
        my ( $postok, $talkid_or_err ) = LJ::Talk::Post::edit_comment($comment);
        unless ($postok) {
            return $err_raw->($talkid_or_err);
        }
        $talkid = $talkid_or_err;
    }
    else {
        my ( $postok, $talkid_or_err ) = LJ::Talk::Post::post_comment( $comment, $unscreen_parent );
        unless ($postok) {
            return $err_raw->($talkid_or_err);
        }
        $talkid = $talkid_or_err;
    }

    # Yeah, we're done.
    my $dtalkid = $talkid * 256 + $entry->{anum};

    # Figure out whether we should offer to update their default formatting.
    my $editor_new;
    if (   $real_remote
        && DW::Formats::is_active( $comment->{editor} )
        && $comment->{editor} ne $real_remote->comment_editor )
    {
        $editor_new = $comment->{editor};
    }

    # Allow style=mine, etc for QR redirects
    my $style_args = LJ::viewing_style_args(%$POST);

# FIXME: potentially can be replaced with some form of additional logic when we have multiple account linkage
    my $posted = $comment->{state} eq 'A' ? "posted=1" : "";

    my $cthread = $POST->{'viewing_thread'} ? "thread=$POST->{viewing_thread}" : "view=$dtalkid";
    my $commentlink = LJ::Talk::talkargs( $talkurl, $cthread, $style_args, $posted )
        . LJ::Talk::comment_anchor($dtalkid);

    my $mlcode;
    if ( $comment->{state} eq 'A' ) {

        # Redirect straight to the post as long as:
        # - it isn't screened
        # - it didn't unscreen its parent
        # - its formatting type didn't change
        # - it didn't log the user in as a side-effect
        if ( !( $wasscreened && ( $parent->{state} ne 'S' ) ) && !$didlogin && !$editor_new ) {
            LJ::set_lastcomment( $journalu->id, $commenter, $dtalkid );
            return $r->redirect($commentlink);
        }

        $mlcode = '.success.message2';
    }
    else {
        # otherwise, it's a screened comment.
        if ( $journalu && $journalu->is_community ) {
            if ( $POST->{usertype} eq 'anonymous' ) {
                $mlcode = '.success.screened.comm.anon3';
            }
            elsif ( $commenter && $commenter->can_manage($journalu) ) {
                $mlcode = '.success.screened.comm.owncomm4';
            }
            else {
                $mlcode = '.success.screened.comm3';
            }
        }
        else {    # not a community
            if ( $POST->{usertype} eq 'anonymous' ) {
                $mlcode = '.success.screened.user.anon3';
            }
            elsif ( $commenter && $commenter->equals($journalu) ) {
                $mlcode = '.success.screened.user.ownjournal3';
            }
            else {
                $mlcode = '.success.screened.user3';
            }
        }
    }
    $vars->{title} = $title;

    my @notices = ( LJ::Lang::ml( "/talkpost_do.tt$mlcode", { aopts => "href='$commentlink'" } ) );
    push @notices,
        DW::Template->template_string(
        'default_editor_form.tt',
        {
            type      => 'comment',
            format    => $DW::Formats::formats{$editor_new},
            exit_text => "Return to comment",
            exit_url  => $commentlink,
        }
        ) if $editor_new;
    push @notices, LJ::Lang::ml('/talkpost_do.tt.success.unscreened')
        if $wasscreened && ( $parent->{state} ne 'S' );
    push @notices, LJ::Lang::ml('/talkpost_do.tt.success.loggedin') if $didlogin;
    $vars->{html} = join( "\n", map { "<p>$_</p>" } @notices );

    return DW::Template->render_template( 'talkpost_do.tt', $vars );
}

# Handles user auth for the talkform's "from" fields.
# Args:
# - $form: a hashref representing the POSTed comment form. We might mutate its
# usertype, userpost, cookieuser, and oidurl fields, in order to canonicalize
# some values or help the comment form react to a change in the global login
# state.
# - $remote: the current logged-in LJ::User, or undef, OR the just-now
# authenticated OpenID user (which is why we can't just get_remote from within).
# - $journalu: LJ::User who owns the journal the comment was submitted to. Need
# this for storing pending comments in first pass through openid auth, and also
# we use it to switch off between variant error messages.
# Returns: (1, result) on success, (0, error) on failure. result is one of:
# - {user => $u, didlogin => $bool} ($u is undef for anons)
# - {check_url => $url} (openid redirect)
sub authenticate_user_and_mutate_form {
    my ( $form, $remote, $journalu ) = @_;

    my $didlogin = 0;

    my $err = sub {
        my $error = shift;
        return ( 0, $error );
    };
    my $mlerr = sub {
        return $err->( LJ::Lang::ml(@_) );
    };
    my $incoherent = sub {
        return $mlerr->("/talkpost_do.tt.error.confused_identity");
    };
    my $got_user = sub {
        my $user = shift;
        return ( 1, { user => $user, didlogin => $didlogin } );
    };

    # The "usertype" field must be one of the following. (Each value might have
    # some associated fields it expects, which are shown as nested lists.)
    # - anonymous
    #   - (nothing)
    # - openid
    #   - oidurl
    #   - oiddo_login
    # - openid_cookie
    #   - (nothing) (in talkform), OR:
    #   - cookieuser (= ext_1234) (in quickreply)
    # - cookieuser (currently logged in user)
    #   - cookieuser (= username) (yes, "cookieuser" is the field's name)
    # - user (non-logged-in user, w/ name/password provided)
    #   - userpost (the username provided in the form)
    #   - password
    #   - do_login

    # CHECKLIST:
    # 1. Check for incoherent combinations of fields. (Most can only happen with
    # JS disabled. I'm told there were once cases where it could post as the
    # wrong user, possibly without auth; who knows. But regardless, conflicting
    # info means the user's intention was not clear and they must clarify.)
    # 2. Validate the specified user type's credentials.
    # 3. If validated, return the relevant user object.
    # 4. OpenID is weird.
    # NOTA BENE: This long "if" statement is tedious and stupid, and I'm well
    # aware there's several cleverer and more exciting ways to write it. But
    # don't. KEEP IT STUPID. KEEP IT SAFE. </gandalf voice> -NF, 2020
    if ( $form->{usertype} eq 'anonymous' ) {
        if ( $form->{oidurl} || $form->{userpost} ) {
            return $incoherent->();
        }
        return $got_user->(undef);    # Well! that was easy.
    }
    elsif ( $form->{usertype} eq 'cookieuser' ) {
        if ( $form->{oidurl} ) {
            return $incoherent->();
        }

        # If they selected "current user" and then typed in their own username,
        # well, that's "wrong" but their intention was perfectly clear. But if
        # they typed in a DIFFERENT username, get outta here.
        if ( $form->{userpost} && ( $form->{userpost} ne $form->{cookieuser} ) ) {
            return $incoherent->();
        }

        # OK! Check if that's the logged-in user.
        if ( $remote && ( $remote->user eq $form->{cookieuser} ) ) {
            return $got_user->($remote);    # Cool.
        }
        else {
            return $mlerr->("/talkpost_do.tt.error.lostcookie");
        }
    }
    elsif ( $form->{usertype} eq 'user' ) {
        if ( $form->{oidurl} ) {
            return $incoherent->();
        }

        # No username?
        if ( !$form->{userpost} ) {
            my $iscomm = $journalu->is_community ? '.comm' : '';
            my $noanon = $journalu->prop('opt_whocanreply') eq 'all' ? '' : '.noanon';
            return $mlerr->(
                "/talkpost_do.tt.error.nousername$noanon$iscomm",
                { sitename => $LJ::SITENAMESHORT }
            );
        }

        my $exptype;    # set to long if ! after username
        my $ipfixed;    # set to remote ip if < after username

        # Parse inline login options.
        # MUTATE FORM: remove trailing garbage from username.
        if ( $form->{userpost} =~ s/([!<]{1,2})$// ) {
            $exptype = 'long' if index( $1, "!" ) >= 0;
            $ipfixed = LJ::get_remote_ip() if index( $1, "<" ) >= 0;
        }

        my $up = LJ::load_user( $form->{userpost} );

        # Now for all the things that can go wrong:
        if ( !$up ) {
            return $mlerr->(
                "/talkpost_do.tt.error.badusername2",
                {
                    sitename => $LJ::SITENAMESHORT,
                    aopts    => "href='$LJ::SITEROOT/lostinfo'"
                }
            );
        }

        if ( $up->is_identity ) {
            return $err->( "To comment as an OpenID user, you must choose the "
                    . "OpenID option and authenticate with your identity provider; "
                    . "it's not possible to log in using an OpenID account's "
                    . "internal 'ext_12345' username." );
        }

        if ( $up->is_community || $up->is_syndicated ) {
            return $mlerr->("/talkpost_do.tt.error.postshared");
        }

        # authenticate on username/password
        my $ok = LJ::auth_okay( $up, $form->{password} );

        unless ($ok) {

            # Don't pre-populate the fix-up form with a password we already know is wrong.
            $form->{password} = '';
            return $mlerr->(
                "/talkpost_do.tt.error.badpassword2",
                { aopts => "href='$LJ::SITEROOT/lostinfo'" }
            );
        }

        # GREAT, they're in!

        # if the user chooses to log in, do so
        if ( $form->{do_login} ) {
            $didlogin = $up->make_login_session( $exptype, $ipfixed );

            # MUTATE FORM: change the usertype, so if they need to fix an
            # unrelated error and are already logged in, the form uses the
            # "currently logged-in user" option.
            $form->{usertype}   = 'cookieuser';
            $form->{cookieuser} = $up->user;
        }

        return $got_user->($up);
    }
    elsif ( $form->{usertype} eq 'openid' || $form->{usertype} eq 'openid_cookie' ) {
        if ( $form->{userpost} ) {
            return $incoherent->();
        }

        # Okay: This one's weird, but mostly just because the code order is
        # backwards from how things happen irl, WHICH IS:
        # - Person supplies OpenID URL.
        # - We store the form to the database, bail out, and tell the caller to
        # redirect to an authentication server URL. (The URL also tells the auth
        # server where to redirect to once IT'S done.)
        # - Auth server sends them back to /talkpost_do, but as a GET request
        # instead of a POST.
        # - Controller restores their frozen POST data from last time and calls
        # this function again, passing the newly authenticated user as $remote.
        # (This is why we're not using LJ::get_remote in this function, btw.)
        # - Since $remote is set, we let them through.

        # If $remote looks good, they're in.
        if ( $remote && defined $remote->openid_identity ) {

            # Go ahead and log in, if requested.
            if ( $form->{oiddo_login} ) {

                # Those extra form vars got stored last time, see below.
                $didlogin = $remote->make_login_session( $form->{exptype}, $form->{ipfixed} );

                # MUTATE FORM: change the usertype if they logged in, so
                # things look more consistent if they hit an unrelated error
                $form->{usertype} = 'openid_cookie';
            }

            return $got_user->($remote);    # welcome back
        }
        else {

            # If this is your first time at Tautology Club... you've never been
            # here before.

            return $err->("No OpenID identity URL entered") unless $form->{oidurl};

            my $csr     = LJ::OpenID::consumer();
            my $exptype = 'short';
            my $ipfixed = 0;

            # parse inline login opts
            # MUTATE FORM: remove trailing garbage from oidurl
            if ( $form->{oidurl} =~ s/([!<]{1,2})$// ) {
                $exptype = 'long' if index( $1, "!" ) >= 0;
                $ipfixed = LJ::get_remote_ip() if index( $1, "<" ) >= 0;
            }

            my $tried_local_ref = LJ::OpenID::blocked_hosts($csr);

            my $claimed_id = $csr->claimed_identity( $form->{oidurl} );

            unless ($claimed_id) {
                return $err->(
                    "You can't use a $LJ::SITENAMESHORT OpenID account on $LJ::SITENAME &mdash; "
                        . "just <a href='/login'>go login</a> with your actual $LJ::SITENAMESHORT account."
                ) if $$tried_local_ref;
                return $err->( "No claimed id: " . $csr->err );
            }

            # Store their cleaned up identity url (vs. what they actually typed.)
            # MUTATE FORM: canonicalize oidurl
            $form->{oidurl} = $claimed_id->claimed_url();

            # Store the entry
            my $pendcid = LJ::alloc_user_counter( $journalu, "C" );

            return $err->("Unable to allocate pending id") unless $pendcid;

            # persist login options in the form data, since we removed them from
            # the oidurl
            # MUTATE FORM: add junk that never appears in a real comment form.
            $form->{exptype} = $exptype;
            $form->{ipfixed} = $ipfixed;

            my $penddata = Storable::freeze($form);

            return $err->("Unable to get database handle to store pending comment")
                unless $journalu->writer;

            my $journalid = $journalu->id;

            $journalu->do(
"INSERT INTO pendcomments (jid, pendcid, data, datesubmit) VALUES (?, ?, ?, UNIX_TIMESTAMP())",
                undef, $journalid, $pendcid, $penddata
            );

            return $err->( $journalu->errstr ) if $journalu->err;

            my $check_url = $claimed_id->check_url(
                return_to      => "$LJ::SITEROOT/talkpost_do?jid=$journalid&pendcid=$pendcid",
                trust_root     => "$LJ::SITEROOT",
                delayed_return => 1,
            );

            # Caller must redirect to this URL.
            return ( 1, { check_url => $check_url } );
        }
    }
    else {
        return $err->("Reply form was submitted without any user information.");
    }
}

# Returns hashref for template.
sub preview_comment_args {
    my ($comment) = @_;

    my $cleansubject = $comment->{subject};
    LJ::CleanHTML::clean_subject( \$cleansubject );

    my $cleanbody = $comment->{body};
    LJ::CleanHTML::clean_comment(
        \$cleanbody,
        {
            anon_comment => LJ::Talk::treat_as_anon( $comment->{u}, $comment->{entry}->journal ),
            preformatted => $comment->{preformat},
            admin_post   => $comment->{admin_post},
            editor       => $comment->{editor},
        }
    );

    my $poster = "(Anonymous)";
    my $icon   = '';
    if ( $comment->{u} ) {
        $poster = $comment->{u}->ljuser_display;

        my $userpic = LJ::Userpic->new_from_keyword( $comment->{u}, $comment->{picture_keyword} );
        if ($userpic) {
            $icon =
                  '<a href="'
                . $comment->{u}->allpics_base . '">'
                . $userpic->imgtag( keyword => $comment->{prop_picture_keyword} ) . '</a>';
        }
    }

    my $preview = {
        poster      => $poster,
        subjecticon => LJ::Talk::print_subjecticon_by_id( $comment->{subjecticon} ),
        body        => $cleanbody,
        subject     => $cleansubject,
        icon        => $icon,
        admin_post  => $comment->{admin_post},
    };

    return $preview;
}

# Returns hashref for template.
sub preview_parent_args {
    my ($comment) = @_;

    my $userpic_tag = sub {
        my $item    = shift;
        my $icon    = '';
        my $userpic = $item->userpic;
        if ($userpic) {
            $icon = $userpic->imgtag( keyword => $item->userpic_kw );
        }
        return $icon;
    };

    my $entry = $comment->{entry};

    if ( $comment->{parent}->{talkid} ) {

        # Replying to comment
        my $parentitem =
            LJ::Comment->new( $entry->journal, jtalkid => $comment->{parent}->{talkid} );

        my $poster      = 'Anonymous';
        my $poster_name = '';
        my $in_journal  = $entry->journal->ljuser_display;

        if ( $parentitem->poster ) {
            $poster      = $parentitem->poster->ljuser_display;
            $poster_name = $parentitem->poster->name_html;

            if ( $parentitem->poster->equals( $entry->journal ) ) {
                $in_journal = '';
            }
        }

        return {
            type        => 'comment',
            body        => $parentitem->body_html,
            subject     => $parentitem->subject_html,
            poster      => $poster,
            poster_name => $poster_name,
            in_journal  => $in_journal,
            admin_post  => $parentitem->prop('admin_post'),
            icon        => $userpic_tag->($parentitem),
            time        => $parentitem->{datepost},
            url         => $parentitem->url,
            entry_url   => $entry->url,
        };
    }
    else {
        # Replying to entry

        my $in_journal =
            $entry->poster->equals( $entry->journal ) ? '' : $entry->journal->ljuser_display;

        return {
            type        => 'entry',
            body        => $entry->event_html,
            subject     => $entry->subject_html,
            poster      => $entry->poster->ljuser_display,
            poster_name => $entry->poster->name_html,
            in_journal  => $in_journal,
            icon        => $userpic_tag->($entry),
            time        => $entry->eventtime_mysql,
            url         => $entry->url,
            entry_url   => $entry->url,
        };
    }
}

sub talkmulti_handler {
    my ( $ok, $rv ) = controller( form_auth => 0, anonymous => 0 );
    return $rv unless $ok;

    my $r      = $rv->{r};
    my $POST   = $r->post_args;
    my $remote = $rv->{remote};
    my $GET    = $r->get_args;
    my $title;
    my $vars;

    my $mode = $POST->{'mode'};
    if ( $mode eq 'screen' ) {
        $title = '.title.screen';
    }
    elsif ( $mode eq 'unscreen' ) {
        $title = '.title.unscreen';
    }
    elsif ( $mode eq 'delete' || $mode eq 'deletespam' ) {
        $title = '.title.delete';
    }
    else {
        return error_ml('/talkmulti.tt.error.invalid_mode');
    }

    my $sth;

    return error_ml("bml.requirepost") unless $r->did_post;

    my @talkids;
    foreach ( keys %{$POST} ) {
        push @talkids, $1 if /^selected_(\d+)$/;
    }
    return error_ml('/talkmulti.tt.error.none_selected') unless @talkids;

    my $u = LJ::load_user( $POST->{'journal'} );
    return error_ml('talk.error.bogusargs') unless $u && $u->{'clusterid'};
    return error_ml($LJ::MSG_READONLY_USER) if $u->is_readonly;

    my $dbcr = LJ::get_cluster_def_reader($u);

    my $jid         = $u->userid;
    my $ditemid     = $POST->{'ditemid'} + 0;
    my $e           = LJ::Entry->new( $u, ditemid => $ditemid );
    my $commentlink = $e->url;
    my $itemid      = $ditemid >> 8;
    my $log = $dbcr->selectrow_hashref( "SELECT * FROM log2 WHERE journalid=? AND jitemid=?",
        undef, $jid, $itemid );
    return error_ml('/talkmulti.tt.error.inconsistent_data')
        unless $log && $log->{'anum'} == ( $ditemid & 255 );
    my $up = LJ::load_userid( $log->{'posterid'} );

    # check permissions
    return error_ml('/talkmulti.tt.error.privs.screen')
        if $mode eq "screen" && !LJ::Talk::can_screen( $remote, $u, $up );
    return error_ml('/talkmulti.tt.error.privs.unscreen')
        if $mode eq "unscreen" && !LJ::Talk::can_unscreen( $remote, $u, $up );
    return error_ml('/talkmulti.tt.error.privs.delete')
        if ( $mode eq "delete" || $mode eq 'deletespam' )
        && !LJ::Talk::can_delete( $remote, $u, $up );

    # filter our talkids down to those that are actually attached to the log2
    # specified.  also, learn the state and poster of all the items.
    my $in = join( ',', @talkids );
    $sth =
        $dbcr->prepare( "SELECT jtalkid, state, posterid FROM talk2 "
            . "WHERE journalid=? AND jtalkid IN ($in) "
            . "AND nodetype='L' AND nodeid=?" );
    $sth->execute( $jid, $itemid );
    my %talkinfo;
    while ( my ( $id, $state, $posterid ) = $sth->fetchrow_array ) {
        $talkinfo{$id} = [ $state, $posterid ];
    }
    @talkids = keys %talkinfo;

    # do the work:
    if ( $mode eq "delete" || $mode eq 'deletespam' ) {

        # first, unscreen everything for replycount and hasscreened to adjust
        my @unscreen = grep { $talkinfo{$_}->[0] eq "S" } @talkids;
        LJ::Talk::unscreen_comment( $u, $itemid, @unscreen );

        # FIXME: race condition here... somebody could get lucky and view items while unscreened.
        # then delete, updating the log2 replycount as necessary

        # Mark as spam
        if ( $mode eq 'deletespam' && !LJ::sysban_check( 'spamreport', $u->user ) ) {

            # don't let $remote mark their own comments as spam
            foreach ( grep { $talkinfo{$_}->[1] != $remote->id } @talkids ) {
                my $s = LJ::Talk::mark_comment_as_spam( $u, $_ );
            }
        }

        my $num = LJ::delete_comments( $u, "L", $itemid, @talkids );
        LJ::replycount_do( $u, $itemid, "decr", $num );
        LJ::Talk::update_commentalter( $u, $itemid );
        $vars = {
            title   => '.deleted.title',
            body    => '.deleted.body2',
            'aopts' => "href='$commentlink'"
        };

    }
    elsif ( $mode eq "unscreen" ) {
        LJ::Talk::unscreen_comment( $u, $itemid, @talkids );
        $vars = {
            title   => '.unscreened.title',
            body    => '.unscreened.body2',
            'aopts' => "href='$commentlink'"
        };

    }
    elsif ( $mode eq "screen" ) {
        LJ::Talk::screen_comment( $u, $itemid, @talkids );
        $vars = {
            title   => '.screened.title',
            body    => '.screened.body2',
            'aopts' => "href='$commentlink'"
        };
    }
    return DW::Template->render_template( 'talkmulti.tt', $vars );
}

sub talkscreen_handler {
    my ( $ok, $rv ) = controller( form_auth => 1, anonymous => 0 );
    return $rv unless $ok;

    my $r      = $rv->{r};
    my $POST   = $r->post_args;
    my $remote = $rv->{remote};
    my $GET    = $r->get_args;

    my $jsmode = !!$GET->{jsmode};

    my $error = sub {
        if ($jsmode) {

            # FIXME: remove once we've switched over completely to jquery
            if ( !!$GET->{json} ) {
                $r->print( to_json( { error => LJ::Lang::ml( $_[0] ) } ) );

                return $r->OK;
            }
            else {
                return "alert('" . LJ::ejs( LJ::Lang::ml( $_[0] ) ) . "'); 0;";
            }
        }
        return error_ml( $_[0] );
    };

    my $mode    = $POST->{'mode'}    || $GET->{'mode'};
    my $talkid  = $POST->{'talkid'}  || $GET->{'talkid'};
    my $journal = $POST->{'journal'} || $GET->{'journal'};
    my $qtalkid = $talkid + 0;
    my $dtalkid = $qtalkid;    # display talkid, for use in URL later

    my $jsres = sub {
        my ( $mode, $message ) = @_;

        # flip case of 'un'
        my $newmode = "un$mode";
        $newmode =~ s/^unun//;
        my $alttext = $newmode;
        $alttext =~ s/(\w+)/\u\L$1/g;

        my $stockimg = {
            'screen'   => "silk/comments/screen.png",
            'unscreen' => "silk/comments/unscreen.png",
            'freeze'   => "silk/comments/freeze.png",
            'unfreeze' => "silk/comments/unfreeze.png",
        };

        my $imgprefix = $LJ::IMGPREFIX;
        $imgprefix =~ s/^https?://;

        my $ret = {
            id       => $dtalkid,
            mode     => $mode,
            newalt   => $alttext,
            oldimage => "$imgprefix/$stockimg->{$mode}",
            newimage => "$imgprefix/$stockimg->{$newmode}",
            newurl   => "$LJ::SITEROOT/talkscreen?mode=$newmode&journal=$journal&talkid=$dtalkid",
            msg      => LJ::Lang::ml($message),
        };

        sleep 1 if $LJ::IS_DEV_SERVER;

        $r->print( to_json($ret) );
        return $r->OK;
    };

    # we need to find out: $u, $up (poster of the entry this is a comment to),
    # userpost (username of this comment's author). Then we can check permissions.

    my $u = LJ::load_user($journal);
    return $error->('talk.error.bogusargs') unless $u;

    # if we're on a user vhost, our remote was authed using that vhost,
    # so let's let them only modify the journal that their session
    # was authed against.  if they're on www., then their javascript is
    # off/old, and they get a confirmation page, and we're using their
    # mastersesion cookie anyway.
    my $domain_owner = LJ::Session->url_owner;
    if ($domain_owner) {
        return $error->('talk.error.bad_owner') unless $domain_owner eq $u->{user};
    }

    my $dbcr = LJ::get_cluster_def_reader($u);
    return $error->('error.nodb') unless $dbcr;

    my $post;
    $qtalkid = int( $qtalkid / 256 );      # get rid of anum
    $post    = $dbcr->selectrow_hashref(
              "SELECT jtalkid AS 'talkid', nodetype, state, nodeid AS 'itemid', "
            . "parenttalkid, journalid, posterid FROM talk2 "
            . "WHERE journalid=$u->{'userid'} AND jtalkid=$qtalkid" );

    return $error->('talk.error.nocomment') unless $post;
    return $error->('talk.error.comm_deleted') if $post->{'state'} eq "D";

    my $state = $post->{'state'};

    $u ||= LJ::load_userid( $post->{'journalid'} );
    return $error->($LJ::MSG_READONLY_USER) if $u->is_readonly;

    if ( $post->{'posterid'} ) {
        $post->{'userpost'} = LJ::get_username( $post->{'posterid'} );
    }

    my $qitemid = $post->{'itemid'} + 0;

    my $e = LJ::Entry->new( $u, jitemid => $qitemid );

    my $up = $e->poster;

    my $itemlink = $e->url;

    my $commentlink = LJ::Talk::talkargs( $itemlink, "view=" . $talkid, "", "" )
        . LJ::Talk::comment_anchor($talkid);

    my $vars = {
        itemlink    => $itemlink,
        commentlink => $commentlink,
        mode        => $mode,
        talkid      => $talkid,
        u           => $u
    };

    if ( $mode eq 'screen' ) {
        my $can_screen = LJ::Talk::can_screen( $remote, $u, $up, $post->{'userpost'} );
        return $error->('/talkscreen.tt.error.privs.screen') unless $can_screen;
        if ( $POST->{'confirm'} eq 'Y' ) {
            if ( $state ne 'S' ) {
                LJ::Talk::screen_comment( $u, $qitemid, $qtalkid );
            }

            # FIXME: no error checking?
            return $jsres->( $mode, '/talkscreen.tt.screened.body' ) if $jsmode;
            $vars->{done}   = 1;
            $vars->{action} = 'screened';
        }

    }
    elsif ( $mode eq 'unscreen' ) {
        my $can_unscreen = LJ::Talk::can_unscreen( $remote, $u, $up, $post->{'userpost'} );
        return $error->('/talkscreen.tt.error.privs.unscreen') unless $can_unscreen;
        if ( $POST->{'confirm'} eq 'Y' ) {
            if ( $state ne 'A' ) {
                LJ::Talk::unscreen_comment( $u, $qitemid, $qtalkid );
            }

            # FIXME: no error checking?
            return $jsres->( $mode, '/talkscreen.tt.unscreened.body' ) if $jsmode;
            $vars->{done}   = 1;
            $vars->{action} = 'unscreened';
        }

    }
    elsif ( $mode eq 'freeze' ) {
        my $can_freeze = LJ::Talk::can_freeze( $remote, $u, $up, $post->{userpost} );
        return $error->('/talkscreen.tt.error.privs.freeze') unless $can_freeze;

        if ( $POST->{confirm} eq 'Y' ) {
            if ( $state ne 'F' ) {
                LJ::Talk::freeze_thread( $u, $qitemid, $qtalkid );
            }
            return $jsres->( $mode, '/talkscreen.tt.frozen.body' ) if $jsmode;

            $vars->{done}   = 1;
            $vars->{action} = 'frozen';
        }
    }
    elsif ( $mode eq 'unfreeze' ) {
        my $can_unfreeze = LJ::Talk::can_unfreeze( $remote, $u, $up, $post->{userpost} );
        return $error->("You are not allowed to unfreeze this thread") unless $can_unfreeze;
        if ( $POST->{confirm} eq 'Y' ) {
            if ( $state eq 'F' ) {
                LJ::Talk::unfreeze_thread( $u, $qitemid, $qtalkid );
            }
            return $jsres->( $mode, '/talkscreen.tt.unfrozen.body' ) if $jsmode;

            $vars->{done}   = 1;
            $vars->{action} = 'unfrozen';
        }
    }
    else {
        return error_ml('error.unknownmode');
    }

    return DW::Template->render_template( 'talkscreen.tt', $vars );
}

sub delcomment_handler {
    my ( $ok, $rv ) = controller( form_auth => 1, anonymous => 0 );
    return $rv unless $ok;

    my $r      = $rv->{r};
    my $POST   = $r->post_args;
    my $remote = $rv->{remote};
    my $GET    = $r->get_args;
    my $vars;

    my $jsmode = !!$GET->{jsmode};

    my $error = sub {
        if ($jsmode) {

            # FIXME: remove once we've switched over completely to jquery
            if ( !!$GET->{json} ) {
                sleep 1 if $LJ::IS_DEV_SERVER;
                $r->print( to_json( { error => LJ::Lang::ml( $_[0] ) } ) );
                return $r->OK;
            }
            else {
                return "alert('" . LJ::ejs( LJ::Lang::ml( $_[0] ) ) . "'); 0;";
            }
        }
        return error_ml( $_[0] );
    };
    my $bad_input = sub {
        return $error->( "Bad input: " . LJ::Lang::ml( $_[0] ) ) if $jsmode;
        return error_ml( $_[0] );
    };

    return $error->( LJ::error_noremote() ) unless $remote;

    return $error->('talk.error.bogusargs') unless $GET->{'journal'} ne "" && $GET->{'id'};

    # $u is user object of journal that owns the talkpost
    my $u = LJ::load_user( $GET->{'journal'} );
    return $bad_input->('error.nojournal')
        unless $u;

    # find out whether $u is a community. We'll use this to refer to ML strings
    #  later
    my $iscomm = $u->is_community ? '.comm' : '';

    # if we're on a user vhost, our remote was authed using that vhost,
    # so let's let them only modify the journal that their session
    # was authed against.  if they're on www., then their javascript is
    # off/old, and they get a confirmation page, and we're using their
    # mastersesion cookie anyway.
    my $domain_owner = LJ::Session->url_owner;
    if ($domain_owner) {
        return $bad_input->('talk.error.bad_owner')
            unless $domain_owner eq $u->{user};
    }

    # can't delete if you're suspended
    return $bad_input->('/delcomment.tt.error.suspended')
        if $remote->is_suspended;

    return $error->($LJ::MSG_READONLY_USER) if $u->is_readonly;

    my $dbcr = LJ::get_cluster_def_reader($u);
    return $error->('error.nodb')
        unless $dbcr;

    # $tp is a hashref of info about this individual talkpost row
    my $tpid = $GET->{'id'} >> 8;
    my $tp   = $dbcr->selectrow_hashref(
        "SELECT jtalkid AS 'talkid', nodetype, state, "
            . "nodeid AS 'itemid', parenttalkid, journalid, posterid "
            . "FROM talk2 "
            . "WHERE journalid=? AND jtalkid=?",
        undef, $u->userid, $tpid
    );

    return $bad_input->('/delcomment.tt.error.nocomment')
        unless $tp;

    return $bad_input->('/delcomment.tt.error.invalidtype2')
        unless $tp->{'nodetype'} eq 'L';

    return $bad_input->('/delcomment.tt.error.alreadydeleted')
        if $tp->{'state'} eq "D";

    # get username of poster
    $tp->{'userpost'} = LJ::get_username( $tp->{'posterid'} );

    # userid of user who posted journal entry
    my $jposterid =
        $dbcr->selectrow_array( "SELECT posterid FROM log2 WHERE " . "journalid=? AND jitemid=?",
        undef, $u->userid, $tp->{itemid} );
    my $jposter = LJ::load_userid($jposterid);

    # can $remote delete this comment?
    unless ( LJ::Talk::can_delete( $remote, $u, $jposter, $tp->{'userpost'} ) ) {
        my $err =
            $u->is_community
            ? '/delcomment.tt.error.cantdelete.comm'
            : '/delcomment.tt.error.cantdelete';
        return $error->($err);
    }

    my $can_manage = $remote->can_manage($u);

    # can ban if can manage and the comment is by someone else and not anon
    my $can_ban =
           $can_manage
        && $tp->{posterid}
        && $remote
        && $remote->userid != $tp->{posterid};
    my $can_delthread = $can_manage || $jposterid == $remote->userid;

    # can mark as spam if they're not the comment poster
    # or if the account is not sysbanned
    my $can_spam =
        $remote && $remote->id != $tp->{'posterid'} && !LJ::sysban_check( 'spamreport', $u->user );

    $vars = {
        can_manage    => $can_manage,
        can_ban       => $can_ban,
        can_spam      => $can_spam,
        can_delthread => $can_delthread,
        iscomm        => $iscomm,
        u             => $u,
        id            => $GET->{'id'},
        tp_user       => LJ::ljuser( $tp->{'userpost'} )
    };

    ### perform actions
    if ( $r->did_post && $POST->{'confirm'} ) {
        return $error->( LJ::Lang::ml('/talkpost_do.tt.error.invalidform') )
            unless LJ::check_form_auth( $POST->{lj_form_auth} );

        # mark this as spam?
        LJ::Talk::mark_comment_as_spam( $u, $tp->{talkid} )
            if $POST->{spam} && $can_spam;

        # delete entire thread? or just the one comment?
        if ( $POST->{delthread} && $can_delthread ) {

            # delete entire thread ...
            LJ::Talk::delete_thread( $u, $tp->{'itemid'}, $tpid );
        }
        else {
            # delete single comment...
            LJ::Talk::delete_comment( $u, $tp->{'itemid'}, $tpid, $tp->{'state'} );
        }

        # ban the user, if selected
        my $msg;
        if ( $POST->{'ban'} && $can_ban ) {
            LJ::set_rel( $u->{'userid'}, $tp->{'posterid'}, 'B' );
            $u->log_event( 'ban_set', { actiontarget => $tp->{'posterid'}, remote => $remote } );
            $msg = LJ::Lang::ml(
                "/delcomment.tt.success.andban$iscomm",
                { 'user' => LJ::ljuser( $tp->{'userpost'} ) }
            );
        }
        $msg ||= LJ::Lang::ml('/delcomment.tt.success.noban');
        $msg .= " <p>" . LJ::Lang::ml('/delcomment.tt.success.spam') . "</p>"
            if $POST->{spam} && $can_spam;

        if ($jsmode) {
            if ( !!$GET->{json} ) {
                sleep 1 if $LJ::IS_DEV_SERVER;
                $r->print( to_json( { msg => LJ::strip_html($msg) } ) );
                return $r->OK;
            }
            else {
                return "1;";
            }
        }
        else {
            $vars->{done} = 1;
            $vars->{msg}  = $msg;
        }
    }

    return DW::Template->render_template( 'delcomment.tt', $vars );
}

1;
