mourningdove/cgi-bin/DW/Controller/Talk.pm

1452 lines
53 KiB
Perl
Raw Permalink Normal View History

2026-05-24 01:03:05 +00:00
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;