#!/usr/bin/perl
#
use strict;
no warnings 'uninitialized';
use LJ::Constants;
use Class::Autouse qw(
LJ::Console
LJ::Event::JournalNewEntry
LJ::Event::UserNewEntry
LJ::Event::Befriended
LJ::Entry
LJ::Poll
LJ::EventLogRecord::NewEntry
LJ::EventLogRecord::EditEntry
LJ::Config
LJ::Comment
LJ::RateLimit
);
use LJ::TimeUtil;
LJ::Config->load;
use lib "$ENV{LJHOME}/cgi-bin";
require "taglib.pl";
# have to do this else mailgate will croak with email posting, but only want
# to do it if the site has enabled the hack
require "talklib.pl" if $LJ::NEW_ENTRY_CLEANUP_HACK;
# when posting or editing ping hubbub
require "ljfeed.pl" unless $LJ::DISABLED{'hubbub'};
#### New interface (meta handler) ... other handlers should call into this.
package LJ::Protocol;
# global declaration of this text since we use it in two places
our $CannotBeShown = '(cannot be shown)';
# error classes
use constant E_TEMP => 0;
use constant E_PERM => 1;
# maximum items for get_friends_page function
use constant FRIEND_ITEMS_LIMIT => 50;
my %e = (
# User Errors
"100" => [ E_PERM, "Invalid username" ],
"101" => [ E_PERM, "Invalid password" ],
"102" => [ E_PERM, "Can't use custom/private security on shared/community journals." ],
"103" => [ E_PERM, "Poll error" ],
"104" => [ E_TEMP, "Error adding one or more friends" ],
"105" => [ E_PERM, "Challenge expired" ],
"150" => [ E_PERM, "Can't post as non-user" ],
"151" => [ E_TEMP, "Banned from journal" ],
"152" => [ E_PERM, "Can't make back-dated entries in non-personal journal." ],
"153" => [ E_PERM, "Incorrect time value" ],
"154" => [ E_PERM, "Can't add a redirected account as a friend" ],
"155" => [ E_TEMP, "Non-authenticated email address" ],
"156" => [ E_TEMP, sub { # to reload w/o restart
LJ::tosagree_str('protocol' => 'text') ||
LJ::tosagree_str('protocol' => 'title')
} ],
"157" => [ E_TEMP, "Tags error" ],
# Client Errors
"200" => [ E_PERM, "Missing required argument(s)" ],
"201" => [ E_PERM, "Unknown method" ],
"202" => [ E_PERM, "Too many arguments" ],
"203" => [ E_PERM, "Invalid argument(s)" ],
"204" => [ E_PERM, "Invalid metadata datatype" ],
"205" => [ E_PERM, "Unknown metadata" ],
"206" => [ E_PERM, "Invalid destination journal username." ],
"207" => [ E_PERM, "Protocol version mismatch" ],
"208" => [ E_PERM, "Invalid text encoding" ],
"209" => [ E_PERM, "Parameter out of range" ],
"210" => [ E_PERM, "Client tried to edit with corrupt data. Preventing." ],
"211" => [ E_PERM, "Invalid or malformed tag list" ],
"212" => [ E_PERM, "Message body is too long" ],
"213" => [ E_PERM, "Message body is empty" ],
"214" => [ E_PERM, "Message looks like spam" ],
# Access Errors
"300" => [ E_TEMP, "Don't have access to requested journal" ],
"301" => [ E_TEMP, "Access of restricted feature" ],
"302" => [ E_TEMP, "Can't edit post from requested journal" ],
"303" => [ E_TEMP, "Can't edit post in community journal" ],
"304" => [ E_TEMP, "Can't delete post in this community journal" ],
"305" => [ E_TEMP, "Action forbidden; account is suspended." ],
"306" => [ E_TEMP, "This journal is temporarily in read-only mode. Try again in a couple minutes." ],
"307" => [ E_PERM, "Selected journal no longer exists." ],
"308" => [ E_TEMP, "Account is locked and cannot be used." ],
"309" => [ E_PERM, "Account is marked as a memorial." ],
"310" => [ E_TEMP, "Account needs to be age verified before use." ],
"311" => [ E_TEMP, "Access temporarily disabled." ],
"312" => [ E_TEMP, "Not allowed to add tags to entries in this journal" ],
"313" => [ E_TEMP, "Must use existing tags for entries in this journal (can't create new ones)" ],
"314" => [ E_PERM, "Only paid users allowed to use this request" ],
"315" => [ E_PERM, "User messaging is currently disabled" ],
"316" => [ E_TEMP, "Poster is read-only and cannot post entries." ],
"317" => [ E_TEMP, "Journal is read-only and entries cannot be posted to it." ],
"318" => [ E_TEMP, "Poster is read-only and cannot edit entries." ],
"319" => [ E_TEMP, "Journal is read-only and its entries cannot be edited." ],
"320" => [ E_TEMP, "Sorry, there was a problem with content of the entry" ],
"321" => [ E_TEMP, "Sorry, deleting is temporary disabled. Entry is 'private' now" ],
# Limit errors
"402" => [ E_TEMP, "Your IP address is temporarily banned for exceeding the login failure rate." ],
"404" => [ E_TEMP, "Cannot post" ],
"405" => [ E_TEMP, "Post frequency limit." ],
"406" => [ E_TEMP, "Client is making repeated requests. Perhaps it's broken?" ],
"407" => [ E_TEMP, "Moderation queue full" ],
"408" => [ E_TEMP, "Maximum queued posts for this community+poster combination reached." ],
"409" => [ E_PERM, "Post too large." ],
"410" => [ E_PERM, "Your trial account has expired. Posting now disabled." ],
"411" => [ E_TEMP, "Action frequency limit." ],
# Server Errors
"500" => [ E_TEMP, "Internal server error" ],
"501" => [ E_TEMP, "Database error" ],
"502" => [ E_TEMP, "Database temporarily unavailable" ],
"503" => [ E_TEMP, "Error obtaining necessary database lock" ],
"504" => [ E_PERM, "Protocol mode no longer supported." ],
"505" => [ E_TEMP, "Account data format on server is old and needs to be upgraded." ], # cluster0
"506" => [ E_TEMP, "Journal sync temporarily unavailable." ],
);
my %HANDLERS = (
login => \&login,
getfriendgroups => \&getfriendgroups,
getfriends => \&getfriends,
friendof => \&friendof,
checkfriends => \&checkfriends,
getdaycounts => \&getdaycounts,
postevent => \&postevent,
editevent => \&editevent,
syncitems => \&syncitems,
getevents => \&getevents,
editfriends => \&editfriends,
editfriendgroups => \&editfriendgroups,
consolecommand => \&consolecommand,
getchallenge => \&getchallenge,
sessiongenerate => \&sessiongenerate,
sessionexpire => \&sessionexpire,
getusertags => \&getusertags,
getfriendspage => \&getfriendspage,
getinbox => \&getinbox,
sendmessage => \&sendmessage,
setmessageread => \&setmessageread,
addcomment => \&addcomment,
checksession => \&checksession,
getrecentcomments => \&getrecentcomments
);
sub translate
{
my ($u, $msg, $vars) = @_;
LJ::load_user_props($u, "browselang") unless $u->{'browselang'};
return LJ::Lang::get_text($u->{'browselang'}, "protocol.$msg", undef, $vars);
}
sub error_class
{
my $code = shift;
$code = $1 if $code =~ /^(\d\d\d):(.+)/;
return $e{$code} && ref $e{$code} ? $e{$code}->[0] : undef;
}
sub error_is_transient
{
my $class = error_class($_[0]);
return defined $class ? ! $class+0 : undef;
}
sub error_is_permanent
{
return error_class($_[0]);
}
sub error_message
{
my $code = shift;
my $des;
($code, $des) = ($1, $2) if $code =~ /^(\d\d\d):(.+)/;
my $prefix = "";
my $error =
$e{$code} && ref $e{$code}
? ( ref $e{$code}->[1] eq 'CODE' ? $e{$code}->[1]->() : $e{$code}->[1] )
: "BUG: Unknown error code!";
$prefix = "Client error: " if $code >= 200;
$prefix = "Server error: " if $code >= 500;
my $totalerror = "$prefix$error";
$totalerror .= ": $des" if $des;
return $totalerror;
}
sub do_request
{
# get the request and response hash refs
my ($method, $req, $err, $flags) = @_;
# if version isn't specified explicitly, it's version 0
if (ref $req eq "HASH") {
$req->{'ver'} ||= $req->{'version'};
$req->{'ver'} = 0 unless defined $req->{'ver'};
}
$flags ||= {};
my @args = ($req, $err, $flags);
LJ::Request->notes("codepath" => "protocol.$method")
if LJ::Request->is_inited && ! LJ::Request->notes("codepath");
my $method_ref = $HANDLERS{$method};
if ($method_ref)
{
my $result = $method_ref->(@args);
if ($result && exists $result->{xc3})
{
my $xc3 = delete $result->{xc3};
if ($req->{props}->{interface} eq 'xml-rpc')
{
my $ua = eval { LJ::Request->header_in("User-Agent") };
Encode::from_to($ua, 'utf8', 'utf8') if $ua;
my ($ip_class, $country) = LJ::GeoLocation->ip_class();
my $args = {
function => $method || ''
};
if ($xc3->{u})
{
my $u = $xc3->{u};
$args->{userid} = $u->userid;
$args->{usercaps} = $u->caps;
}
$args->{useragent} = $ua if $ua;
$args->{country} = $country if $country;
$args->{post} = $xc3->{post} if $xc3->{post};
$args->{comment} = $xc3->{comment} if $xc3->{comment};
LJ::run_hooks("remote_procedure_call", $args);
}
}
return $result;
}
LJ::Request->notes("codepath" => "") if LJ::Request->is_inited;
return fail($err, 201);
}
sub checksession {
my ($req, $err, $flags) = @_;
return undef
unless authenticate($req, $err, $flags);
my $u = $flags->{'u'};
my $session = $u->session;
return {
username => $u->username,
session => $u->id.":".$session->id.":".$session->auth,
caps => $u->caps,
usejournals => list_usejournals($u),
xc3 => {
u => $u
}
}
}
sub addcomment
{
my ($req, $err, $flags) = @_;
return undef unless authenticate($req, $err, $flags);
my $u = $flags->{'u'};
my $journal;
if( $req->{journal} ){
return fail($err,100) unless LJ::canonical_username($req->{journal});
$journal = LJ::load_user($req->{journal}) or return fail($err, 100);
return fail($err,214)
if LJ::Talk::Post::require_captcha_test($u, $journal, $req->{body}, $req->{ditemid});
}else{
$journal = $u;
}
# some additional checks
# return fail($err,314) unless $u->get_cap('paid');
return fail($err,214) if LJ::Comment->is_text_spam( \ $req->{body} );
# create
my $comment = LJ::Comment->create(
journal => $journal,
ditemid => $req->{ditemid},
parenttalkid => ($req->{parenttalkid} || int($req->{parent} / 256)),
poster => $u,
body => $req->{body},
subject => $req->{subject},
props => { picture_keyword => $req->{prop_picture_keyword} }
);
# OK
return {
status => "OK",
commentlink => $comment->url,
dtalkid => $comment->dtalkid,
xc3 => {
u => $u,
comment => {
toplevel => ($comment->parenttalkid == 0 ? 1 : 0),
}
}
};
}
sub getrecentcomments {
my ($req, $err, $flags) = @_;
return undef unless authenticate($req, $err, $flags);
my $u = $flags->{'u'};
my $count = $req->{itemshow};
$count = 10 if !$count || ($count > 100) || ($count < 0);
my @recv = $u->get_recent_talkitems($count);
my @recv_talkids = map { $_->{'jtalkid'} } @recv;
my %recv_userids = map { $_->{'posterid'} => 1} @recv;
my $comment_text = LJ::get_talktext2($u, @recv_talkids);
my $users = LJ::load_userids(keys(%recv_userids));
foreach my $comment ( @recv ) {
$comment->{subject} = $comment_text->{$comment->{jtalkid}}[0];
$comment->{text} = $comment_text->{$comment->{jtalkid}}[1];
$comment->{text} = LJ::trim_widgets(
length => $req->{trim_widgets},
img_length => $req->{widgets_img_length},
text => $comment->{text},
read_more => ' ...',
) if $req->{trim_widgets};
$comment->{text} = LJ::convert_lj_tags_to_links(
event => $comment->{text},
embed_url => $comment->url,
) if $req->{parseljtags};
$comment->{postername} = $users->{$comment->{posterid}}
&& $users->{$comment->{posterid}}->username;
}
return {
status => 'OK',
comments => [ @recv ],
xc3 => {
u => $u
}
};
}
sub getfriendspage
{
my ($req, $err, $flags) = @_;
return undef unless authenticate($req, $err, $flags);
my $u = $flags->{'u'};
my $itemshow = (defined $req->{itemshow}) ? $req->{itemshow} : 100;
return fail($err, 209, "Bad itemshow value") if $itemshow ne int($itemshow ) or $itemshow <= 0 or $itemshow > 100;
my $skip = (defined $req->{skip}) ? $req->{skip} : 0;
return fail($err, 209, "Bad skip value") if $skip ne int($skip ) or $skip < 0 or $skip > 100;
my $lastsync = int $req->{lastsync};
my $before = int $req->{before};
my $before_count = 0;
my $before_skip = 0;
if ($before){
$before_skip = $skip + 0;
$skip = 0;
}
my @entries = LJ::get_friend_items({
'u' => $u,
'userid' => $u->{'userid'},
'remote' => $u,
'itemshow' => $itemshow,
'skip' => $skip,
'dateformat' => 'S2',
});
my @attrs = qw/subject_raw event_raw journalid posterid ditemid security reply_count userpic props security/;
my @uids;
my @res = ();
while (my $ei = shift @entries) {
next unless $ei;
# exit cycle if maximum friend items limit reached
last
if scalar @res >= FRIEND_ITEMS_LIMIT;
# if passed lastsync argument - skip items with logtime less than lastsync
if($lastsync) {
next
if $LJ::EndOfTime - $ei->{rlogtime} <= $lastsync;
}
if($before) {
last if @res >= $itemshow;
push @entries, LJ::get_friend_items({
'u' => $u,
'userid' => $u->{'userid'},
'remote' => $u,
'itemshow' => $itemshow,
'skip' => $skip + ($before_count += $itemshow),
'dateformat' => 'S2',
}) unless @entries;
next if $LJ::EndOfTime - $ei->{rlogtime} > $before;
next if $before_skip-- > 0;
}
my $entry = LJ::Entry->new_from_item_hash($ei);
next unless $entry;
# event result data structure
my %h = ();
# Add more data for public posts
foreach my $method (@attrs) {
$h{$method} = $entry->$method;
}
$h{event_raw} = LJ::trim_widgets(
length => $req->{trim_widgets},
img_length => $req->{widgets_img_length},
text => $h{event_raw},
read_more => ' ...',
) if $req->{trim_widgets};
$h{event_raw} = LJ::convert_lj_tags_to_links(
event => $h{event_raw},
embed_url => $entry->url,
) if $req->{parseljtags};
#userpic
$h{poster_userpic_url} = $h{userpic} && $h{userpic}->url;
# log time value
$h{logtime} = $LJ::EndOfTime - $ei->{rlogtime};
$h{do_captcha} = LJ::Talk::Post::require_captcha_test($u, $entry->poster, '', $h{ditemid}, 1)?1:0;
push @res, \%h;
push @uids, $h{posterid}, $h{journalid};
}
my $users = LJ::load_userids(@uids);
foreach (@res) {
$_->{journalname} = $users->{ $_->{journalid} }->{'user'};
$_->{journaltype} = $users->{ $_->{journalid} }->{'journaltype'};
$_->{journalurl} = $users->{ $_->{journalid} }->journal_base;
delete $_->{journalid};
$_->{postername} = $users->{ $_->{posterid} }->{'user'};
$_->{postertype} = $users->{ $_->{posterid} }->{'journaltype'};
$_->{posterurl} = $users->{ $_->{posterid} }->journal_base;
delete $_->{posterid};
}
LJ::run_hooks("getfriendspage", {userid => $u->userid, });
return {
entries => [ @res ],
skip => $skip,
xc3 => {
u => $u
}
};
}
sub getinbox
{
my ($req, $err, $flags) = @_;
return undef unless authenticate($req, $err, $flags);
my $u = $flags->{'u'};
my $itemshow = (defined $req->{itemshow}) ? $req->{itemshow} : 100;
return fail($err, 209, "Bad itemshow value") if $itemshow ne int($itemshow ) or $itemshow <= 0 or $itemshow > 100;
my $skip = (defined $req->{skip}) ? $req->{skip} : 0;
return fail($err, 209, "Bad skip value") if $skip ne int($skip ) or $skip < 0 or $skip > 100;
# get the user's inbox
my $inbox = $u->notification_inbox or return fail($err, 500, "Cannot get user inbox");
my %type_number = (
Befriended => 1,
Birthday => 2,
CommunityInvite => 3,
CommunityJoinApprove => 4,
CommunityJoinReject => 5,
CommunityJoinRequest => 6,
Defriended => 7,
InvitedFriendJoins => 8,
JournalNewComment => 9,
JournalNewEntry => 10,
NewUserpic => 11,
NewVGift => 12,
OfficialPost => 13,
PermSale => 14,
PollVote => 15,
SupOfficialPost => 16,
UserExpunged => 17,
UserMessageRecvd => 18,
UserMessageSent => 19,
UserNewComment => 20,
UserNewEntry => 21,
);
my %number_type = reverse %type_number;
my @notifications;
my $sync_date;
# check lastsync for valid date
if ($req->{'lastsync'}) {
$sync_date = int $req->{'lastsync'};
if($sync_date <= 0) {
return fail($err,203,"Invalid syncitems date format (must be unixtime)");
}
}
if ($req->{gettype}) {
$req->{gettype} = [$req->{gettype}] unless ref($req->{gettype});
my %filter;
$filter{"LJ::Event::" . $number_type{$_}} = 1 for @{$req->{gettype}};
@notifications = grep { exists $filter{$_->event->class} } $inbox->items;
} else {
@notifications = $inbox->all_items;
}
# By default, notifications are sorted as "oldest are the first"
# Reverse it by "newest are the first"
@notifications = reverse @notifications;
if (my $before = $req->{'before'}) {
return fail($err,203,"Invalid syncitems date format (must be unixtime)") if $before <= 0;
@notifications = grep {$_->when_unixtime <= $before} @notifications;
}
$itemshow = scalar @notifications - $skip if scalar @notifications < $skip + $itemshow;
my @res;
foreach my $item (@notifications[$skip .. $itemshow + $skip - 1]) {
next if $sync_date && $item->when_unixtime < $sync_date;
my $raw = $item->event->raw_info($u, {extended => $req->{extended}});
my $type_index = $type_number{$raw->{type}};
if (defined $type_index) {
$raw->{type} = $type_index;
} else {
$raw->{typename} = $raw->{type};
$raw->{type} = 0;
}
$raw->{state} = $item->{state};
push @res, { %$raw,
when => $item->when_unixtime,
qid => $item->qid,
};
}
return {
'skip' => $skip,
'items' => \@res,
'login' => $u->user,
'journaltype' => $u->journaltype,
xc3 => {
u => $u
}
};
}
sub setmessageread {
my ($req, $err, $flags) = @_;
return undef unless authenticate($req, $err, $flags);
my $u = $flags->{'u'};
# get the user's inbox
my $inbox = $u->notification_inbox or return fail($err, 500, "Cannot get user inbox");
my @result;
# passing requested ids for loading
my @notifications = $inbox->all_items;
# Try to select messages by qid if specified
my @qids = @{$req->{qid}};
if (scalar @qids) {
foreach my $qid (@qids) {
my $item = eval {LJ::NotificationItem->new($u, $qid)};
$item->mark_read if $item;
push @result, { qid => $qid, result => 'set read' };
}
} else { # Else select it by msgid for back compatibility
# make hash of requested message ids
my %requested_items = map { $_ => 1 } @{$req->{messageid}};
# proccessing only requested ids
foreach my $item (@notifications) {
my $msgid = $item->event->raw_info($u)->{msgid};
next unless $requested_items{$msgid};
# if message already read -
if ($item->{state} eq 'R') {
push @result, { msgid => $msgid, result => 'already red' };
next;
}
# in state no 'R' - marking as red
$item->mark_read;
push @result, { msgid => $msgid, result => 'set read' };
}
}
return {
result => \@result,
xc3 => {
u => $u
}
};
}
sub sendmessage
{
my ($req, $err, $flags) = @_;
return fail($err, 315) if $LJ::DISABLED{user_messaging};
return undef unless authenticate($req, $err, $flags);
my $u = $flags->{'u'};
return fail($err, 305) if $u->statusvis eq 'S'; # suspended cannot send private messages
my $msg_limit = LJ::get_cap($u, "usermessage_length");
my @errors;
my $subject_text = LJ::strip_html($req->{'subject'});
return fail($err, 208, 'subject')
unless LJ::text_in($subject_text);
# strip HTML from body and test encoding and length
my $body_text = LJ::strip_html($req->{'body'});
return fail($err, 208, 'body')
unless LJ::text_in($body_text);
my ($msg_len_b, $msg_len_c) = LJ::text_length($body_text);
return fail($err, 212, 'found: ' . LJ::commafy($msg_len_c) . ' characters, it should not exceed ' . LJ::commafy($msg_limit))
unless ($msg_len_c <= $msg_limit);
return fail($err, 213, 'found: ' . LJ::commafy($msg_len_c) . ' characters, it should exceed zero')
if ($msg_len_c <= 0);
my @to = (ref $req->{'to'}) ? @{$req->{'to'}} : ($req->{'to'});
return fail($err, 200) unless scalar @to;
# remove duplicates
my %to = map { lc($_), 1 } @to;
@to = keys %to;
my @msg;
BML::set_language('en'); # FIXME
foreach my $to (@to) {
my $tou = LJ::load_user($to);
return fail($err, 100, $to)
unless $tou;
my $msg = LJ::Message->new({
journalid => $u->userid,
otherid => $tou->userid,
subject => $subject_text,
body => $body_text,
parent_msgid => defined $req->{'parent'} ? $req->{'parent'} + 0 : undef,
userpic => $req->{'userpic'} || undef,
});
push @msg, $msg
if $msg->can_send(\@errors);
}
return fail($err, 203, join('; ', @errors))
if scalar @errors;
foreach my $msg (@msg) {
$msg->send(\@errors);
}
return {
'sent_count' => scalar @msg,
'msgid' => [ grep { $_ } map { $_->msgid } @msg ],
(@errors ? ('last_errors' => \@errors) : () ),
xc3 => {
u => $u
}
};
}
sub login
{
my ($req, $err, $flags) = @_;
return undef unless authenticate($req, $err, $flags);
my $u = $flags->{'u'};
my $res = {
xc3 => {
u => $u
}
};
my $ver = $req->{'ver'};
## check for version mismatches
## non-Unicode installations can't handle versions >=1
return fail($err,207, "This installation does not support Unicode clients")
if $ver>=1 and not $LJ::UNICODE;
# do not let locked people log in
return fail($err, 308) if $u->{statusvis} eq 'L';
## return a message to the client to be displayed (optional)
login_message($req, $res, $flags);
LJ::text_out(\$res->{'message'}) if $ver>=1 and defined $res->{'message'};
## report what shared journals this user may post in
$res->{'usejournals'} = list_usejournals($u);
## return their friend groups
$res->{'friendgroups'} = list_friendgroups($u);
return fail($err, 502, "Error loading friend groups") unless $res->{'friendgroups'};
if ($ver >= 1) {
foreach (@{$res->{'friendgroups'}}) {
LJ::text_out(\$_->{'name'});
}
}
## if they gave us a number of moods to get higher than, then return them
if (defined $req->{'getmoods'}) {
$res->{'moods'} = list_moods($req->{'getmoods'});
if ($ver >= 1) {
# currently all moods are in English, but this might change
foreach (@{$res->{'moods'}}) { LJ::text_out(\$_->{'name'}) }
}
}
### picture keywords, if they asked for them.
if ($req->{'getpickws'} || $req->{'getpickwurls'}) {
my $pickws = list_pickws($u);
@$pickws = sort { lc($a->[0]) cmp lc($b->[0]) } @$pickws;
$res->{'pickws'} = [ map { $_->[0] } @$pickws ] if $req->{'getpickws'};
if ($req->{'getpickwurls'}) {
if ($u->{'defaultpicid'}) {
$res->{'defaultpicurl'} = "$LJ::USERPIC_ROOT/$u->{'defaultpicid'}/$u->{'userid'}";
}
$res->{'pickwurls'} = [ map {
"$LJ::USERPIC_ROOT/$_->[1]/$u->{'userid'}"
} @$pickws ];
}
if ($ver >= 1) {
# validate all text
foreach(@{$res->{'pickws'}}) { LJ::text_out(\$_); }
foreach(@{$res->{'pickwurls'}}) { LJ::text_out(\$_); }
LJ::text_out(\$res->{'defaultpicurl'});
}
}
## return caps, if they asked for them
if ($req->{'getcaps'}) {
$res->{'caps'} = $u->caps;
}
## return client menu tree, if requested
if ($req->{'getmenus'}) {
$res->{'menus'} = hash_menus($u);
if ($ver >= 1) {
# validate all text, just in case, even though currently
# it's all English
foreach (@{$res->{'menus'}}) {
LJ::text_out(\$_->{'text'});
LJ::text_out(\$_->{'url'}); # should be redundant
}
}
}
## tell some users they can hit the fast servers later.
$res->{'fastserver'} = 1 if LJ::get_cap($u, "fastserver");
## user info
$res->{'userid'} = $u->{'userid'};
$res->{'fullname'} = $u->{'name'};
LJ::text_out(\$res->{'fullname'}) if $ver >= 1;
if ($req->{'clientversion'} =~ /^\S+\/\S+$/) {
eval {
LJ::Request->notes("clientver", $req->{'clientversion'});
};
}
## update or add to clientusage table
if ($req->{'clientversion'} =~ /^\S+\/\S+$/ &&
! $LJ::DISABLED{'clientversionlog'})
{
my $client = $req->{'clientversion'};
return fail($err, 208, "Bad clientversion string")
if $ver >= 1 and not LJ::text_in($client);
my $dbh = LJ::get_db_writer();
my $qclient = $dbh->quote($client);
my $cu_sql = "REPLACE INTO clientusage (userid, clientid, lastlogin) " .
"SELECT $u->{'userid'}, clientid, NOW() FROM clients WHERE client=$qclient";
my $sth = $dbh->prepare($cu_sql);
$sth->execute;
unless ($sth->rows) {
# only way this can be 0 is if client doesn't exist in clients table, so
# we need to add a new row there, to get a new clientid for this new client:
$dbh->do("INSERT INTO clients (client) VALUES ($qclient)");
# and now we can do the query from before and it should work:
$sth = $dbh->prepare($cu_sql);
$sth->execute;
}
}
return $res;
}
sub getfriendgroups
{
my ($req, $err, $flags) = @_;
return undef unless authenticate($req, $err, $flags);
my $u = $flags->{'u'};
my $res = {
xc3 => {
u => $u
}
};
$res->{'friendgroups'} = list_friendgroups($u);
return fail($err, 502, "Error loading friend groups") unless $res->{'friendgroups'};
if ($req->{'ver'} >= 1) {
foreach (@{$res->{'friendgroups'} || []}) {
LJ::text_out(\$_->{'name'});
}
}
return $res;
}
sub getusertags
{
my ($req, $err, $flags) = @_;
return undef unless authenticate($req, $err, $flags);
return undef unless check_altusage($req, $err, $flags);
my $u = $flags->{'u'};
my $uowner = $flags->{'u_owner'} || $u;
return fail($req, 502) unless $u && $uowner;
my $tags = LJ::Tags::get_usertags($uowner, { remote => $u });
return {
tags => [ values %$tags ],
xc3 => {
u => $u
}
};
}
sub getfriends
{
my ($req, $err, $flags) = @_;
return undef unless authenticate($req, $err, $flags);
return fail($req,502) unless LJ::get_db_reader();
my $u = $flags->{'u'};
my $res = {
xc3 => {
u => $u
}
};
if ($req->{'includegroups'}) {
$res->{'friendgroups'} = list_friendgroups($u);
return fail($err, 502, "Error loading friend groups") unless $res->{'friendgroups'};
if ($req->{'ver'} >= 1) {
foreach (@{$res->{'friendgroups'} || []}) {
LJ::text_out(\$_->{'name'});
}
}
}
# TAG:FR:protocol:getfriends_of
if ($req->{'includefriendof'}) {
$res->{'friendofs'} = list_friends($u, {
'limit' => $req->{'friendoflimit'},
'friendof' => 1,
});
if ($req->{'ver'} >= 1) {
foreach(@{$res->{'friendofs'}}) { LJ::text_out(\$_->{'fullname'}) };
}
}
# TAG:FR:protocol:getfriends
$res->{'friends'} = list_friends($u, {
'limit' => $req->{'friendlimit'},
'includebdays' => $req->{'includebdays'},
});
if ($req->{'ver'} >= 1) {
foreach(@{$res->{'friends'}}) { LJ::text_out(\$_->{'fullname'}) };
}
return $res;
}
sub friendof
{
my ($req, $err, $flags) = @_;
return undef unless authenticate($req, $err, $flags);
return fail($req,502) unless LJ::get_db_reader();
my $u = $flags->{'u'};
my $res = {
xc3 => {
u => $u
}
};
# TAG:FR:protocol:getfriends_of2 (same as TAG:FR:protocol:getfriends_of)
$res->{'friendofs'} = list_friends($u, {
'friendof' => 1,
'limit' => $req->{'friendoflimit'},
});
if ($req->{'ver'} >= 1) {
foreach(@{$res->{'friendofs'}}) { LJ::text_out(\$_->{'fullname'}) };
}
return $res;
}
sub checkfriends
{
my ($req, $err, $flags) = @_;
return undef unless authenticate($req, $err, $flags);
my $u = $flags->{'u'};
my $res = {
xc3 => {
u => $u
}
};
# return immediately if they can't use this mode
unless (LJ::get_cap($u, "checkfriends")) {
$res->{'new'} = 0;
$res->{'interval'} = 36000; # tell client to bugger off
return $res;
}
## have a valid date?
my $lastupdate = $req->{'lastupdate'};
if ($lastupdate) {
return fail($err,203) unless
($lastupdate =~ /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/);
} else {
$lastupdate = "0000-00-00 00:00:00";
}
my $interval = LJ::get_cap_min($u, "checkfriends_interval");
$res->{'interval'} = $interval;
my $mask;
if ($req->{'mask'} and $req->{'mask'} !~ /\D/) {
$mask = $req->{'mask'};
}
my $memkey = [$u->{'userid'},"checkfriends:$u->{userid}:$mask"];
my $update = LJ::MemCache::get($memkey);
unless ($update) {
# TAG:FR:protocol:checkfriends (wants reading list of mask, not "friends")
my $fr = LJ::get_friends($u, $mask);
unless ($fr && %$fr) {
$res->{'new'} = 0;
$res->{'lastupdate'} = $lastupdate;
return $res;
}
if (@LJ::MEMCACHE_SERVERS) {
my $tu = LJ::get_timeupdate_multi({ memcache_only => 1 }, keys %$fr);
my $max = 0;
while ($_ = each %$tu) {
$max = $tu->{$_} if $tu->{$_} > $max;
}
$update = LJ::TimeUtil->mysql_time($max) if $max;
} else {
my $dbr = LJ::get_db_reader();
unless ($dbr) {
# rather than return a 502 no-db error, just say no updates,
# because problem'll be fixed soon enough by db admins
$res->{'new'} = 0;
$res->{'lastupdate'} = $lastupdate;
return $res;
}
my $list = join(", ", map { int($_) } keys %$fr);
if ($list) {
my $sql = "SELECT MAX(timeupdate) FROM userusage ".
"WHERE userid IN ($list)";
$update = $dbr->selectrow_array($sql);
}
}
LJ::MemCache::set($memkey,$update,time()+$interval) if $update;
}
$update ||= "0000-00-00 00:00:00";
if ($req->{'lastupdate'} && $update gt $lastupdate) {
$res->{'new'} = 1;
} else {
$res->{'new'} = 0;
}
$res->{'lastupdate'} = $update;
return $res;
}
sub getdaycounts
{
my ($req, $err, $flags) = @_;
return undef unless authenticate($req, $err, $flags);
return undef unless check_altusage($req, $err, $flags);
my $u = $flags->{'u'};
my $uowner = $flags->{'u_owner'} || $u;
my $ownerid = $flags->{'ownerid'};
my $res = {
xc3 => {
u => $u
}
};
my $daycts = LJ::get_daycounts($uowner, $u);
return fail($err,502) unless $daycts;
foreach my $day (@$daycts) {
my $date = sprintf("%04d-%02d-%02d", $day->[0], $day->[1], $day->[2]);
push @{$res->{'daycounts'}}, { 'date' => $date, 'count' => $day->[3] };
}
return $res;
}
sub common_event_validation
{
my ($req, $err, $flags) = @_;
# clean up event whitespace
# remove surrounding whitespace
$req->{event} =~ s/^\s+//;
$req->{event} =~ s/\s+$//;
# convert line endings to unix format
if ($req->{'lineendings'} eq "mac") {
$req->{event} =~ s/\r/\n/g;
} else {
$req->{event} =~ s/\r//g;
}
# date validation
if ($req->{'year'} !~ /^\d\d\d\d$/ ||
$req->{'year'} < 1970 || # before unix time started = bad
$req->{'year'} > 2037) # after unix time ends = worse! :)
{
return fail($err,203,"Invalid year value.");
}
if ($req->{'mon'} !~ /^\d{1,2}$/ ||
$req->{'mon'} < 1 ||
$req->{'mon'} > 12)
{
return fail($err,203,"Invalid month value.");
}
if ($req->{'day'} !~ /^\d{1,2}$/ || $req->{'day'} < 1 ||
$req->{'day'} > LJ::TimeUtil->days_in_month($req->{'mon'},
$req->{'year'}))
{
return fail($err,203,"Invalid day of month value.");
}
if ($req->{'hour'} !~ /^\d{1,2}$/ ||
$req->{'hour'} < 0 || $req->{'hour'} > 23)
{
return fail($err,203,"Invalid hour value.");
}
if ($req->{'min'} !~ /^\d{1,2}$/ ||
$req->{'min'} < 0 || $req->{'min'} > 59)
{
return fail($err,203,"Invalid minute value.");
}
# column width
# we only trim Unicode data
if ($req->{'ver'} >=1 ) {
$req->{'subject'} = LJ::text_trim($req->{'subject'}, LJ::BMAX_SUBJECT, LJ::CMAX_SUBJECT);
$req->{'event'} = LJ::text_trim($req->{'event'}, LJ::BMAX_EVENT, LJ::CMAX_EVENT);
foreach (keys %{$req->{'props'}}) {
# do not trim this property, as it's magical and handled later
next if $_ eq 'taglist';
# Allow syn_links and syn_ids the full width of the prop, to avoid truncating long URLS
if ($_ eq 'syn_link' || $_ eq 'syn_id') {
$req->{'props'}->{$_} = LJ::text_trim($req->{'props'}->{$_}, LJ::BMAX_PROP);
} else {
$req->{'props'}->{$_} = LJ::text_trim($req->{'props'}->{$_}, LJ::BMAX_PROP, LJ::CMAX_PROP);
}
}
}
# setup non-user meta-data. it's important we define this here to
# 0. if it's not defined at all, then an editevent where a user
# removes random 8bit data won't remove the metadata. not that
# that matters much. but having this here won't hurt. false
# meta-data isn't saved anyway. so the only point of this next
# line is making the metadata be deleted on edit.
$req->{'props'}->{'unknown8bit'} = 0;
# we don't want attackers sending something that looks like gzipped data
# in protocol version 0 (unknown8bit allowed), otherwise they might
# inject a 100MB string of single letters in a few bytes.
return fail($err,208,"Cannot send gzipped data")
if substr($req->{'event'},0,2) eq "\037\213";
# non-ASCII?
unless ( $flags->{'use_old_content'} || (
LJ::is_ascii($req->{'event'}) &&
LJ::is_ascii($req->{'subject'}) &&
LJ::is_ascii(join(' ', values %{$req->{'props'}})) ))
{
if ($req->{'ver'} < 1) { # client doesn't support Unicode
## Hack: some old clients do send valid UTF-8 data,
## but don't tell us about that.
## Check, if the event/subject are valid UTF-8 strings.
my $tmp_event = $req->{'event'};
my $tmp_subject = $req->{'subject'};
Encode::from_to($tmp_event, "utf-8", "utf-8");
Encode::from_to($tmp_subject, "utf-8", "utf-8");
if ($tmp_event eq $req->{'event'} && $tmp_subject eq $req->{'subject'}) {
## ok, this looks like valid UTF-8
} else {
## encoding is unknown - it's neither ASCII nor UTF-8
# only people should have unknown8bit entries.
my $uowner = $flags->{u_owner} || $flags->{u};
return fail($err,207,'Posting in a community with international or special characters require a Unicode-capable LiveJournal client. Download one at http://www.livejournal.com/download/.')
if $uowner->{journaltype} ne 'P';
# so rest of site can change chars to ? marks until
# default user's encoding is set. (legacy support)
$req->{'props'}->{'unknown8bit'} = 1;
}
} else {
return fail($err,207, "This installation does not support Unicode clients") unless $LJ::UNICODE;
# validate that the text is valid UTF-8
if (!LJ::text_in($req->{'subject'}) ||
!LJ::text_in($req->{'event'}) ||
grep { !LJ::text_in($_) } values %{$req->{'props'}}) {
return fail($err, 208, "The text entered is not a valid UTF-8 stream");
}
}
}
## handle meta-data (properties)
LJ::load_props("log");
foreach my $pname (keys %{$req->{'props'}})
{
my $p = LJ::get_prop("log", $pname);
# does the property even exist?
unless ($p) {
$pname =~ s/[^\w]//g;
return fail($err,205,$pname);
}
# don't validate its type if it's 0 or undef (deleting)
next unless ($req->{'props'}->{$pname});
my $ptype = $p->{'datatype'};
my $val = $req->{'props'}->{$pname};
if ($ptype eq "bool" && $val !~ /^[01]$/) {
return fail($err,204,"Property \"$pname\" should be 0 or 1");
}
if ($ptype eq "num" && $val =~ /[^\d]/) {
return fail($err,204,"Property \"$pname\" should be numeric");
}
if ($pname eq "current_coords" && ! eval { LJ::Location->new(coords => $val) }) {
return fail($err,204,"Property \"current_coords\" has invalid value");
}
}
# check props for inactive userpic
if (my $pickwd = $req->{'props'}->{'picture_keyword'}) {
my $pic = LJ::get_pic_from_keyword($flags->{'u'}, $pickwd);
# need to make sure they aren't trying to post with an inactive keyword, but also
# we don't want to allow them to post with a keyword that has no pic at all to prevent
# them from deleting the keyword, posting, then adding it back with editpics.bml
delete $req->{'props'}->{'picture_keyword'} if ! $pic || $pic->{'state'} eq 'I';
}
# validate incoming list of tags
return fail($err, 211)
if $req->{props}->{taglist} &&
! LJ::Tags::is_valid_tagstring($req->{props}->{taglist});
return 1;
}
sub postevent
{
my ($req, $err, $flags) = @_;
un_utf8_request($req);
my $post_noauth = LJ::run_hook('post_noauth', $req);
return undef unless $post_noauth || authenticate($req, $err, $flags);
my $spam = 0;
LJ::run_hook('spam_detector', $req, \$spam);
return fail($err,320) if $spam;
# if going through mod queue, then we know they're permitted to post at least this entry
$flags->{'usejournal_okay'} = 1 if $post_noauth;
return undef unless check_altusage($req, $err, $flags) || $flags->{nomod};
my $u = $flags->{'u'};
my $ownerid = $flags->{'ownerid'}+0;
my $uowner = $flags->{'u_owner'} || $u;
# Make sure we have a real user object here
$uowner = LJ::want_user($uowner) unless LJ::isu($uowner);
r($uowner) unless LJ::isu($uowner);
my $clusterid = $uowner->{'clusterid'};
my $dbh = LJ::get_db_writer();
my $dbcm = LJ::get_cluster_master($uowner);
return fail($err,306) unless $dbh && $dbcm && $uowner->writer;
return fail($err,200) unless $req->{'event'} =~ /\S/;
### make sure community, shared, or news journals don't post
### note: shared and news journals are deprecated. every shared journal
## should one day be a community journal, of some form.
return fail($err,150) if ($u->{'journaltype'} eq "C" ||
$u->{'journaltype'} eq "S" ||
$u->{'journaltype'} eq "I" ||
$u->{'journaltype'} eq "N");
# underage users can't do this
return fail($err,310) if $u->underage;
# suspended users can't post
return fail($err,305) if ($u->{'statusvis'} eq "S");
# memorials can't post
return fail($err,309) if $u->{statusvis} eq 'M';
# locked accounts can't post
return fail($err,308) if $u->{statusvis} eq 'L';
# check the journal's read-only bit
return fail($err,306) if LJ::get_cap($uowner, "readonly");
# is the user allowed to post?
return fail($err,404,$LJ::MSG_NO_POST) unless LJ::get_cap($u, "can_post");
# is the user allowed to post?
return fail($err,410) if LJ::get_cap($u, "disable_can_post");
# read-only accounts can't post
return fail($err,316) if $u->is_readonly;
# read-only accounts can't be posted to
return fail($err,317) if $uowner->is_readonly;
# can't post to deleted/suspended community
return fail($err,307) unless $uowner->{'statusvis'} eq "V";
# user must have a validated email address to post to any journal - including its own,
# except syndicated (rss, 'Y') journals
# unless this is approved from the mod queue (we'll error out initially, but in case they change later)
return fail($err, 155, "You must have an authenticated email address in order to post to another account")
unless $flags->{'noauth'} || $u->{'status'} eq 'A' || $u->{'journaltype'} eq 'Y';
$req->{'event'} =~ s/\r\n/\n/g; # compact new-line endings to more comfort chars count near 65535 limit
# post content too large
# NOTE: requires $req->{event} be binary data, but we've already
# removed the utf-8 flag in the XML-RPC path, and it never gets
# set in the "flat" protocol path.
return fail($err,409) if length($req->{'event'}) >= LJ::BMAX_EVENT;
my $time_was_faked = 0;
my $offset = 0; # assume gmt at first.
if (defined $req->{'tz'}) {
if ($req->{tz} eq 'guess') {
LJ::get_timezone($u, \$offset, \$time_was_faked);
} elsif ($req->{'tz'} =~ /^[+\-]\d\d\d\d$/) {
# FIXME we ought to store this timezone and make use of it somehow.
$offset = $req->{'tz'} / 100.0;
} else {
return fail($err, 203, "Invalid tz");
}
}
if (defined $req->{'tz'} and not grep { defined $req->{$_} } qw(year mon day hour min)) {
my @ltime = gmtime(time() + ($offset*3600));
$req->{'year'} = $ltime[5]+1900;
$req->{'mon'} = $ltime[4]+1;
$req->{'day'} = $ltime[3];
$req->{'hour'} = $ltime[2];
$req->{'min'} = $ltime[1];
$time_was_faked = 1;
}
return undef
unless common_event_validation($req, $err, $flags);
# confirm we can add tags, at least
return fail($err, 312)
if $req->{props} && $req->{props}->{taglist} &&
! LJ::Tags::can_add_tags($uowner, $u);
my $event = $req->{'event'};
### allow for posting to journals that aren't yours (if you have permission)
my $posterid = $u->{'userid'}+0;
# make the proper date format
my $eventtime = sprintf("%04d-%02d-%02d %02d:%02d",
$req->{'year'}, $req->{'mon'},
$req->{'day'}, $req->{'hour'},
$req->{'min'});
my $qeventtime = $dbh->quote($eventtime);
# load userprops all at once
my @poster_props = qw(newesteventtime dupsig_post);
my @owner_props = qw(newpost_minsecurity moderated);
push @owner_props, 'opt_weblogscom' unless $req->{'props'}->{'opt_backdated'};
LJ::load_user_props($u, @poster_props, @owner_props);
if ($uowner->{'userid'} == $u->{'userid'}) {
$uowner->{$_} = $u->{$_} foreach (@owner_props);
} else {
LJ::load_user_props($uowner, @owner_props);
}
# are they trying to post back in time?
if ($posterid == $ownerid && $u->{'journaltype'} ne 'Y' &&
!$time_was_faked && $u->{'newesteventtime'} &&
$eventtime lt $u->{'newesteventtime'} &&
!$req->{'props'}->{'opt_backdated'}) {
return fail($err, 153, "You have an entry which was posted at $u->{'newesteventtime'}, but you're trying to post an entry before this. Please check the date and time of both entries. If the other entry is set in the future on purpose, edit that entry to use the \"Date Out of Order\" option. Otherwise, use the \"Date Out of Order\" option for this entry instead.");
}
my $qallowmask = $req->{'allowmask'}+0;
my $security = "public";
my $uselogsec = 0;
if ($req->{'security'} eq "usemask" || $req->{'security'} eq "private") {
$security = $req->{'security'};
}
if ($req->{'security'} eq "usemask") {
$uselogsec = 1;
}
# can't specify both a custom security and 'friends-only'
return fail($err, 203, "Invalid friends group security set")
if $qallowmask > 1 && $qallowmask % 2;
## if newpost_minsecurity is set, new entries have to be
## a minimum security level
$security = "private"
if $uowner->newpost_minsecurity eq "private";
($security, $qallowmask) = ("usemask", 1)
if $uowner->newpost_minsecurity eq "friends"
and $security eq "public";
my $qsecurity = $dbh->quote($security);
### make sure user can't post with "custom/private security" on shared journals
return fail($err,102)
if ($ownerid != $posterid && # community post
($req->{'security'} eq "private" ||
($req->{'security'} eq "usemask" && $qallowmask != 1 )));
# make sure this user isn't banned from posting here (if
# this is a community journal)
return fail($err,151) if
LJ::is_banned($posterid, $ownerid);
# don't allow backdated posts in communities
return fail($err,152) if
($req->{'props'}->{"opt_backdated"} &&
$uowner->{'journaltype'} ne "P");
# do processing of embedded polls (doesn't add to database, just
# does validity checking)
my @polls = ();
if (LJ::Poll->contains_new_poll(\$event))
{
return fail($err,301,"Your account type doesn't permit creating polls.")
unless (LJ::get_cap($u, "makepoll")
|| ($uowner->{'journaltype'} eq "C"
&& LJ::get_cap($uowner, "makepoll")
&& LJ::can_manage_other($u, $uowner)));
my $error = "";
@polls = LJ::Poll->new_from_html(\$event, \$error, {
'journalid' => $ownerid,
'posterid' => $posterid,
});
return fail($err,103,$error) if $error;
}
# convert RTE lj-embeds to normal lj-embeds
$event = LJ::EmbedModule->transform_rte_post($event);
# process module embedding
LJ::EmbedModule->parse_module_embed($uowner, \$event);
my $now = $dbcm->selectrow_array("SELECT UNIX_TIMESTAMP()");
my $anum = int(rand(256));
# by default we record the true reverse time that the item was entered.
# however, if backdate is on, we put the reverse time at the end of time
# (which makes it equivalent to 1969, but get_recent_items will never load
# it... where clause there is: < $LJ::EndOfTime). but this way we can
# have entries that don't show up on friends view, now that we don't have
# the hints table to not insert into.
my $rlogtime = $LJ::EndOfTime;
unless ($req->{'props'}->{"opt_backdated"}) {
$rlogtime -= $now;
}
my $dupsig = Digest::MD5::md5_hex(join('', map { $req->{$_} }
qw(subject event usejournal security allowmask)));
my $lock_key = "post-$ownerid";
# release our duplicate lock
my $release = sub { $dbcm->do("SELECT RELEASE_LOCK(?)", undef, $lock_key); };
# our own local version of fail that releases our lock first
my $fail = sub { $release->(); return fail(@_); };
my $res = {};
my $res_done = 0; # set true by getlock when post was duplicate, or error getting lock
my $getlock = sub {
my $r = $dbcm->selectrow_array("SELECT GET_LOCK(?, 2)", undef, $lock_key);
unless ($r) {
$res = undef; # a failure case has an undef result
fail($err,503); # set error flag to "can't get lock";
$res_done = 1; # tell caller to bail out
return;
}
my @parts = split(/:/, $u->{'dupsig_post'});
if ($parts[0] eq $dupsig) {
# duplicate! let's make the client think this was just the
# normal first response.
$res->{'itemid'} = $parts[1];
$res->{'anum'} = $parts[2];
my $dup_entry = LJ::Entry->new($uowner, jitemid => $res->{'itemid'}, anum => $res->{'anum'});
$res->{'url'} = $dup_entry->url;
$res_done = 1;
$release->();
}
};
my $need_moderated = ( $uowner->{'moderated'} =~ /^[1A]$/ ) ? 1 : 0;
if ( $uowner->{'moderated'} eq 'F' ) {
## Scan post for spam
LJ::run_hook('spam_community_detector', $uowner, $req, \$need_moderated);
}
# if posting to a moderated community, store and bail out here
if ($uowner->{'journaltype'} eq 'C' && $need_moderated && !$flags->{'nomod'}) {
# don't moderate admins, moderators & pre-approved users
my $dbh = LJ::get_db_writer();
my $relcount = $dbh->selectrow_array("SELECT COUNT(*) FROM reluser ".
"WHERE userid=$ownerid AND targetid=$posterid ".
"AND type IN ('A','M','N')");
unless ($relcount) {
# moderation queue full?
my $modcount = $dbcm->selectrow_array("SELECT COUNT(*) FROM modlog WHERE journalid=$ownerid");
return fail($err, 407) if $modcount >= LJ::get_cap($uowner, "mod_queue");
$modcount = $dbcm->selectrow_array("SELECT COUNT(*) FROM modlog ".
"WHERE journalid=$ownerid AND posterid=$posterid");
return fail($err, 408) if $modcount >= LJ::get_cap($uowner, "mod_queue_per_poster");
$req->{'_moderate'}->{'authcode'} = LJ::make_auth_code(15);
# create tag from HTML-tag