mourningdove/cgi-bin/LJ/Protocol.pm

4460 lines
144 KiB
Perl
Raw Normal View History

2026-05-24 01:03:05 +00:00
#!/usr/bin/perl
#
# This code was forked from the LiveJournal project owned and operated
# by Live Journal, Inc. The code has been modified and expanded by
# Dreamwidth Studios, LLC. These files were originally licensed under
# the terms of the license supplied by Live Journal, Inc, which can
# currently be found at:
#
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
#
# In accordance with the original license, this code and all its
# modifications are provided under the GNU General Public License.
# A copy of that license can be found in the LICENSE file included as
# part of this distribution.
use strict;
no warnings 'uninitialized';
use Digest::MD5;
use Encode ();
use SOAP::Lite ();
use LJ::Global::Constants;
use LJ::Console;
use LJ::Event::JournalNewEntry;
use LJ::Event::AddedToCircle;
use LJ::Entry;
use LJ::Poll;
use LJ::Config;
use LJ::Comment;
use DW::Task::SphinxCopier;
use DW::Task::XPost;
LJ::Config->load;
use DW::API::Key;
use DW::Auth::Challenge;
use LJ::Tags;
use LJ::Feed;
use LJ::EmbedModule;
#### 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 security on community journals." ],
"103" => [ E_PERM, "Poll error" ],
"104" => [ E_TEMP, "Error adding one or more friends" ],
"105" => [ E_PERM, "Challenge expired" ],
"106" =>
[ E_PERM, "Can only use administrator-locked security on community journals you manage." ],
"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" ],
"157" => [ E_TEMP, "Tags error" ],
"158" => [ E_PERM, "Comment 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." ],
# 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." ],
"411" => [ E_PERM, "Subject too long." ],
"412" => [ E_PERM, "Maximum number of comments reached" ],
# 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." ],
"507" => [ E_TEMP, "Method temporarily disabled; try again later." ],
);
sub translate {
my ( $u, $msg, $vars ) = @_;
# we no longer support preferred language selection
return LJ::Lang::get_default_text( "protocol.$msg", $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_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 );
DW::Stats::increment( 'dw.protocol_request', 1, ["method:$method"] );
if ( $method eq "login" ) { return login(@args); }
if ( $method eq "getfriendgroups" ) { return getfriendgroups(@args); }
if ( $method eq "gettrustgroups" ) { return gettrustgroups(@args); }
if ( $method eq "getfriends" ) { return getfriends(@args); }
if ( $method eq "getcircle" ) { return getcircle(@args); }
if ( $method eq "editcircle" ) { return editcircle(@args); }
if ( $method eq "friendof" ) { return friendof(@args); }
if ( $method eq "checkfriends" ) { return checkfriends(@args); }
if ( $method eq "checkforupdates" ) { return checkforupdates(@args); }
if ( $method eq "getdaycounts" ) { return getdaycounts(@args); }
if ( $method eq "postevent" ) { return postevent(@args); }
if ( $method eq "editevent" ) { return editevent(@args); }
if ( $method eq "syncitems" ) { return syncitems(@args); }
if ( $method eq "getevents" ) { return getevents(@args); }
if ( $method eq "editfriends" ) { return editfriends(@args); }
if ( $method eq "editfriendgroups" ) { return editfriendgroups(@args); }
if ( $method eq "consolecommand" ) { return consolecommand(@args); }
if ( $method eq "getchallenge" ) { return getchallenge(@args); }
if ( $method eq "sessiongenerate" ) { return sessiongenerate(@args); }
if ( $method eq "sessionexpire" ) { return sessionexpire(@args); }
if ( $method eq "getusertags" ) { return getusertags(@args); }
if ( $method eq "getfriendspage" ) { return getfriendspage(@args); }
if ( $method eq "getreadpage" ) { return getreadpage(@args); }
if ( $method eq "getinbox" ) { return getinbox(@args); }
if ( $method eq "sendmessage" ) { return sendmessage(@args); }
if ( $method eq "setmessageread" ) { return setmessageread(@args); }
if ( $method eq "addcomment" ) { return addcomment(@args); }
return fail( $err, 201 );
}
sub addcomment {
my ( $req, $err, $flags ) = @_;
return undef unless authenticate( $req, $err, $flags );
my $u = $flags->{'u'};
# some additional checks
return fail( $err, 314 ) unless $u->is_paid || $flags->{nocheckcap};
my $journal;
if ( $req->{journal} ) {
$journal = LJ::load_user( $req->{journal} ) or return fail( $err, 100 );
my $entry = LJ::Entry->new( $journal, ditemid => $req->{ditemid} );
return fail( $err, 214 )
if LJ::Talk::Post::require_captcha_test( $u, $journal, $req->{body}, $entry );
}
else {
$journal = $u;
}
# create
my $comment_err;
my $comment = LJ::Comment->create(
journal => $journal,
ditemid => $req->{ditemid},
parenttalkid => ( $req->{parenttalkid} || ( $req->{parent} >> 8 ) ),
poster => $u,
body => $req->{body},
subject => $req->{subject},
props => {
picture_keyword => $req->{prop_picture_keyword},
editor => $req->{prop_editor},
},
err_ref => \$comment_err,
);
my $err_code_mapping = {
bad_journal => 206, # authenticate() takes care of this
bad_poster => 100, # authenticate() takes care of this
bad_args => 202,
no_entry => 200,
too_many_comments => 412,
init_comment => 158,
post_comment => 158,
}->{ $comment_err->{code} }
if $comment_err;
return fail( $err, $err_code_mapping, $comment_err->{msg} ) if $comment_err;
my %props = ();
$props{useragent} = $req->{useragent} if $req->{useragent};
$comment->set_props(%props);
# OK
return {
status => "OK",
commentlink => $comment->url,
dtalkid => $comment->dtalkid,
};
}
sub getfriendspage {
return fail( $_[1], 504, "Use 'getreadpage' instead." );
}
sub getreadpage {
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 @entries = $u->watch_items(
remote => $u,
itemshow => $itemshow,
skip => $skip,
dateformat => 'S2',
);
my @attrs = qw/subject_raw event_raw journalid posterid ditemid security/;
my @uids;
my @res = ();
my $lastsync = int $req->{lastsync};
foreach my $ei (@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;
}
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;
}
# log time value
$h{logtime} = $LJ::EndOfTime - $ei->{rlogtime};
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'};
delete $_->{journalid};
$_->{postername} = $users->{ $_->{posterid} }->{'user'};
$_->{postertype} = $users->{ $_->{posterid} }->{'journaltype'};
delete $_->{posterid};
}
LJ::Hooks::run_hooks( "getfriendspage", { 'userid' => $u->userid, } );
return { 'entries' => [@res] };
}
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 = (
AddedToCircle => 1,
Birthday => 2,
CommunityInvite => 3,
CommunityJoinApprove => 4,
CommunityJoinReject => 5,
CommunityJoinRequest => 6,
RemovedFromCircle => 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,
);
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} ) {
@notifications =
grep { $_->event->class eq "LJ::Event::" . $number_type{ $req->{gettype} } }
$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;
$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 {
'items' => \@res,
'login' => $u->user,
'journaltype' => $u->journaltype
};
}
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 };
}
# Sends a private message from one account to another
sub sendmessage {
my ( $req, $err, $flags ) = @_;
return fail( $err, 315 ) unless LJ::is_enabled('user_messaging');
return undef unless authenticate( $req, $err, $flags );
my $u = $flags->{'u'};
return fail( $err, 305 ) if $u->is_suspended; # suspended cannot send private messages
my $msg_limit = $u->count_usermessage_length;
my @errors;
# test encoding and length
my $subject_text = $req->{'subject'};
return fail( $err, 208, 'subject' )
unless LJ::text_in($subject_text);
# test encoding and length
my $body_text = $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 );
#test if to argument is present
return fail( $err, 200, "to" ) unless exists $req->{'to'};
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 $msguserpic;
$msguserpic = $req->{'userpic'} if defined $req->{'userpic'};
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 => $msguserpic,
}
);
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 ) : () ),
};
}
sub login {
my ( $req, $err, $flags ) = @_;
return undef unless authenticate( $req, $err, $flags );
my $u = $flags->{'u'};
my $res = {};
my $ver = $req->{'ver'};
# do not let locked people log in
return fail( $err, 308 ) if $u->is_locked;
## 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'} ) {
my $pickws = list_pickws($u);
@$pickws = sort { lc( $a->[0] ) cmp lc( $b->[0] ) } @$pickws;
$res->{'pickws'} = [ map { $_->[0] } @$pickws ];
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 $u->can_use_fastlane;
## user info
$res->{'userid'} = $u->{'userid'};
$res->{'fullname'} = $u->{'name'};
LJ::text_out( \$res->{'fullname'} ) if $ver >= 1;
if ( $req->{'clientversion'} =~ /^\S+\/\S+$/ ) {
eval {
my $apache_r = BML::get_request();
$apache_r->notes->{clientver} = $req->{'clientversion'};
};
}
## update or add to clientusage table
if ( $req->{'clientversion'} =~ /^\S+\/\S+$/
&& LJ::is_enabled('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;
}
#deprecated
sub getfriendgroups {
return fail( $_[1], 504 );
}
sub gettrustgroups {
my ( $req, $err, $flags ) = @_;
return undef unless authenticate( $req, $err, $flags );
my $u = $flags->{u};
my $res = {};
$res->{trustgroups} = list_trustgroups($u);
return fail( $err, 502, "Error loading trust groups" ) unless $res->{trustgroups};
if ( $req->{ver} >= 1 ) {
foreach ( @{ $res->{trustgroups} || [] } ) {
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 ] };
}
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 = {};
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 getcircle {
my ( $req, $err, $flags ) = @_;
return undef unless authenticate( $req, $err, $flags );
my $u = $flags->{u};
my $res = {};
my $limit = $LJ::MAX_WT_EDGES_LOAD;
$limit = $req->{limit}
if defined $req->{limit} && $req->{limit} < $limit;
if ( $req->{includetrustgroups} ) {
$res->{trustgroups} = list_trustgroups($u);
return fail( $err, 502, "Error loading trust groups" ) unless $res->{trustgroups};
if ( $req->{ver} >= 1 ) {
LJ::text_out( \$_->{name} ) foreach ( @{ $res->{trustgroups} || [] } );
}
}
if ( $req->{includecontentfilters} ) {
$res->{contentfilters} = list_contentfilters($u);
return fail( $err, 502, "Error loading content filters" ) unless $res->{contentfilters};
if ( $req->{ver} >= 1 ) {
LJ::text_out( \$_->{name} ) foreach ( @{ $res->{contentfilters} || [] } );
}
}
if ( $req->{includewatchedusers} ) {
$res->{watchedusers} = list_users(
$u,
limit => $limit,
watched => 1,
includebdays => $req->{includebdays},
);
if ( $req->{ver} >= 1 ) {
LJ::text_out( \$_->{fullname} ) foreach ( @{ $res->{watchedusers} || [] } );
}
}
if ( $req->{includewatchedby} ) {
$res->{watchedbys} = list_users(
$u,
limit => $limit,
watchedby => 1,
);
if ( $req->{ver} >= 1 ) {
LJ::text_out( \$_->{fullname} ) foreach ( @{ $res->{watchedbys} || [] } );
}
}
if ( $req->{includetrustedusers} ) {
$res->{trustedusers} = list_users(
$u,
limit => $limit,
trusted => 1,
includebdays => $req->{includebdays},
);
if ( $req->{ver} >= 1 ) {
LJ::text_out( \$_->{fullname} ) foreach ( @{ $res->{trustedusers} || [] } );
}
}
if ( $req->{includetrustedby} ) {
$res->{trustedbys} = list_users(
$u,
limit => $limit,
trustedby => 1,
);
if ( $req->{ver} >= 1 ) {
LJ::text_out( \$_->{fullname} ) foreach ( @{ $res->{trustedbys} || [] } );
}
}
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 = {};
# 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 {
return fail( $_[1], 504, "Use 'checkforupdates' instead." );
}
sub checkforupdates {
my ( $req, $err, $flags ) = @_;
return undef unless authenticate( $req, $err, $flags );
my $u = $flags->{'u'};
my $res = {};
# return immediately if they can't use this mode
unless ( $u->can_use_checkforupdates ) {
$res->{'new'} = 0;
$res->{'interval'} = 36000;
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::Capabilities::get_cap_min( $u, "checkfriends_interval" );
$res->{'interval'} = $interval;
my $filter;
if ( $req->{filter} ) {
$filter = $u->content_filters( name => $req->{filter} );
return fail( $err, 203,
"Invalid filter name. Trying to check updates for a filter that does not exist." )
unless $filter;
}
my $memkey = [ $u->id, "checkforupdates:$u->{userid}:" . ( $filter ? $filter->id : "" ) ];
my $update = LJ::MemCache::get($memkey);
unless ($update) {
my @fr = $u->watched_userids;
# FIXME: see whether we can just get the list of users who are in the filter
if ($filter) {
my @filter_users;
foreach my $fid (@fr) {
push @filter_users, $fid
if $filter->contains_userid($fid);
}
@fr = @filter_users;
}
unless (@fr) {
$res->{'new'} = 0;
$res->{'lastupdate'} = $lastupdate;
return $res;
}
if (@LJ::MEMCACHE_SERVERS) {
my $tu = LJ::get_timeupdate_multi( { memcache_only => 1 }, @fr );
my $max = 0;
foreach ( values %$tu ) {
$max = $_ if $_ > $max;
}
$update = LJ::mysql_time($max) if $max;
}
unless ($update) {
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($_) } @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'};
return fail( $err, 502 ) unless LJ::isu($uowner);
my $res = {};
my $daycts = $uowner->get_daycounts($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 (must be in the range 1970-2037)." );
}
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::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." );
}
# 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
# 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->is_person;
}
# 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" );
}
}
# trim to column width
# we did a quick check for number of bytes earlier
# this one also handles the case of too many characters,
# even if we'd be within the byte limit
my $did_trim = 0;
$req->{'event'} = LJ::text_trim( $req->{'event'}, LJ::BMAX_EVENT, LJ::CMAX_EVENT, \$did_trim );
return fail( $err, 409 ) if $did_trim;
$did_trim = 0;
$req->{'subject'} =
LJ::text_trim( $req->{'subject'}, LJ::BMAX_SUBJECT, LJ::CMAX_SUBJECT, \$did_trim );
return fail( $err, 411 ) if $did_trim && !$flags->{allow_truncated_subject};
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 );
}
}
## handle meta-data (properties)
LJ::load_props("log");
my $allow_system = $flags->{allow_system} || {};
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 );
}
# This is a system logprop
# fail with unknown metadata here?
if ( $p->{ownership} eq 'system' && !( $allow_system == 1 || $allow_system->{$pname} ) ) {
$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'} ) and !$flags->{allow_inactive} ) {
my $pic = LJ::Userpic->new_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 the editicons page
delete $req->{props}->{picture_keyword}
unless $pic && $pic->state ne '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 schedule_xposts {
my ( $u, $ditemid, $deletep, $fn ) = @_;
return unless LJ::isu($u) && $ditemid > 0;
return unless $fn && ref $fn eq 'CODE';
my ( @successes, @failures );
my @accounts = DW::External::Account->get_external_accounts($u);
foreach my $acct (@accounts) {
my ( $xpostp, $info ) = $fn->($acct);
next unless $xpostp;
my $jobargs = {
uid => $u->userid,
accountid => $acct->acctid,
ditemid => $ditemid + 0,
delete => $deletep ? 1 : 0,
%{$info}
};
DW::TaskQueue->dispatch( DW::Task::XPost->new($jobargs) );
push @successes, $acct;
}
return ( \@successes, \@failures );
}
sub postevent {
my ( $req, $err, $flags ) = @_;
un_utf8_request($req);
# if the importer is calling us, we want to allow it to post in all but the most extreme
# of cases. and even then, we try our hardest to allow content to be posted. setting this
# flag will bypass a lot of the safety restrictions about who can post where and when, so
# we trust the importer to be intelligent about this. (And if you aren't the importer, don't
# use this option!!!)
my $importer_bypass = $flags->{importer_bypass} ? 1 : 0;
if ($importer_bypass) {
$flags->{nomod} = 1;
$flags->{ignore_tags_max} = 1;
$flags->{nonotify} = 1;
$flags->{noauth} = 1;
$flags->{usejournal_okay} = 1;
$flags->{no_xpost} = 1;
$flags->{create_unknown_picture_mapid} = 1;
$flags->{allow_inactive} = 1;
}
return undef
unless LJ::Hooks::run_hook( 'post_noauth', $req ) || authenticate( $req, $err, $flags );
# if going through mod queue, then we know they're permitted to post at least this entry
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);
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 journals don't post
return fail( $err, 150 ) if $u->is_community;
# suspended users can't post
return fail( $err, 305 ) if !$importer_bypass && $u->is_suspended;
# memorials can't post
return fail( $err, 309 ) if !$importer_bypass && $u->is_memorial;
# locked accounts can't post
return fail( $err, 308 ) if !$importer_bypass && $u->is_locked;
# check the journal's read-only bit
return fail( $err, 306 ) if $uowner->is_readonly;
# is the user allowed to post?
return fail( $err, 404, $LJ::MSG_NO_POST ) unless $importer_bypass || $u->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 $importer_bypass || $uowner->is_visible;
# must have a validated email address to post to a community
# 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 $u->equals($uowner) || $u->{'status'} eq 'A' || $flags->{'nomod'};
return fail( $err, 155, "You must confirm your email address before posting." )
if $u->{'status'} eq 'N' && !$importer_bypass && !$u->is_syndicated && !$LJ::_T_CONFIG;
# 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 );
# now we can move over to picture_mapid instead of picture_keyword if appropriate
if ( $req->{props} && defined $req->{props}->{picture_keyword} && $u->userpic_have_mapid ) {
$req->{props}->{picture_mapid} =
$u->get_mapid_from_keyword( $req->{props}->{picture_keyword},
create => $flags->{create_unknown_picture_mapid} || 0 );
delete $req->{props}->{picture_keyword};
}
# confirm we can add tags, at least
return fail( $err, 312 )
if $req->{props}
&& $req->{props}->{taglist}
&& !( $importer_bypass || 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);
$u->preload_props( @poster_props, @owner_props );
if ( $u->equals($uowner) ) {
$uowner->{$_} = $u->{$_} foreach @owner_props;
}
else {
$uowner->preload_props(@owner_props);
}
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 security" on communities
return fail( $err, 102 )
if $ownerid != $posterid && # community post
$req->{'security'} eq "usemask" && $qallowmask != 1;
## make sure user can't post with "private security" on communities they don't manage
return fail( $err, 106 )
if $ownerid != $posterid && # community post
$req->{'security'} eq "private"
&& !$u->can_manage($uowner);
# make sure this user isn't banned from posting here (if
# this is a community journal)
return fail( $err, 151 ) if $uowner->has_banned($u);
# don't allow backdated posts in communities
return fail( $err, 152 )
if ( $req->{props}->{opt_backdated}
&& !$importer_bypass
&& $uowner->is_community );
# 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 ( $u->can_create_polls
|| ( $uowner->is_community && $uowner->can_create_polls ) );
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 $logtime = "FROM_UNIXTIME($now)";
# this is when the entry was posted. for most cases this is accurate but in case
# we're using the importer in the community case, it will mess life up.
if ( $importer_bypass && $posterid != $ownerid ) {
$logtime = $qeventtime;
$rlogtime = "$LJ::EndOfTime - UNIX_TIMESTAMP($qeventtime)";
}
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;
}
# If we're the importer, don't do duplicate detection here; the importer already
# has tooling to do that to compare remote vs local
return if $importer_bypass;
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->();
}
};
# if posting to a moderated community, store and bail out here
if ( $uowner->is_community && $uowner->{'moderated'} && !$flags->{'nomod'} ) {
# Don't moderate 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 ('N')" );
unless ($relcount) {
# moderation queue full?
my $modcount =
$dbcm->selectrow_array("SELECT COUNT(*) FROM modlog WHERE journalid=$ownerid");
return fail( $err, 407 ) if $modcount >= $uowner->count_max_mod_queue;
$modcount = $dbcm->selectrow_array( "SELECT COUNT(*) FROM modlog "
. "WHERE journalid=$ownerid AND posterid=$posterid" );
return fail( $err, 408 ) if $modcount >= $uowner->count_max_mod_queue_per_poster;
$req->{'_moderate'}->{'authcode'} = LJ::make_auth_code(15);
# create tag <lj-embed> from HTML-tag <embed>
LJ::EmbedModule->parse_module_embed( $uowner, \$req->{event} );
my $fr = $dbcm->quote( Storable::freeze($req) );
return fail( $err, 409 ) if length($fr) > 600_000;
# store
my $modid = LJ::alloc_user_counter( $uowner, "M" );
return fail( $err, 501 ) unless $modid;
$uowner->do(
"INSERT INTO modlog (journalid, modid, posterid, subject, logtime) "
. "VALUES ($ownerid, $modid, $posterid, ?, NOW())",
undef,
LJ::text_trim( $req->{'subject'}, 30, 0 )
);
return fail( $err, 501 ) if $uowner->err;
$uowner->do( "INSERT INTO modblob (journalid, modid, request_stor) "
. "VALUES ($ownerid, $modid, $fr)" );
if ( $uowner->err ) {
$uowner->do("DELETE FROM modlog WHERE journalid=$ownerid AND modid=$modid");
return fail( $err, 501 );
}
# expire mod_queue_count memcache
$uowner->memc_delete('mqcount');
# alert moderator(s)
my $mods = LJ::load_rel_user( $dbh, $ownerid, 'M' ) || [];
if (@$mods) {
my $modlist = LJ::load_userids(@$mods);
my @emails;
foreach my $mod ( values %$modlist ) {
next unless $mod->is_visible;
LJ::Event::CommunityModeratedEntryNew->new( $mod, $uowner, $modid )->fire;
}
}
my $msg = translate( $u, "modpost", undef );
return { 'message' => $msg };
}
} # /moderated comms
# posting:
$getlock->();
return $res if $res_done;
# do rate-checking
if ( !$u->is_syndicated && !$u->rate_log( "post", 1 ) && !$importer_bypass ) {
return $fail->( $err, 405 );
}
my $jitemid = LJ::alloc_user_counter( $uowner, "L" );
return $fail->( $err, 501, "No itemid could be generated." ) unless $jitemid;
LJ::Entry->can("dostuff");
LJ::replycount_do( $uowner, $jitemid, "init" );
my $dberr;
$uowner->log2_do( \$dberr,
"INSERT INTO log2 (journalid, jitemid, posterid, eventtime, logtime, security, "
. "allowmask, replycount, year, month, day, revttime, rlogtime, anum) "
. "VALUES ($ownerid, $jitemid, $posterid, $qeventtime, $logtime, $qsecurity, $qallowmask, "
. "0, $req->{'year'}, $req->{'mon'}, $req->{'day'}, $LJ::EndOfTime-"
. "UNIX_TIMESTAMP($qeventtime), $rlogtime, $anum)" );
return $fail->( $err, 501, $dberr ) if $dberr;
LJ::MemCache::incr( [ $ownerid, "log2ct:$ownerid" ] );
$uowner->clear_daycounts( $qallowmask || $security );
# set userprops.
{
my %set_userprop;
# keep track of itemid/anum for later potential duplicates
$set_userprop{"dupsig_post"} = "$dupsig:$jitemid:$anum";
# record the eventtime of the last update (for own journals only)
$set_userprop{"newesteventtime"} = $eventtime
if $posterid == $ownerid
and not $req->{'props'}->{'opt_backdated'}
and not $time_was_faked;
$u->set_prop( \%set_userprop );
}
# end duplicate locking section
$release->();
my $ditemid = $jitemid * 256 + $anum;
### finish embedding stuff now that we have the itemid
{
### this should NOT return an error, and we're mildly fucked by now
### if it does (would have to delete the log row up there), so we're
### not going to check it for now.
my $error = "";
foreach my $poll (@polls) {
$poll->save_to_db(
journalid => $ownerid,
posterid => $posterid,
ditemid => $ditemid,
error => \$error,
);
my $pollid = $poll->pollid;
$event =~ s/<poll-placeholder>/<poll-$pollid>/;
}
}
#### /embedding
# record journal's disk usage
my $bytes = length($event) + length( $req->{'subject'} );
$uowner->dudata_set( 'L', $jitemid, $bytes );
$uowner->do(
"INSERT INTO logtext2 (journalid, jitemid, subject, event) "
. "VALUES ($ownerid, $jitemid, ?, ?)",
undef, $req->{'subject'}, LJ::text_compress($event)
);
if ( $uowner->err ) {
my $msg = $uowner->errstr;
LJ::delete_entry( $uowner, $jitemid ); # roll-back
return fail( $err, 501, "logtext:$msg" );
}
LJ::MemCache::set( [ $ownerid, "logtext:$clusterid:$ownerid:$jitemid" ],
[ $req->{'subject'}, $event ] );
# warn the user of any bad markup errors
my $clean_event = $event;
my $errref;
# TODO: accept editor prop and thread it through to the cleaner.
my $editor = undef;
LJ::CleanHTML::clean_event( \$clean_event, { errref => \$errref, editor => $editor } );
$res->{message} = translate(
$u, $errref,
{
aopts => "href='$LJ::SITEROOT/editjournal?journal="
. $uowner->user
. "&itemid=$ditemid'"
}
) if $errref;
# keep track of custom security stuff in other table.
if ($uselogsec) {
$uowner->do( "INSERT INTO logsec2 (journalid, jitemid, allowmask) "
. "VALUES ($ownerid, $jitemid, $qallowmask)" );
if ( $uowner->err ) {
my $msg = $uowner->errstr;
LJ::delete_entry( $uowner, $jitemid ); # roll-back
return fail( $err, 501, "logsec2:$msg" );
}
}
# Entry tags
if ( $req->{props} && defined $req->{props}->{taglist} && $req->{props}->{taglist} ne '' ) {
# slightly misnamed, the taglist is/was normally a string, but now can also be an arrayref.
my $taginput = $req->{props}->{taglist};
my $tagerr = "";
my $logtag_opts = {
remote => $u,
ignore_max => $flags->{ignore_tags_max} ? 1 : 0,
force => $importer_bypass,
err_ref => \$tagerr,
};
if ( ref $taginput eq 'ARRAY' ) {
$logtag_opts->{set} = [@$taginput];
$req->{props}->{taglist} = join( ", ", @$taginput );
}
else {
$logtag_opts->{set_string} = $taginput;
}
# Do not fail here; worst case we lose tags, but if we fail here we don't perform
# half of the processing below
LJ::Tags::update_logtags( $uowner, $jitemid, $logtag_opts );
# Propagate any "skippable" errors
$res->{message} = $tagerr if $tagerr;
}
# meta-data
if ( %{ $req->{'props'} } ) {
my $propset = {};
foreach my $pname ( keys %{ $req->{'props'} } ) {
next unless $req->{'props'}->{$pname};
next if $pname eq "revnum" || $pname eq "revtime";
my $p = LJ::get_prop( "log", $pname );
next unless $p;
next unless $req->{'props'}->{$pname};
$propset->{$pname} = $req->{'props'}->{$pname};
}
my %logprops;
LJ::set_logprop( $uowner, $jitemid, $propset, \%logprops ) if %$propset;
# if set_logprop modified props above, we can set the memcache key
# to be the hashref of modified props, since this is a new post
LJ::MemCache::set( [ $uowner->{'userid'}, "logprop:$uowner->{'userid'}:$jitemid" ],
\%logprops )
if %logprops;
}
$dbh->do("UPDATE userusage SET timeupdate=NOW(), lastitemid=$jitemid WHERE userid=$ownerid")
unless $flags->{'notimeupdate'};
LJ::MemCache::set( [ $ownerid, "tu:$ownerid" ], pack( "N", time() ), 30 * 60 );
# update timeupdate_public for stats page
if ( $security eq 'public' ) {
$dbh->do("UPDATE userusage SET timeupdate_public=NOW() WHERE userid=$ownerid")
unless $flags->{'notimeupdate'};
}
# argh, this is all too ugly. need to unify more postpost stuff into async
$u->invalidate_directory_record;
# Insert the slug (try to, this will fail if this slug is already used)
my $slug = LJ::canonicalize_slug( $req->{slug} );
if ( defined $slug && length $slug > 0 ) {
$u->do( 'INSERT INTO logslugs (journalid, jitemid, slug) VALUES (?, ?, ?)',
undef, $ownerid, $jitemid, $slug );
if ( $u->err ) {
$res->{message} ||= 'Sorry, it looks like that slug has already been used. '
. 'Your entry has been posted without a slug, but you can still edit it to add a unique slug.';
}
}
# if the post was public, and the user has not opted out, try to insert into the random table;
# We're doing a REPLACE INTO because chances are the user will already
# be in there (having posted less than 7 days ago).
if ( $security eq 'public' && !$u->prop('latest_optout') ) {
$u->do(
"REPLACE INTO random_user_set (posttime, userid, journaltype) VALUES (UNIX_TIMESTAMP(), ?, ?)",
undef, $uowner->{userid}, $uowner->{journaltype}
);
}
my @jobs; # jobs to add into TaskQueue
my $entry = LJ::Entry->new( $uowner, jitemid => $jitemid, anum => $anum );
if ( $u->equals($uowner) && $req->{xpost} ne '0' && !$flags->{no_xpost} ) {
schedule_xposts( $u, $ditemid, 0, sub { ( (shift)->xpostbydefault, {} ) } );
}
# run local site-specific actions
LJ::Hooks::run_hooks(
"postpost",
{
'itemid' => $jitemid,
'anum' => $anum,
'journal' => $uowner,
'poster' => $u,
'event' => $event,
'eventtime' => $eventtime,
'subject' => $req->{'subject'},
'security' => $security,
'allowmask' => $qallowmask,
'props' => $req->{'props'},
'entry' => $entry,
'jobs' => \@jobs, # for hooks to push jobs onto
}
);
# cluster tracking
LJ::mark_user_active( $u, 'post' );
LJ::mark_user_active( $uowner, 'post' ) unless $u->equals($uowner);
DW::Stats::increment( 'dw.action.entry.post', 1,
[ 'journal_type:' . $uowner->journaltype_readable ] );
$res->{'itemid'} = $jitemid; # by request of mart
$res->{'anum'} = $anum;
$res->{'url'} = $entry->url;
# if the caller told us not to fire events (importer?) then skip the user events,
# but still fire the logging events
unless ( $flags->{nonotify} ) {
push @jobs, LJ::Event::JournalNewEntry->new($entry);
push @jobs, LJ::Event::OfficialPost->new($entry) if $uowner->is_official;
# latest posts feed update
DW::LatestFeed->new_item($entry);
}
# update the sphinx search engine
if ( @LJ::SPHINX_SEARCHD && !$importer_bypass ) {
push @jobs,
DW::Task::SphinxCopier->new(
{ userid => $uowner->id, jitemid => $jitemid, source => "entrynew" } );
}
DW::TaskQueue->dispatch(@jobs) if @jobs;
# To minimize impact on legacy code, let's make sure the entry object in
# memory has been populated with data. Easiest way to do that is to call
# one of the methods that loads the relevant row from the database.
$entry->valid;
return $res;
}
sub editevent {
my ( $req, $err, $flags ) = @_;
my $res = {};
my $deleted = 0;
un_utf8_request($req);
my $add_message = sub {
my $new_message = shift;
if ( $res->{message} ) {
$res->{message} .= "\n\n" . $new_message;
}
else {
$res->{message} = $new_message;
}
};
return undef unless authenticate( $req, $err, $flags );
# we check later that user owns entry they're modifying, so all
# we care about for check_altusage is that the target journal
# exists, and we want it to setup some data in $flags.
$flags->{'ignorecanuse'} = 1;
return undef unless check_altusage( $req, $err, $flags );
my $u = $flags->{'u'};
my $ownerid = $flags->{'ownerid'};
my $uowner = $flags->{'u_owner'} || $u;
# Make sure we have a user object here
$uowner = LJ::want_user($uowner) unless LJ::isu($uowner);
my $clusterid = $uowner->{'clusterid'};
my $posterid = $u->{'userid'};
my $qallowmask = $req->{'allowmask'} + 0;
my $sth;
my $itemid = $req->{'itemid'} + 0;
# check the journal's read-only bit
return fail( $err, 306 ) if $uowner->is_readonly;
# can't edit in deleted/suspended community
return fail( $err, 307 ) unless $uowner->is_visible || $uowner->is_readonly;
my $dbcm = LJ::get_cluster_master($uowner);
return fail( $err, 306 ) unless $dbcm;
# can't specify both a custom security and 'friends-only'
return fail( $err, 203, "Invalid friends group security set." )
if $qallowmask > 1 && $qallowmask % 2;
### make sure user can't post with "custom security" on communities
return fail( $err, 102 )
if $ownerid != $posterid && # community post
$req->{'security'} eq "usemask" && $qallowmask != 1;
## make sure user can't post with "private security" on communities they don't manage
return fail( $err, 106 )
if $ownerid != $posterid && # community post
$req->{'security'} eq "private"
&& !$u->can_manage($uowner);
# make sure the new entry's under the char limit
# NOTE: as in postevent, this requires $req->{event} to 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;
# fetch the old entry from master database so we know what we
# really have to update later. usually people just edit one part,
# not every field in every table. reads are quicker than writes,
# so this is worth it.
my $oldevent =
$dbcm->selectrow_hashref( "SELECT journalid AS 'ownerid', posterid, eventtime, logtime, "
. "compressed, security, allowmask, year, month, day, "
. "rlogtime, anum FROM log2 WHERE journalid=$ownerid AND jitemid=$itemid" );
my $ditemid = $itemid * 256 + $oldevent->{anum};
( $oldevent->{subject}, $oldevent->{event} ) = $dbcm->selectrow_array(
"SELECT subject, event FROM logtext2 " . "WHERE journalid=$ownerid AND jitemid=$itemid" );
LJ::text_uncompress( \$oldevent->{'event'} );
# use_old_content indicates the subject and entry are not changing
if ( $flags->{'use_old_content'} ) {
$req->{'event'} = $oldevent->{event};
$req->{'subject'} = $oldevent->{subject};
}
# kill seconds in eventtime, since we don't use it, then we can use 'eq' and such
$oldevent->{'eventtime'} =~ s/:00$//;
### make sure this user is allowed to edit this entry
return fail( $err, 302 )
unless ( $ownerid == $oldevent->{'ownerid'} );
### load existing meta-data
my %curprops;
LJ::load_log_props2( $dbcm, $ownerid, [$itemid], \%curprops );
# xpost helper for later
my $schedule_xposts = sub {
my $xpost_string = $curprops{$itemid}->{xpost};
if ( $xpost_string && $u->equals($uowner) && $req->{xpost} ne '0' ) {
my $xpost_info = DW::External::Account->xpost_string_to_hash($xpost_string);
schedule_xposts( $u, $ditemid, $deleted,
sub { ( $xpost_info->{ (shift)->acctid }, {} ) } );
}
};
### what can they do to somebody elses entry? (in shared journal)
### can edit it if they own or maintain the journal, but not if the journal is read-only
if ( $posterid != $oldevent->{'posterid'} || $u->is_readonly || $uowner->is_readonly ) {
## deleting.
return fail( $err, 304 )
if $req->{'event'} !~ /\S/ && !$u->can_manage($uowner);
## editing:
if ( $req->{'event'} =~ /\S/ ) {
return fail( $err, 303 ) if $posterid != $oldevent->{'posterid'};
return fail( $err, 318 ) if $u->is_readonly;
return fail( $err, 319 ) if $uowner->is_readonly;
}
}
# simple logic for deleting an entry
if ( !$flags->{'use_old_content'} && $req->{'event'} !~ /\S/ ) {
$deleted = 1;
# if their newesteventtime prop equals the time of the one they're deleting
# then delete their newesteventtime.
if ( $u->equals($uowner) ) {
$u->preload_props( { use_master => 1 }, "newesteventtime" );
if ( $u->{'newesteventtime'} eq $oldevent->{'eventtime'} ) {
$u->set_prop( "newesteventtime", undef );
}
}
# log this event, unless noauth is on, which means it is being done internally and we should
# rely on them to log why they're deleting the entry if they need to. that way we don't have
# double entries, and we have as much information available as possible at the location the
# delete is initiated.
$uowner->log_event(
'delete_entry',
{
remote => $u,
actiontarget => $ditemid,
method => 'protocol',
}
) unless $flags->{noauth};
LJ::delete_entry( $uowner, $req->{'itemid'}, 'quick', $oldevent->{'anum'} );
# clear their duplicate protection, so they can later repost
# what they just deleted. (or something... probably rare.)
$u->set_prop( "dupsig_post", undef );
$uowner->clear_daycounts( $qallowmask || $req->{security} );
# pass the delete
$schedule_xposts->();
$res = { itemid => $itemid, anum => $oldevent->{anum} };
return $res;
}
# now make sure the new entry text isn't $CannotBeShown
return fail( $err, 210 )
if $req->{event} eq $CannotBeShown;
# don't allow backdated posts in communities... unless this is an import
if ( $req->{props}->{opt_backdated} && $uowner->is_community ) {
return fail( $err, 152 )
unless $curprops{$itemid}->{import_source};
}
# make year/mon/day/hour/min optional in an edit event,
# and just inherit their old values
{
$oldevent->{'eventtime'} =~ /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d)/;
$req->{'year'} = $1 unless defined $req->{'year'};
$req->{'mon'} = $2 + 0 unless defined $req->{'mon'};
$req->{'day'} = $3 + 0 unless defined $req->{'day'};
$req->{'hour'} = $4 + 0 unless defined $req->{'hour'};
$req->{'min'} = $5 + 0 unless defined $req->{'min'};
}
# updating an entry:
return undef
unless common_event_validation( $req, $err, $flags );
# now we can move over to picture_mapid instead of picture_keyword if appropriate
if ( $req->{props} && defined $req->{props}->{picture_keyword} && $u->userpic_have_mapid ) {
$req->{props}->{picture_mapid} = '';
$req->{props}->{picture_mapid} =
$u->get_mapid_from_keyword( $req->{props}->{picture_keyword},
create => $flags->{create_unknown_picture_mapid} || 0 )
if defined $req->{props}->{picture_keyword};
delete $req->{props}->{picture_keyword};
}
## handle meta-data (properties)
# FIXME: Hey... I think this just throws the changed props away???? -NF
my %props_byname = ();
foreach my $key ( keys %{ $req->{'props'} } ) {
## changing to something else?
if ( $curprops{$itemid}->{$key} ne $req->{'props'}->{$key} ) {
$props_byname{$key} = $req->{'props'}->{$key};
}
}
# additionally, if the 'opt_nocomments_maintainer' prop was set before and the poster now sets
# 'opt_nocomments' to 0 again, 'opt_nocomments_maintainer' should be set to 0 again, as well
# so comments are enabled again
$req->{props}->{opt_nocomments_maintainer} = 0
if defined $req->{props}->{opt_nocomments} && !$req->{props}->{opt_nocomments};
my $event = $req->{'event'};
my $owneru = LJ::load_userid($ownerid);
$event = LJ::EmbedModule->transform_rte_post($event);
LJ::EmbedModule->parse_module_embed( $owneru, \$event );
my $bytes = length($event) + length( $req->{'subject'} );
my $eventtime =
sprintf( "%04d-%02d-%02d %02d:%02d", map { $req->{$_} } qw(year mon day hour min) );
my $qeventtime = $dbcm->quote($eventtime);
# preserve old security by default, use user supplied if it's understood
my $security = $oldevent->{security};
$security = $req->{security}
if $req->{security}
&& $req->{security} =~ /^(?:public|private|usemask)$/;
my $do_tags = $req->{props} && defined $req->{props}->{taglist};
my $do_tags_security;
my $entry_tags;
if ( $oldevent->{security} ne $security || $qallowmask != $oldevent->{allowmask} ) {
# FIXME: this is a hopefully temporary hack which deletes tags from the entry
# when the security has changed. the real fix is to make update_logtags aware
# of security changes so it can update logkwsum appropriately.
# we need to fix security on this entry's tags; if the user didn't give us a
# tag list to work with, we use the existing tags on this entry
unless ($do_tags) {
$entry_tags = LJ::Tags::get_logtags( $uowner, $itemid );
$entry_tags = $entry_tags->{$itemid};
$entry_tags = join( ',', sort values %{ $entry_tags || {} } );
$req->{props}->{taglist} = $entry_tags;
}
# FIXME: temporary hack until we can make update_logtags recognize entry security edits
if ( LJ::Tags::can_control_tags( $uowner, $u ) || LJ::Tags::can_add_tags( $uowner, $u ) ) {
my $delete = LJ::Tags::delete_logtags( $uowner, $itemid );
$do_tags_security = 1;
}
}
my $qyear = $req->{'year'} + 0;
my $qmonth = $req->{'mon'} + 0;
my $qday = $req->{'day'} + 0;
if ( $eventtime ne $oldevent->{'eventtime'}
|| $security ne $oldevent->{'security'}
|| ( !$curprops{$itemid}->{opt_backdated} && $req->{props}{opt_backdated} )
|| $qallowmask != $oldevent->{'allowmask'} )
{
# are they changing their most recent post?
if ( $u->equals($uowner)
&& $u->prop("newesteventtime") eq $oldevent->{eventtime} )
{
if ( !$curprops{$itemid}->{opt_backdated} && $req->{props}{opt_backdated} ) {
# if they set the backdated flag, then we no longer know
# the newesteventtime.
$u->set_prop( "newesteventtime", undef );
}
elsif ( $eventtime ne $oldevent->{eventtime} ) {
# otherwise, if they changed time on this event,
# the newesteventtime is this event's new time.
$u->set_prop( "newesteventtime", $eventtime );
}
}
my $qsecurity = $uowner->quote($security);
my $dberr;
$uowner->log2_do( \$dberr,
"UPDATE log2 SET eventtime=$qeventtime, revttime=$LJ::EndOfTime-"
. "UNIX_TIMESTAMP($qeventtime), year=$qyear, month=$qmonth, day=$qday, "
. "security=$qsecurity, allowmask=$qallowmask WHERE journalid=$ownerid "
. "AND jitemid=$itemid" );
return fail( $err, 501, $dberr ) if $dberr;
# update memcached
my $sec = $qallowmask;
$sec = 0 if $security eq 'private';
$sec = $LJ::PUBLICBIT if $security eq 'public';
my $row = pack( $LJ::LOGMEMCFMT,
$oldevent->{'posterid'},
LJ::mysqldate_to_time( $eventtime, 1 ),
LJ::mysqldate_to_time( $oldevent->{'logtime'}, 1 ),
$sec, $ditemid );
LJ::MemCache::set( [ $ownerid, "log2:$ownerid:$itemid" ], $row );
}
if ( $security ne $oldevent->{'security'}
|| $qallowmask != $oldevent->{'allowmask'} )
{
if ( $security eq "public" || $security eq "private" ) {
$uowner->do("DELETE FROM logsec2 WHERE journalid=$ownerid AND jitemid=$itemid");
}
else {
$uowner->do( "REPLACE INTO logsec2 (journalid, jitemid, allowmask) "
. "VALUES ($ownerid, $itemid, $qallowmask)" );
}
return fail( $err, 501, $dbcm->errstr ) if $uowner->err;
}
LJ::MemCache::set( [ $ownerid, "logtext:$clusterid:$ownerid:$itemid" ],
[ $req->{'subject'}, $event ] );
if (
!$flags->{'use_old_content'}
&& ( $event ne $oldevent->{'event'}
|| $req->{'subject'} ne $oldevent->{'subject'} )
)
{
$uowner->do(
"UPDATE logtext2 SET subject=?, event=? "
. "WHERE journalid=$ownerid AND jitemid=$itemid",
undef, $req->{'subject'}, LJ::text_compress($event)
);
return fail( $err, 501, $uowner->errstr ) if $uowner->err;
# update disk usage
$uowner->dudata_set( 'L', $itemid, $bytes );
}
my $clean_event = $event;
my $errref;
# TODO: get editor prop from the new props (or current, if unchanged) and
# thread it through to the cleaner.
my $editor = undef;
LJ::CleanHTML::clean_event( \$clean_event, { errref => \$errref, editor => $editor } );
$add_message->(
translate(
$u, $errref,
{
aopts => "href='$LJ::SITEROOT/editjournal?journal="
. $uowner->user
. "&itemid=$ditemid'"
}
)
) if $errref;
# up the revision number
$req->{'props'}->{'revnum'} = ( $curprops{$itemid}->{'revnum'} || 0 ) + 1;
$req->{'props'}->{'revtime'} = time();
if ($do_tags) {
# we only want to update the tags if they've been modified
# so load the original entry tags
unless ($entry_tags) {
$entry_tags = LJ::Tags::get_logtags( $uowner, $itemid );
$entry_tags = $entry_tags->{$itemid};
$entry_tags = join( ',', sort values %{ $entry_tags || {} } );
}
my $request_tags = [];
LJ::Tags::is_valid_tagstring( $req->{props}->{taglist}, $request_tags );
$request_tags = join( ",", sort @{ $request_tags || [] } );
$do_tags = ( $request_tags ne $entry_tags );
}
# handle tags if they're defined
if ( $do_tags || $do_tags_security ) {
my $tagerr = "";
my $rv = LJ::Tags::update_logtags(
$uowner, $itemid,
{
set_string => $req->{props}->{taglist},
remote => $u,
err_ref => \$tagerr,
}
);
# we only want to warn if we tried to edit the tags, not if we just tried to edit the security
$add_message->($tagerr) if $tagerr && $do_tags;
}
# handle the props
{
my $propset = {};
foreach my $pname ( keys %{ $req->{'props'} } ) {
my $p = LJ::get_prop( "log", $pname );
next unless $p;
$propset->{$pname} = $req->{'props'}->{$pname};
}
LJ::set_logprop( $uowner, $itemid, $propset );
}
# deal with backdated changes. if the entry's rlogtime is
# $EndOfTime, then it's backdated. if they want that off, need to
# reset rlogtime to real reverse log time. also need to set
# rlogtime to $EndOfTime if they're turning backdate on.
if ( $req->{'props'}->{'opt_backdated'} eq "1"
&& $oldevent->{'rlogtime'} != $LJ::EndOfTime )
{
my $dberr;
$uowner->log2_do( undef,
"UPDATE log2 SET rlogtime=$LJ::EndOfTime WHERE "
. "journalid=$ownerid AND jitemid=$itemid" );
return fail( $err, 501, $dberr ) if $dberr;
}
if ( $req->{'props'}->{'opt_backdated'} eq "0"
&& $oldevent->{'rlogtime'} == $LJ::EndOfTime )
{
my $dberr;
$uowner->log2_do( \$dberr,
"UPDATE log2 SET rlogtime=$LJ::EndOfTime-UNIX_TIMESTAMP(logtime) "
. "WHERE journalid=$ownerid AND jitemid=$itemid" );
return fail( $err, 501, $dberr ) if $dberr;
}
return fail( $err, 501, $dbcm->errstr ) if $dbcm->err;
$uowner->clear_daycounts( $oldevent->{allowmask} + 0 || $oldevent->{security},
$qallowmask || $security );
# Update the slug (try to, this will fail if this slug is already used). To
# delete or change the slug, you must pass this parameter in. If it is not
# present, we leave the slug alone.
if ( exists $req->{slug} ) {
LJ::MemCache::delete( [ $ownerid, "logslug:$ownerid:$itemid" ] );
$u->do( 'DELETE FROM logslugs WHERE journalid = ? AND jitemid = ?',
undef, $ownerid, $itemid );
my $slug = LJ::canonicalize_slug( $req->{slug} );
if ( defined $slug && length $slug > 0 ) {
$u->do( 'INSERT INTO logslugs (journalid, jitemid, slug) VALUES (?, ?, ?)',
undef, $ownerid, $itemid, $slug );
if ( $u->err ) {
$add_message->( 'Sorry, it looks like that slug has already been used. '
. 'Your entry has been updated, but you can still edit it again to add a unique slug.'
);
}
}
}
my $entry = LJ::Entry->new( $ownerid, jitemid => $itemid );
$res->{itemid} = $itemid;
if ( defined $oldevent->{'anum'} ) {
$res->{'anum'} = $oldevent->{'anum'};
$res->{'url'} = $entry->url;
}
DW::Stats::increment( 'dw.action.entry.edit', 1,
[ 'journal_type:' . $uowner->journaltype_readable ] );
# fired to copy the post over to the Sphinx search database
my @jobs;
if (@LJ::SPHINX_SEARCHD) {
push @jobs,
DW::Task::SphinxCopier->new(
{ userid => $ownerid, jitemid => $itemid, source => "entryedt" } );
}
LJ::Hooks::run_hooks( "editpost", $entry, \@jobs );
DW::TaskQueue->dispatch(@jobs) if @jobs;
# ensure our xposted edit fires
$schedule_xposts->();
return $res;
}
sub getevents {
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;
### shared-journal support
my $posterid = $u->{'userid'};
my $ownerid = $flags->{'ownerid'};
my $dbr = LJ::get_db_reader();
my $sth;
my $dbcr = LJ::get_cluster_reader($uowner);
return fail( $err, 502 ) unless $dbcr && $dbr;
# can't pull events from deleted/suspended journal
return fail( $err, 307 ) unless $uowner->is_visible || $uowner->is_readonly;
my $reject_code = $LJ::DISABLE_PROTOCOL{getevents};
if ( ref $reject_code eq "CODE" ) {
my $apache_r = eval { BML::get_request() };
my $errmsg = $reject_code->( $req, $flags, $apache_r );
if ($errmsg) { return fail( $err, "311", $errmsg ); }
}
# if this is on, we sort things different (logtime vs. posttime)
# to avoid timezone issues
my $is_community = $uowner->is_community;
# in some cases we'll use the master, to ensure there's no
# replication delay. useful cases: getting one item, use master
# since user might have just made a typo and realizes it as they
# post, or wants to append something they forgot, etc, etc. in
# other cases, slave is pretty sure to have it.
my $use_master = 0;
# the benefit of this mode over actually doing 'lastn/1' is
# the $use_master usage.
if ( $req->{'selecttype'} eq "one" && $req->{'itemid'} eq "-1" ) {
$req->{'selecttype'} = "lastn";
$req->{'howmany'} = 1;
undef $req->{'itemid'};
$use_master = 1; # see note above.
}
# build the query to get log rows. each selecttype branch is
# responsible for either populating the following 3 variables
# OR just populating $sql
my ( $orderby, $where, $limit );
my $sql;
if ( $req->{'selecttype'} eq "day" ) {
return fail( $err, 203 )
unless ( $req->{'year'} =~ /^\d\d\d\d$/
&& $req->{'month'} =~ /^\d\d?$/
&& $req->{'day'} =~ /^\d\d?$/
&& $req->{'month'} >= 1
&& $req->{'month'} <= 12
&& $req->{'day'} >= 1
&& $req->{'day'} <= 31 );
my $qyear = $dbr->quote( $req->{'year'} );
my $qmonth = $dbr->quote( $req->{'month'} );
my $qday = $dbr->quote( $req->{'day'} );
$where = "AND year=$qyear AND month=$qmonth AND day=$qday";
$limit = "LIMIT 200"; # FIXME: unhardcode this constant (also in ljviews.pl)
# see note above about why the sort order is different
$orderby = $is_community ? "ORDER BY logtime" : "ORDER BY eventtime";
}
elsif ( $req->{'selecttype'} eq "lastn" ) {
my $howmany = $req->{'howmany'} || 20;
if ( $howmany > 50 ) { $howmany = 50; }
$howmany = $howmany + 0;
$limit = "LIMIT $howmany";
# okay, follow me here... see how we add the revttime predicate
# even if no beforedate key is present? you're probably saying,
# what, huh? -- you're saying: "revttime > 0", that's like
# saying, "if entry occurred at all." yes yes, but that hints
# mysql's optimizer to use the right index.
my $rtime_after = 0;
my $rtime_what = $is_community ? "rlogtime" : "revttime";
if ( $req->{'beforedate'} ) {
return fail( $err, 203, "Invalid beforedate format." )
unless ( $req->{'beforedate'} =~ /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/ );
my $qd = $dbr->quote( $req->{'beforedate'} );
$rtime_after = "$LJ::EndOfTime-UNIX_TIMESTAMP($qd)";
}
$where .= "AND $rtime_what > $rtime_after ";
$orderby = "ORDER BY $rtime_what";
}
elsif ( $req->{'selecttype'} eq "one" ) {
my $id = $req->{'itemid'} + 0;
$where = "AND jitemid=$id";
}
elsif ( $req->{'selecttype'} eq "syncitems" ) {
return fail( $err, 506 ) unless LJ::is_enabled('syncitems');
my $date = $req->{'lastsync'} || "0000-00-00 00:00:00";
return fail( $err, 203, "Invalid syncitems date format" )
unless ( $date =~ /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/ );
my $now = time();
# broken client loop prevention
if ( $req->{'lastsync'} ) {
my $pname = "rl_syncitems_getevents_loop";
# format is: time/date/time/date/time/date/... so split
# it into a hash, then delete pairs that are older than an hour
my %reqs = split( m!/!, $u->prop($pname) );
foreach ( grep { $_ < $now - 60 * 60 } keys %reqs ) {
delete $reqs{$_};
}
my $count = grep { $_ eq $date } values %reqs;
$reqs{$now} = $date;
if ( $count >= 2 ) {
# 2 prior, plus this one = 3 repeated requests for same synctime.
# their client is busted. (doesn't understand syncitems semantics)
return fail( $err, 406 );
}
$u->set_prop(
$pname,
join(
'/', map { $_, $reqs{$_} }
sort { $b <=> $a } keys %reqs
)
);
}
my %item;
$sth = $dbcr->prepare(
"SELECT jitemid, logtime FROM log2 WHERE " . "journalid=? and logtime > ?" );
$sth->execute( $ownerid, $date );
while ( my ( $id, $dt ) = $sth->fetchrow_array ) {
$item{$id} = $dt;
}
my $p_revtime = LJ::get_prop( "log", "revtime" );
$sth =
$dbcr->prepare( "SELECT jitemid, FROM_UNIXTIME(value) "
. "FROM logprop2 WHERE journalid=? "
. "AND propid=$p_revtime->{'id'} "
. "AND value+0 > UNIX_TIMESTAMP(?)" );
$sth->execute( $ownerid, $date );
while ( my ( $id, $dt ) = $sth->fetchrow_array ) {
$item{$id} = $dt;
}
my $limit = 100;
my @ids = sort { $item{$a} cmp $item{$b} } keys %item;
if ( @ids > $limit ) { @ids = @ids[ 0 .. $limit - 1 ]; }
my $in = join( ',', @ids ) || "0";
$where = "AND jitemid IN ($in)";
}
elsif ( $req->{'selecttype'} eq "multiple" ) {
my @ids;
foreach my $num ( split( /\s*,\s*/, $req->{'itemids'} ) ) {
return fail( $err, 203, "Non-numeric itemid" ) unless $num =~ /^\d+$/;
push @ids, $num;
}
my $limit = 100;
return fail( $err, 209, "Can't retrieve more than $limit entries at once" )
if @ids > $limit;
my $in = join( ',', @ids );
$where = "AND jitemid IN ($in)";
}
else {
return fail( $err, 200, "Invalid selecttype." );
}
my $mask = 0;
if ( $u && ( $u->is_person || $u->is_identity ) && $posterid != $ownerid ) {
# if this is a community we're viewing, fake the mask to select on, as communities
# no longer have masks to users
if ( $uowner->is_community ) {
$mask = $u->member_of($uowner) ? 1 : 0;
}
else {
$mask = $uowner->trustmask($u);
}
}
# check security!
my $secwhere;
if ( $u && $u->can_manage($uowner) ) {
# journal owners and community admins can see everything
$secwhere = "";
}
elsif ($mask) {
# can see public or things with them in the mask
$secwhere = "AND (security='public' OR (security='usemask' AND allowmask & $mask != 0))";
}
else {
# not on access list or a member; only see public.
$secwhere = "AND security='public'";
}
# common SQL template:
unless ($sql) {
$sql = "SELECT jitemid, eventtime, logtime, security, allowmask, anum, posterid "
. "FROM log2 WHERE journalid=$ownerid $where $secwhere $orderby $limit";
}
# whatever selecttype might have wanted us to use the master db.
$dbcr = LJ::get_cluster_def_reader($uowner) if $use_master;
return fail( $err, 502 ) unless $dbcr;
## load the log rows
( $sth = $dbcr->prepare($sql) )->execute;
return fail( $err, 501, $dbcr->errstr ) if $dbcr->err;
my $count = 0;
my @itemids = ();
my $res = {};
my $events = $res->{events} = [];
my %evt_from_itemid;
while ( my ( $itemid, $eventtime, $logtime, $sec, $mask, $anum, $jposterid ) =
$sth->fetchrow_array )
{
$count++;
my $evt = {};
$evt->{itemid} = $itemid;
push @itemids, $itemid;
$evt_from_itemid{$itemid} = $evt;
$evt->{eventtime} = $eventtime;
$evt->{logtime} = $logtime;
if ( $sec ne "public" ) {
$evt->{security} = $sec;
$evt->{allowmask} = $mask if $sec eq "usemask";
}
$evt->{anum} = $anum;
$evt->{poster} = LJ::get_username($jposterid)
if $jposterid != $ownerid;
$evt->{url} = LJ::item_link( $uowner, $itemid, $anum );
push @$events, $evt;
}
# load properties. Even if the caller doesn't want them, we need
# them in Unicode installations to recognize older 8bit non-UTF-8
# entries.
{
### do the properties now
$count = 0;
my %props = ();
LJ::load_log_props2( $dbcr, $ownerid, \@itemids, \%props );
# load the tags for these entries, unless told not to
unless ( $req->{notags} ) {
# construct %idsbycluster for the multi call to get these tags
my $tags = LJ::Tags::get_logtags( $uowner, \@itemids );
# add to props
foreach my $itemid (@itemids) {
next unless $tags->{$itemid};
$props{$itemid}->{taglist} = join( ', ', values %{ $tags->{$itemid} } );
}
}
foreach my $itemid ( keys %props ) {
# 'replycount' is a pseudo-prop, don't send it.
# FIXME: this goes away after we restructure APIs and
# replycounts cease being transferred in props
delete $props{$itemid}->{'replycount'};
# the xpost property is not something we should be distributing
# as it's a serialized string and confuses clients
delete $props{$itemid}->{xpost};
my $evt = $evt_from_itemid{$itemid};
$evt->{'props'} = {};
foreach my $name ( keys %{ $props{$itemid} } ) {
my $value = $props{$itemid}->{$name};
$value =~ s/\n/ /g;
$evt->{'props'}->{$name} = $value;
}
}
}
## load the text
my $text = LJ::DB::cond_no_cache(
$use_master,
sub {
return LJ::get_logtext2( $uowner, @itemids );
}
);
foreach my $i (@itemids) {
my $t = $text->{$i};
my $evt = $evt_from_itemid{$i};
# if they want subjects to be events, replace event
# with subject when requested.
if ( $req->{prefersubject} && length( $t->[0] ) ) {
$t->[1] = $t->[0]; # event = subject
$t->[0] = undef; # subject = undef
}
# re-generate the picture_keyword prop for the returned data, as a mapid will mean nothing
my $pu = $uowner;
$pu = LJ::load_user( $evt->{poster} ) if $evt->{poster};
$evt->{props}->{picture_keyword} =
$pu->get_keyword_from_mapid( $evt->{props}->{picture_mapid} )
if $pu->userpic_have_mapid;
# now that we have the subject, the event and the props,
# auto-translate them to UTF-8 if they're not in UTF-8.
if ( $req->{ver} >= 1 && $evt->{props}->{unknown8bit} ) {
LJ::item_toutf8( $uowner, \$t->[0], \$t->[1], $evt->{props} );
$evt->{converted_with_loss} = 1;
}
if ( $req->{'ver'} < 1 && !$evt->{'props'}->{'unknown8bit'} ) {
unless ( LJ::is_ascii( $t->[0] )
&& LJ::is_ascii( $t->[1] )
&& LJ::is_ascii( join( ' ', values %{ $evt->{'props'} } ) ) )
{
# we want to fail the client that wants to get this entry
# but we make an exception for selecttype=day, in order to allow at least
# viewing the daily summary
if ( $req->{'selecttype'} eq 'day' ) {
$t->[0] = $t->[1] = $CannotBeShown;
}
else {
return fail( $err, 207,
"Cannot display/edit a Unicode post with a non-Unicode client. Please see $LJ::SITEROOT/support/encodings for more information."
);
}
}
}
if ( $t->[0] ) {
$t->[0] =~ s/[\r\n]/ /g;
$evt->{'subject'} = $t->[0];
}
# truncate
if ( $req->{'truncate'} >= 4 ) {
my $original = $t->[1];
if ( $req->{'ver'} > 1 ) {
$t->[1] = LJ::text_trim( $t->[1], $req->{'truncate'} - 3, 0 );
}
else {
$t->[1] = LJ::text_trim( $t->[1], 0, $req->{'truncate'} - 3 );
}
# only append the elipsis if the text was actually truncated
$t->[1] .= "..." if $t->[1] ne $original;
}
# line endings
$t->[1] =~ s/\r//g;
if ( $req->{'lineendings'} eq "unix" ) {
# do nothing. native format.
}
elsif ( $req->{'lineendings'} eq "mac" ) {
$t->[1] =~ s/\n/\r/g;
}
elsif ( $req->{'lineendings'} eq "space" ) {
$t->[1] =~ s/\n/ /g;
}
elsif ( $req->{'lineendings'} eq "dots" ) {
$t->[1] =~ s/\n/ ... /g;
}
else { # "pc" -- default
$t->[1] =~ s/\n/\r\n/g;
}
$evt->{'event'} = $t->[1];
}
# maybe we don't need the props after all
if ( $req->{'noprops'} ) {
foreach (@$events) { delete $_->{'props'}; }
}
return $res;
}
# deprecated
sub editfriends {
return fail( $_[1], 504 );
}
# deprecated
sub editfriendgroups {
return fail( $_[1], 504 );
}
sub editcircle {
my ( $req, $err, $flags ) = @_;
return undef unless authenticate( $req, $err, $flags );
my $u = $flags->{u};
my $res = {};
if ( ref $req->{settrustgroups} eq 'HASH' ) {
while ( my ( $bit, $group ) = each %{ $req->{settrustgroups} } ) {
my $name = $group->{name};
my $sortorder = $group->{sort};
my $public = $group->{public};
my %params = (
id => $bit,
groupname => $name,
_force_create => 1
);
$params{sortorder} = $sortorder if defined $sortorder;
$params{is_public} = $public if defined $public;
$u->edit_trust_group(%params);
}
}
if ( ref $req->{deletetrustgroups} eq 'ARRAY' ) {
foreach my $bit ( @{ $req->{deletetrustgroups} } ) {
$u->delete_trust_group( id => $bit );
}
}
if ( ref $req->{setcontentfilters} eq 'HASH' ) {
while ( my ( $bit, $group ) = each %{ $req->{setcontentfilters} } ) {
my $name = $group->{name};
my $public = $group->{public};
my $sortorder = $group->{sort};
my $cf = $u->content_filters( id => $bit );
if ($cf) {
$cf->name($name)
if $name && $name ne $cf->name;
$cf->public($public)
if ( defined $public ) && $public ne $cf->public;
$cf->sortorder($sortorder)
if ( defined $sortorder ) && $sortorder ne $cf->sortorder;
}
else {
my $fid = $u->create_content_filter(
name => $name,
public => $public,
sortorder => $sortorder
);
my $added = {
id => $fid,
name => $name,
};
push @{ $res->{addedcontentfilters} }, $added;
}
}
}
if ( ref $req->{deletecontentfilters} eq 'ARRAY' ) {
foreach my $bit ( @{ $req->{deletecontentfilters} } ) {
$u->delete_content_filter( id => $bit );
}
}
if ( ref $req->{add} eq 'ARRAY' ) {
foreach my $row ( @{ $req->{add} } ) {
my $other_user = LJ::load_user( $row->{username} );
return fail( $err, 203 ) unless $other_user;
my $other_userid = $other_user->{userid};
if ( defined( $row->{groupmask} ) ) {
$u->add_edge(
$other_userid,
trust => {
mask => $row->{groupmask},
nonotify => 1,
}
);
}
else {
if ( $row->{edge} & 1 ) {
$u->add_edge(
$other_userid,
trust => {
nonotify => $u->trusts($other_userid) ? 1 : 0,
}
);
}
else {
$u->remove_edge(
$other_userid,
trust => {
nonotify => $u->trusts($other_userid) ? 0 : 1,
}
);
}
if ( $row->{edge} & 2 ) {
my $fg = $row->{fgcolor} || "#000000";
my $bg = $row->{bgcolor} || "#FFFFFF";
$u->add_edge(
$other_userid,
watch => {
fgcolor => LJ::color_todb($fg),
bgcolor => LJ::color_todb($bg),
nonotify => $u->watches($other_userid) ? 1 : 0,
}
);
}
else {
$u->remove_edge(
$other_userid,
watch => {
nonotify => $u->watches($other_userid) ? 0 : 1,
}
);
}
if ( $row->{edge} ) {
my $myid = $u->userid;
my $added = {
username => $other_user->{user},
fullname => $other_user->{name},
trusted => $u->trusts($other_userid),
trustedby => $other_user->trusts($myid),
watched => $u->watches($other_userid),
watchedby => $other_user->watches($myid)
};
push @{ $res->{added} }, $added;
}
}
}
}
# if ( ref $req->{delete} eq 'ARRAY' ) {
# foreach my $row ( @{$req->{delete}} ) {
# not implemented yet - maybe unnecessary
# }
# }
if ( ref $req->{addtocontentfilters} eq 'ARRAY' ) {
foreach my $row ( @{ $req->{addtocontentfilters} } ) {
my $other_user = LJ::load_user( $row->{username} );
return fail( $err, 203 ) unless $other_user;
my $other_userid = $other_user->{userid};
my $cf = $u->content_filters( id => $row->{id} );
$cf->add_row( userid => $other_userid ) if $cf;
}
}
if ( ref $req->{deletefromcontentfilters} eq 'ARRAY' ) {
foreach my $row ( @{ $req->{deletefromcontentfilters} } ) {
my $other_user = LJ::load_user( $row->{username} );
return fail( $err, 203 ) unless $other_user;
my $other_userid = $other_user->{userid};
my $cf = $u->content_filters( id => $row->{id} );
$cf->delete_row($other_userid) if $cf;
}
}
return $res;
}
sub sessionexpire {
my ( $req, $err, $flags ) = @_;
return undef unless authenticate( $req, $err, $flags );
my $u = $flags->{u};
# expunge one? or all?
if ( $req->{expireall} ) {
$u->kill_all_sessions;
return {};
}
# just expire a list
my $list = $req->{expire} || [];
return {} unless @$list;
return fail( $err, 502 ) unless $u->writer;
$u->kill_sessions(@$list);
return {};
}
sub sessiongenerate {
# generate a session
my ( $req, $err, $flags ) = @_;
return undef unless authenticate( $req, $err, $flags );
# sanitize input
$req->{expiration} = 'short' unless $req->{expiration} eq 'long';
my $boundip;
$boundip = LJ::get_remote_ip() if $req->{bindtoip};
my $u = $flags->{u};
my $sess_opts = {
exptype => $req->{expiration},
ipfixed => $boundip,
};
# do not let locked people do this
return fail( $err, 308 ) if $u->is_locked;
my $sess = LJ::Session->create( $u, %$sess_opts );
# return our hash
return { ljsession => $sess->master_cookie_string, };
}
sub list_friends {
my ( $u, $opts ) = @_;
# do not show people in here
my %hide; # userid -> 1
# TAG:FR:protocol:list_friends
my $sql;
unless ( $opts->{'friendof'} ) {
$sql = "SELECT friendid, fgcolor, bgcolor, groupmask FROM friends WHERE userid=?";
}
else {
$sql = "SELECT userid FROM friends WHERE friendid=?";
if ( my $list = LJ::load_rel_user( $u, 'B' ) ) {
$hide{$_} = 1 foreach @$list;
}
}
my $dbr = LJ::get_db_reader();
my $sth = $dbr->prepare($sql);
$sth->execute( $u->{'userid'} );
my @frow;
while ( my @row = $sth->fetchrow_array ) {
next if $hide{ $row[0] };
push @frow, [@row];
}
my $us = LJ::load_userids( map { $_->[0] } @frow );
my $limitnum = $opts->{'limit'} + 0;
my $res = [];
foreach my $f (
sort { $us->{ $a->[0] }{'user'} cmp $us->{ $b->[0] }{'user'} }
grep { $us->{ $_->[0] } } @frow
)
{
my $u = $us->{ $f->[0] };
next if $opts->{'friendof'} && !$u->is_visible;
my $r = {
'username' => $u->{'user'},
'fullname' => $u->{'name'},
};
if ( $u->identity ) {
my $i = $u->identity;
$r->{'identity_type'} = $i->pretty_type;
$r->{'identity_value'} = $i->value;
$r->{'identity_display'} = $u->display_name;
}
if ( $opts->{'includebdays'}
&& $u->{'bdate'}
&& $u->{'bdate'} ne "0000-00-00"
&& $u->can_show_full_bday )
{
$r->{'birthday'} = $u->{'bdate'};
}
unless ( $opts->{'friendof'} ) {
$r->{'fgcolor'} = LJ::color_fromdb( $f->[1] );
$r->{'bgcolor'} = LJ::color_fromdb( $f->[2] );
$r->{"groupmask"} = $f->[3] if $f->[3] != 1;
}
else {
$r->{'fgcolor'} = "#000000";
$r->{'bgcolor'} = "#ffffff";
}
$r->{"type"} = {
'C' => 'community',
'Y' => 'syndicated',
'I' => 'identity',
}->{ $u->journaltype }
unless $u->is_person;
$r->{"status"} = {
'D' => "deleted",
'S' => "suspended",
'X' => "purged",
}->{ $u->statusvis }
unless $u->is_visible;
push @$res, $r;
# won't happen for zero limit (which means no limit)
last if @$res == $limitnum;
}
return $res;
}
sub list_users {
my ( $u, %opts ) = @_;
my %hide;
my $list = LJ::load_rel_user( $u, 'B' );
$hide{$_} = 1 foreach @{ $list || [] };
my $friendof = $opts{trustedby} || $opts{watchedby};
my ( $filter, @userids );
if ($friendof) {
@userids = $opts{trustedby} ? $u->trusted_by_userids : $u->watched_by_userids;
}
else {
$filter = $opts{trusted} ? $u->trust_list : $u->watch_list;
@userids = keys %{$filter};
}
my $limitnum = $opts{limit} + 0;
my @res;
my $us = LJ::load_userids(@userids);
while ( my ( $userid, $u ) = each %$us ) {
next unless LJ::isu($u);
next if $friendof && !$u->is_visible;
next if $hide{$userid};
my $r = {
username => $u->user,
fullname => $u->display_name
};
if ( $u->identity ) {
my $i = $u->identity;
$r->{identity_type} = $i->pretty_type;
$r->{identity_value} = $i->value;
$r->{identity_display} = $u->display_name;
}
if ( $opts{includebdays} ) {
$r->{birthday} = $u->bday_string;
}
unless ($friendof) {
$r->{fgcolor} = LJ::color_fromdb( $filter->{$userid}->{fgcolor} );
$r->{bgcolor} = LJ::color_fromdb( $filter->{$userid}->{bgcolor} );
$r->{groupmask} = $filter->{$userid}->{groupmask};
}
$r->{type} = {
C => 'community',
Y => 'syndicated',
I => 'identity',
}->{ $u->journaltype }
unless $u->is_person;
$r->{status} = {
D => 'deleted',
S => 'suspended',
X => 'purged',
}->{ $u->statusvis }
unless $u->is_visible;
push @res, $r;
# won't happen for zero limit (which means no limit)
last if scalar @res == $limitnum;
}
return \@res;
}
sub syncitems {
my ( $req, $err, $flags ) = @_;
return undef unless authenticate( $req, $err, $flags );
return undef unless check_altusage( $req, $err, $flags );
return fail( $err, 506 ) unless LJ::is_enabled('syncitems');
my $ownerid = $flags->{'ownerid'};
my $uowner = $flags->{'u_owner'} || $flags->{'u'};
my $sth;
my $db = LJ::get_cluster_reader($uowner);
return fail( $err, 502 ) unless $db;
## have a valid date?
my $date = $req->{'lastsync'};
if ($date) {
return fail( $err, 203, "Invalid date format" )
unless ( $date =~ /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/ );
}
else {
$date = "0000-00-00 00:00:00";
}
my $LIMIT = 500;
my %item;
$sth =
$db->prepare( "SELECT jitemid, logtime FROM log2 WHERE " . "journalid=? and logtime > ?" );
$sth->execute( $ownerid, $date );
while ( my ( $id, $dt ) = $sth->fetchrow_array ) {
$item{$id} = [ 'L', $id, $dt, "create" ];
}
my %cmt;
my $p_calter = LJ::get_prop( "log", "commentalter" );
my $p_revtime = LJ::get_prop( "log", "revtime" );
$sth =
$db->prepare( "SELECT jitemid, propid, FROM_UNIXTIME(value) "
. "FROM logprop2 WHERE journalid=? "
. "AND propid IN ($p_calter->{'id'}, $p_revtime->{'id'}) "
. "AND value+0 > UNIX_TIMESTAMP(?)" );
$sth->execute( $ownerid, $date );
while ( my ( $id, $prop, $dt ) = $sth->fetchrow_array ) {
if ( $prop == $p_calter->{'id'} ) {
$cmt{$id} = [ 'C', $id, $dt, "update" ];
}
elsif ( $prop == $p_revtime->{'id'} ) {
$item{$id} = [ 'L', $id, $dt, "update" ];
}
}
my @ev = sort { $a->[2] cmp $b->[2] } ( values %item, values %cmt );
my $res = {};
my $list = $res->{'syncitems'} = [];
$res->{'total'} = scalar @ev;
my $ct = 0;
while ( my $ev = shift @ev ) {
$ct++;
push @$list,
{
'item' => "$ev->[0]-$ev->[1]",
'time' => $ev->[2],
'action' => $ev->[3],
};
last if $ct >= $LIMIT;
}
$res->{'count'} = $ct;
return $res;
}
sub consolecommand {
my ( $req, $err, $flags ) = @_;
# logging in isn't necessary, but most console commands do require it
LJ::set_remote( $flags->{'u'} ) if authenticate( $req, $err, $flags );
my $res = {};
my $cmdout = $res->{'results'} = [];
foreach my $cmd ( @{ $req->{'commands'} } ) {
# callee can pre-parse the args, or we can do it bash-style
my @args =
ref $cmd eq "ARRAY"
? @$cmd
: LJ::Console->parse_line($cmd);
my $c = LJ::Console->parse_array(@args);
my $rv = $c->execute_safely;
my @output;
push @output, [ $_->status, $_->text ] foreach $c->responses;
push @{$cmdout},
{
'success' => $rv,
'output' => \@output,
};
}
return $res;
}
sub getchallenge {
my ( $req, $err, $flags ) = @_;
my $res = {};
my $now = time();
my $etime = 60;
$res->{'challenge'} = DW::Auth::Challenge->generate($etime);
$res->{'server_time'} = $now;
$res->{'expire_time'} = $now + $etime;
$res->{'auth_scheme'} = "c0"; # fixed for now, might support others later
return $res;
}
sub login_message {
my ( $req, $res, $flags ) = @_;
my $u = $flags->{'u'};
my $msg = sub {
my $code = shift;
my $args = shift || {};
$args->{'sitename'} = $LJ::SITENAME;
$args->{'siteroot'} = $LJ::SITEROOT;
my $pre = delete $args->{'pre'};
$res->{'message'} = $pre . translate( $u, $code, $args );
};
return $msg->("readonly") if $u->is_readonly;
return $msg->("not_validated") if $u->{'status'} eq "N";
return $msg->("must_revalidate") if $u->{'status'} eq "T";
return $msg->("old_win32_client") if $req->{'clientversion'} =~ /^Win32-MFC\/(1.2.[0123456])$/;
return $msg->("old_win32_client") if $req->{'clientversion'} =~ /^Win32-MFC\/(1.3.[01234])\b/;
return $msg->("hello_test") if grep { $u->{user} eq $_ } @LJ::TESTACCTS;
}
sub list_friendgroups {
my $u = shift;
# warn "LJ::Protocol: list_friendgroups called.\n";
return [];
}
sub list_trustgroups {
my $u = shift;
my $groups = $u->trust_groups;
return undef unless $groups;
# we got all of the groups, so put them into an arrayref sorted by the
# group sortorder; also note that the map is used to construct a new hashref
# out of the old group hashref so that we have all of the field names converted
# to a format our callers can recognize
my @res = map {
{
id => $_->{groupnum},
name => $_->{groupname},
public => $_->{is_public},
sortorder => $_->{sortorder},
}
}
sort { $a->{sortorder} <=> $b->{sortorder} } values %$groups;
return \@res;
}
sub list_contentfilters {
my $u = shift;
my @filters = $u->content_filters;
return [] unless @filters;
my @res = map {
{
id => $_->{id},
name => $_->{name},
public => $_->{public},
sortorder => $_->{sortorder},
data => join(
' ',
map {
my $uid = $_;
LJ::load_userid($uid)->user
} ( keys %{ $u->content_filters( id => $_->id )->data } )
)
}
} @filters;
return \@res;
}
sub list_usejournals {
my $u = shift;
my @us = $u->posting_access_list;
my @unames = map { $_->{user} } @us;
return \@unames;
}
sub hash_menus {
my $u = shift;
my $user = $u->{'user'};
my $menu = [
{
'text' => "Recent Entries",
'url' => "$LJ::SITEROOT/users/$user/",
},
{
'text' => "Calendar View",
'url' => "$LJ::SITEROOT/users/$user/archive",
},
{
'text' => "Friends View",
'url' => "$LJ::SITEROOT/users/$user/read",
},
{ 'text' => "-", },
{
'text' => "Your Profile",
'url' => "$LJ::SITEROOT/profile?user=$user",
},
{ 'text' => "-", },
{
'text' => "Change Settings",
'sub' => [
{
'text' => "Personal Info",
'url' => "$LJ::SITEROOT/manage/profile/",
},
{
'text' => "Customize Journal",
'url' => "$LJ::SITEROOT/customize/",
},
]
},
{ 'text' => "-", },
{
'text' => "Support",
'url' => "$LJ::SITEROOT/support/",
}
];
LJ::Hooks::run_hooks(
"modify_login_menu",
{
'menu' => $menu,
'u' => $u,
'user' => $user,
}
);
return $menu;
}
sub list_pickws {
my ($u) = @_;
return [] unless LJ::isu($u);
my $pi = $u->get_userpic_info;
my @res;
my %seen; # mashifiedptr -> 1
# FIXME: should be a utf-8 sort
foreach my $kw ( sort keys %{ $pi->{kw} } ) {
my $pic = $pi->{kw}{$kw};
$seen{$pic} = 1;
next if $pic->{state} eq "I";
push @res, [ $kw, $pic->{picid} ];
}
# now add all the pictures that don't have a keyword
foreach my $picid ( keys %{ $pi->{pic} } ) {
my $pic = $pi->{pic}{$picid};
next if $seen{$pic};
next if $pic->{state} eq "I";
push @res, [ "pic#$picid", $picid ];
}
return \@res;
}
sub list_moods {
my $mood_max = int(shift);
DW::Mood->load_moods;
my $res = [];
return $res if $mood_max >= $LJ::CACHED_MOOD_MAX;
for ( my $id = $mood_max + 1 ; $id <= $LJ::CACHED_MOOD_MAX ; $id++ ) {
next unless defined $LJ::CACHE_MOODS{$id};
my $mood = $LJ::CACHE_MOODS{$id};
next unless $mood->{'name'};
push @$res,
{
'id' => $id,
'name' => $mood->{'name'},
'parent' => $mood->{'parent'}
};
}
return $res;
}
sub check_altusage {
my ( $req, $err, $flags ) = @_;
my $alt = $req->{'usejournal'};
my $u = $flags->{'u'};
unless ($u) {
my $username = $req->{'username'};
return fail( $err, 200 ) unless $username;
return fail( $err, 100 ) unless LJ::canonical_username($username);
my $dbr = LJ::get_db_reader();
return fail( $err, 502 ) unless $dbr;
$u = $flags->{'u'} = LJ::load_user($username);
}
$flags->{'ownerid'} = $u->{'userid'};
# all good if not using an alt journal
return 1 unless $alt;
# complain if the username is invalid
return fail( $err, 206 ) unless LJ::canonical_username($alt);
# we are going to load the alt user
$flags->{u_owner} = LJ::load_user($alt);
$flags->{ownerid} = $flags->{u_owner} ? $flags->{u_owner}->id : undef;
my $apache_r = eval { BML::get_request() };
$apache_r->notes->{journalid} = $flags->{ownerid}
if $apache_r && !$apache_r->notes->{journalid};
# allow usage if we're told explicitly that it's okay
if ( $flags->{usejournal_okay} ) {
return 1 if $flags->{ownerid};
return fail( $err, 206 );
}
# or, if they have explicitly said to ignore canuse
return 1 if $flags->{ignorecanuse};
# otherwise, check for access
return 1 if $u->can_post_to( $flags->{u_owner} );
# not allowed to access it, bad user, no post
return fail( $err, 300 );
}
# Validate login/talk md5 responses. THIS IS DEPRECATED. This now only checks
# against a user's API keys as a pseudo "
#
# Return 1 on valid, 0 on invalid.
sub check_login {
my ( $u, $chal, $res, $banned, $opts ) = @_;
return 0 unless $u;
my @keys = @{ DW::API::Key->get_keys_for_user($u) || [] };
return 0 unless @keys;
# set the IP banned flag, if it was provided.
my $fake_scalar;
my $ref = ref $banned ? $banned : \$fake_scalar;
if ( LJ::login_ip_banned($u) ) {
$$ref = 1;
return 0;
}
else {
$$ref = 0;
}
# check the challenge string validity
return 0 unless DW::Auth::Challenge->check( $chal, $opts );
# Validate password against the user's list of API keys
foreach my $key (@keys) {
my $hashed = Digest::MD5::md5_hex( $chal . Digest::MD5::md5_hex( $key->hash ) );
if ( $hashed eq $res ) {
return 1;
}
}
# Login failed against all keys
LJ::handle_bad_login($u);
return 0;
}
sub authenticate {
my ( $req, $err, $flags ) = @_;
my $username = $req->{username};
return fail( $err, 200 ) unless $username;
return fail( $err, 100 ) unless LJ::canonical_username($username);
my $u = $flags->{u};
unless ($u) {
my $dbr = LJ::get_db_reader()
or return fail( $err, 502 );
$u = LJ::load_user($username);
}
return fail( $err, 100 ) unless $u;
return fail( $err, 100 ) if $u->is_expunged;
return fail( $err, 309 ) if $u->is_memorial; # memorial users can't do anything
return fail( $err, 505 ) unless $u->{clusterid};
my $r = DW::Request->get;
my $ip = LJ::get_remote_ip();
if ($r) {
$r->note( ljuser => $u->user )
unless $r->note('ljuser');
$r->note( journalid => $u->id )
unless $r->note('journalid');
}
my $ip_banned = 0;
my $chal_expired = 0;
my $auth_check = sub {
my $auth_meth = $req->{auth_method} || 'clear';
if ( $auth_meth eq 'clear' ) {
return LJ::auth_okay(
$u, $req->{password} // $req->{hpassword},
is_ip_banned => \$ip_banned,
allow_hpassword => 1,
allow_api_keys => 1
);
}
if ( $auth_meth eq 'challenge' ) {
my $chal_opts = {};
my $chal_ok = check_login( $u, $req->{auth_challenge},
$req->{auth_response}, \$ip_banned, $chal_opts );
$chal_expired = 1 if $chal_opts->{expired};
return $chal_ok;
}
if ( $auth_meth eq 'cookie' ) {
return unless $r && $r->header_in('X-LJ-Auth') eq 'cookie';
my $remote = LJ::get_remote();
return $remote && $remote->user eq $username ? 1 : 0;
}
return 0;
};
unless ( $flags->{nopassword}
|| $flags->{noauth}
|| $auth_check->() )
{
return fail( $err, 402 ) if $ip_banned;
return fail( $err, 105 ) if $chal_expired;
return fail( $err, 101 );
}
# remember the user record for later.
$flags->{u} = $u;
return 1;
}
sub fail {
my $err = shift;
my $code = shift;
my $des = shift;
$code .= ":$des" if $des;
$$err = $code if ( ref $err eq "SCALAR" );
return undef;
}
sub un_utf8_request {
my $req = shift;
$req->{$_} = LJ::no_utf8_flag( $req->{$_} ) foreach qw(subject event);
my $props = $req->{props} || {};
foreach my $k ( keys %$props ) {
next if ref $props->{$k}; # if this is multiple levels deep? don't think so.
$props->{$k} = LJ::no_utf8_flag( $props->{$k} );
}
}
# xmlrpc_method: dispatch an XMLRPC method call to do_request and wrap the
# result in SOAP types. Originally lived in Apache/LiveJournal.pm but moved
# here so it is available under Plack as well.
sub xmlrpc_method {
my $method = shift;
shift; # get rid of package name that dispatcher includes.
my $req = shift;
if (@_) {
# don't allow extra arguments
die SOAP::Fault->faultstring( LJ::Protocol::error_message(202) )->faultcode(202);
}
my $error = 0;
if ( ref $req eq "HASH" ) {
# get rid of the UTF8 flag in scalars
while ( my ( $k, $v ) = each %$req ) {
$req->{$k} = Encode::encode_utf8($v) if Encode::is_utf8($v);
}
}
my $res = LJ::Protocol::do_request( $method, $req, \$error );
if ($error) {
# FIXME [#1709]: which errors don't start with numbers?
print STDERR "[#1709] xmlrpc error for $method needs faultcode: $error\n"
unless $error =~ /^\d{3}/;
# existing behavior
die SOAP::Fault->faultstring( LJ::Protocol::error_message($error) )
->faultcode( substr( $error, 0, 3 ) );
}
# Perl is untyped language and XML-RPC is typed.
# When library XMLRPC::Lite tries to guess type, it errors sometimes
# (e.g. string username goes as int, if username contains digits only).
# As workaround, we can select some elements by it's names
# and label them by correct types.
# Key - field name, value - type.
my %lj_types_map = (
journalname => 'string',
fullname => 'string',
username => 'string',
poster => 'string',
postername => 'string',
name => 'string',
);
my $recursive_mark_elements;
$recursive_mark_elements = sub {
my $structure = shift;
my $ref = ref($structure);
if ( $ref eq 'HASH' ) {
foreach my $hash_key ( keys %$structure ) {
if ( exists( $lj_types_map{$hash_key} ) ) {
$structure->{$hash_key} = SOAP::Data->type( $lj_types_map{$hash_key} )
->value( $structure->{$hash_key} );
}
else {
$recursive_mark_elements->( $structure->{$hash_key} );
}
}
}
elsif ( $ref eq 'ARRAY' ) {
foreach my $idx (@$structure) {
$recursive_mark_elements->($idx);
}
}
};
$recursive_mark_elements->($res);
return $res;
}
#### Old interface (flat key/values) -- wrapper aruond LJ::Protocol
package LJ;
sub do_request {
# get the request and response hash refs
my ( $req, $res, $flags ) = @_;
# initialize some stuff
%{$res} = (); # clear the given response hash
$flags = {} unless ( ref $flags eq "HASH" );
# did they send a mode?
unless ( $req->{'mode'} ) {
$res->{'success'} = "FAIL";
$res->{'errmsg'} = "Client error: No mode specified.";
return;
}
# this method doesn't require auth
if ( $req->{'mode'} eq "getchallenge" ) {
return getchallenge( $req, $res, $flags );
}
# mode from here on out require a username
my $user = LJ::canonical_username( $req->{'user'} );
unless ($user) {
$res->{'success'} = "FAIL";
$res->{'errmsg'} = "Client error: No username sent.";
return;
}
## dispatch wrappers
if ( $req->{'mode'} eq "login" ) {
return login( $req, $res, $flags );
}
if ( $req->{'mode'} eq "getfriendgroups" ) {
return getfriendgroups( $req, $res, $flags );
}
if ( $req->{'mode'} eq "gettrustgroups" ) {
return gettrustgroups( $req, $res, $flags );
}
if ( $req->{'mode'} eq "getfriends" ) {
return getfriends( $req, $res, $flags );
}
if ( $req->{'mode'} eq "friendof" ) {
return friendof( $req, $res, $flags );
}
if ( $req->{'mode'} eq "checkfriends" ) {
return checkfriends( $req, $res, $flags );
}
if ( $req->{'mode'} eq "checkforupdates" ) {
return checkforupdates( $req, $res, $flags );
}
if ( $req->{'mode'} eq "getdaycounts" ) {
return getdaycounts( $req, $res, $flags );
}
if ( $req->{'mode'} eq "postevent" ) {
return postevent( $req, $res, $flags );
}
if ( $req->{'mode'} eq "editevent" ) {
return editevent( $req, $res, $flags );
}
if ( $req->{'mode'} eq "syncitems" ) {
return syncitems( $req, $res, $flags );
}
if ( $req->{'mode'} eq "getevents" ) {
return getevents( $req, $res, $flags );
}
if ( $req->{'mode'} eq "editfriends" ) {
return editfriends( $req, $res, $flags );
}
if ( $req->{'mode'} eq "editfriendgroups" ) {
return editfriendgroups( $req, $res, $flags );
}
if ( $req->{'mode'} eq "consolecommand" ) {
return consolecommand( $req, $res, $flags );
}
if ( $req->{'mode'} eq "sessiongenerate" ) {
return sessiongenerate( $req, $res, $flags );
}
if ( $req->{'mode'} eq "sessionexpire" ) {
return sessionexpire( $req, $res, $flags );
}
if ( $req->{'mode'} eq "getusertags" ) {
return getusertags( $req, $res, $flags );
}
if ( $req->{'mode'} eq "getfriendspage" ) {
return getfriendspage( $req, $res, $flags );
}
if ( $req->{'mode'} eq "getreadpage" ) {
return getreadpage( $req, $res, $flags );
}
### unknown mode!
$res->{'success'} = "FAIL";
$res->{'errmsg'} = "Client error: Unknown mode ($req->{'mode'})";
return;
}
## flat wrapper
sub getfriendspage {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
my $rs = LJ::Protocol::do_request( "getfriendspage", $rq, \$err, $flags );
unless ($rs) {
$res->{'success'} = "FAIL";
$res->{'errmsg'} = LJ::Protocol::error_message($err);
return 0;
}
return 1;
}
sub getreadpage {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
my $rs = LJ::Protocol::do_request( "getreadpage", $rq, \$err, $flags );
unless ($rs) {
$res->{'success'} = "FAIL";
$res->{'errmsg'} = LJ::Protocol::error_message($err);
return 0;
}
my $ect = 0;
foreach my $evt ( @{ $rs->{'entries'} } ) {
$ect++;
foreach
my $f (qw(subject_raw journalname journaltype postername postertype ditemid security))
{
if ( defined $evt->{$f} ) {
$res->{"entries_${ect}_$f"} = $evt->{$f};
}
}
$res->{"entries_${ect}_event"} = LJ::eurl( $evt->{'event_raw'} );
}
$res->{'entries_count'} = $ect;
$res->{'success'} = "OK";
return 1;
}
## flat wrapper
sub login {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
my $rs = LJ::Protocol::do_request( "login", $rq, \$err, $flags );
unless ($rs) {
$res->{'success'} = "FAIL";
$res->{'errmsg'} = LJ::Protocol::error_message($err);
return 0;
}
$res->{'success'} = "OK";
$res->{'name'} = $rs->{'fullname'};
$res->{'message'} = $rs->{'message'} if $rs->{'message'};
$res->{'fastserver'} = 1 if $rs->{'fastserver'};
$res->{'caps'} = $rs->{'caps'} if $rs->{'caps'};
# shared journals
my $access_count = 0;
foreach my $user ( @{ $rs->{'usejournals'} } ) {
$access_count++;
$res->{"access_${access_count}"} = $user;
}
if ($access_count) {
$res->{"access_count"} = $access_count;
}
# friend groups
populate_friend_groups( $res, $rs->{'friendgroups'} );
my $flatten = sub {
my ( $prefix, $listref ) = @_;
my $ct = 0;
foreach (@$listref) {
$ct++;
$res->{"${prefix}_$ct"} = $_;
}
$res->{"${prefix}_count"} = $ct;
};
### picture keywords
$flatten->( "pickw", $rs->{'pickws'} )
if defined $req->{"getpickws"};
$flatten->( "pickwurl", $rs->{'pickwurls'} )
if defined $req->{"getpickwurls"};
$res->{'defaultpicurl'} = $rs->{'defaultpicurl'} if $rs->{'defaultpicurl'};
### report new moods that this client hasn't heard of, if they care
if ( defined $req->{"getmoods"} ) {
my $mood_count = 0;
foreach my $m ( @{ $rs->{'moods'} } ) {
$mood_count++;
$res->{"mood_${mood_count}_id"} = $m->{'id'};
$res->{"mood_${mood_count}_name"} = $m->{'name'};
$res->{"mood_${mood_count}_parent"} = $m->{'parent'};
}
if ($mood_count) {
$res->{"mood_count"} = $mood_count;
}
}
#### send web menus
if ( $req->{"getmenus"} == 1 ) {
my $menu = $rs->{'menus'};
my $menu_num = 0;
populate_web_menu( $res, $menu, \$menu_num );
}
return 1;
}
## flat wrapper
sub getfriendgroups {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
my $rs = LJ::Protocol::do_request( "getfriendgroups", $rq, \$err, $flags );
unless ($rs) {
$res->{'success'} = "FAIL";
$res->{'errmsg'} = LJ::Protocol::error_message($err);
return 0;
}
$res->{'success'} = "OK";
populate_friend_groups( $res, $rs->{'friendgroups'} );
return 1;
}
## flat wrapper
sub gettrustgroups {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
my $rs = LJ::Protocol::do_request( 'gettrustgroups', $rq, \$err, $flags );
unless ($rs) {
$res->{success} = "FAIL";
$res->{errmsg} = LJ::Protocol::error_message($err);
return 0;
}
$res->{success} = "OK";
populate_groups( $res, 'tr', $rs->{trustgroups} );
return 1;
}
## flat wrapper
sub getusertags {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
my $rs = LJ::Protocol::do_request( "getusertags", $rq, \$err, $flags );
unless ($rs) {
$res->{'success'} = "FAIL";
$res->{'errmsg'} = LJ::Protocol::error_message($err);
return 0;
}
$res->{'success'} = "OK";
my $ct = 0;
foreach my $tag ( @{ $rs->{tags} } ) {
$ct++;
$res->{"tag_${ct}_security"} = $tag->{security_level};
$res->{"tag_${ct}_uses"} = $tag->{uses} if $tag->{uses};
$res->{"tag_${ct}_display"} = $tag->{display} if $tag->{display};
$res->{"tag_${ct}_name"} = $tag->{name};
foreach my $lev (qw(friends private public)) {
$res->{"tag_${ct}_sb_$_"} = $tag->{security}->{$_}
if $tag->{security}->{$_};
}
my $gm = 0;
foreach my $grpid ( keys %{ $tag->{security}->{groups} } ) {
next unless $tag->{security}->{groups}->{$grpid};
$gm++;
$res->{"tag_${ct}_sb_group_${gm}_id"} = $grpid;
$res->{"tag_${ct}_sb_group_${gm}_count"} = $tag->{security}->{groups}->{$grpid};
}
$res->{"tag_${ct}_sb_group_count"} = $gm if $gm;
}
$res->{'tag_count'} = $ct;
return 1;
}
## flat wrapper
sub getfriends {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
my $rs = LJ::Protocol::do_request( "getfriends", $rq, \$err, $flags );
unless ($rs) {
$res->{'success'} = "FAIL";
$res->{'errmsg'} = LJ::Protocol::error_message($err);
return 0;
}
$res->{'success'} = "OK";
if ( $req->{'includegroups'} ) {
populate_friend_groups( $res, $rs->{'friendgroups'} );
}
if ( $req->{'includefriendof'} ) {
populate_friends( $res, "friendof", $rs->{'friendofs'} );
}
populate_friends( $res, "friend", $rs->{'friends'} );
return 1;
}
## flat wrapper
sub friendof {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
my $rs = LJ::Protocol::do_request( "friendof", $rq, \$err, $flags );
unless ($rs) {
$res->{'success'} = "FAIL";
$res->{'errmsg'} = LJ::Protocol::error_message($err);
return 0;
}
$res->{'success'} = "OK";
populate_friends( $res, "friendof", $rs->{'friendofs'} );
return 1;
}
## flat wrapper
sub checkfriends {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
my $rs = LJ::Protocol::do_request( "checkfriends", $rq, \$err, $flags );
unless ($rs) {
$res->{'success'} = "FAIL";
$res->{'errmsg'} = LJ::Protocol::error_message($err);
return 0;
}
$res->{'success'} = "OK";
$res->{'new'} = $rs->{'new'};
$res->{'lastupdate'} = $rs->{'lastupdate'};
$res->{'interval'} = $rs->{'interval'};
return 1;
}
## flat wrapper
sub checkforupdates {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
my $rs = LJ::Protocol::do_request( "checkforupdates", $rq, \$err, $flags );
unless ($rs) {
$res->{success} = "FAIL";
$res->{errmsg} = LJ::Protocol::error_message($err);
return 0;
}
$res->{success} = "OK";
$res->{new} = $rs->{new};
$res->{lastupdate} = $rs->{lastupdate};
$res->{interval} = $rs->{interval};
return 1;
}
## flat wrapper
sub getdaycounts {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
my $rs = LJ::Protocol::do_request( "getdaycounts", $rq, \$err, $flags );
unless ($rs) {
$res->{'success'} = "FAIL";
$res->{'errmsg'} = LJ::Protocol::error_message($err);
return 0;
}
$res->{'success'} = "OK";
foreach my $d ( @{ $rs->{'daycounts'} } ) {
$res->{ $d->{'date'} } = $d->{'count'};
}
return 1;
}
## flat wrapper
sub syncitems {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
my $rs = LJ::Protocol::do_request( "syncitems", $rq, \$err, $flags );
unless ($rs) {
$res->{'success'} = "FAIL";
$res->{'errmsg'} = LJ::Protocol::error_message($err);
return 0;
}
$res->{'success'} = "OK";
$res->{'sync_total'} = $rs->{'total'};
$res->{'sync_count'} = $rs->{'count'};
my $ct = 0;
foreach my $s ( @{ $rs->{'syncitems'} } ) {
$ct++;
foreach my $a (qw(item action time)) {
$res->{"sync_${ct}_$a"} = $s->{$a};
}
}
return 1;
}
## flat wrapper: limited functionality. (1 command only, server-parsed only)
sub consolecommand {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
delete $rq->{'command'};
$rq->{'commands'} = [ $req->{'command'} ];
my $rs = LJ::Protocol::do_request( "consolecommand", $rq, \$err, $flags );
unless ($rs) {
$res->{'success'} = "FAIL";
$res->{'errmsg'} = LJ::Protocol::error_message($err);
return 0;
}
$res->{'cmd_success'} = $rs->{'results'}->[0]->{'success'};
$res->{'cmd_line_count'} = 0;
foreach my $l ( @{ $rs->{'results'}->[0]->{'output'} } ) {
$res->{'cmd_line_count'}++;
my $line = $res->{'cmd_line_count'};
$res->{"cmd_line_${line}_type"} = $l->[0]
if $l->[0];
$res->{"cmd_line_${line}"} = $l->[1];
}
$res->{'success'} = "OK";
}
## flat wrapper
sub getchallenge {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rs = LJ::Protocol::do_request( "getchallenge", $req, \$err, $flags );
# stupid copy (could just return $rs), but it might change in the future
# so this protects us from future accidental harm.
foreach my $k (qw(challenge server_time expire_time auth_scheme)) {
$res->{$k} = $rs->{$k};
}
$res->{'success'} = "OK";
return $res;
}
## flat wrapper
sub editfriends {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
$rq->{'add'} = [];
$rq->{'delete'} = [];
foreach ( keys %$req ) {
if (/^editfriend_add_(\d+)_user$/) {
my $n = $1;
next unless ( $req->{"editfriend_add_${n}_user"} =~ /\S/ );
my $fa = {
'username' => $req->{"editfriend_add_${n}_user"},
'fgcolor' => $req->{"editfriend_add_${n}_fg"},
'bgcolor' => $req->{"editfriend_add_${n}_bg"},
'groupmask' => $req->{"editfriend_add_${n}_groupmask"},
};
push @{ $rq->{'add'} }, $fa;
}
elsif (/^editfriend_delete_(\w+)$/) {
push @{ $rq->{'delete'} }, $1;
}
}
my $rs = LJ::Protocol::do_request( "editfriends", $rq, \$err, $flags );
unless ($rs) {
$res->{'success'} = "FAIL";
$res->{'errmsg'} = LJ::Protocol::error_message($err);
return 0;
}
$res->{'success'} = "OK";
my $ct = 0;
foreach my $fa ( @{ $rs->{'added'} } ) {
$ct++;
$res->{"friend_${ct}_user"} = $fa->{'username'};
$res->{"friend_${ct}_name"} = $fa->{'fullname'};
}
$res->{'friends_added'} = $ct;
return 1;
}
## flat wrapper
sub editfriendgroups {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
$rq->{'groupmasks'} = {};
$rq->{'set'} = {};
$rq->{'delete'} = [];
foreach ( keys %$req ) {
if (/^efg_set_(\d+)_name$/) {
next unless ( $req->{$_} ne "" );
my $n = $1;
my $fs = {
'name' => $req->{"efg_set_${n}_name"},
'sort' => $req->{"efg_set_${n}_sort"},
};
if ( defined $req->{"efg_set_${n}_public"} ) {
$fs->{'public'} = $req->{"efg_set_${n}_public"};
}
$rq->{'set'}->{$n} = $fs;
}
elsif (/^efg_delete_(\d+)$/) {
if ( $req->{$_} ) {
# delete group if value is true
push @{ $rq->{'delete'} }, $1;
}
}
elsif (/^editfriend_groupmask_(\w+)$/) {
$rq->{'groupmasks'}->{$1} = $req->{$_};
}
}
my $rs = LJ::Protocol::do_request( "editfriendgroups", $rq, \$err, $flags );
unless ($rs) {
$res->{'success'} = "FAIL";
$res->{'errmsg'} = LJ::Protocol::error_message($err);
return 0;
}
$res->{'success'} = "OK";
return 1;
}
sub flatten_props {
my ( $req, $rq ) = @_;
## changes prop_* to props hashref
foreach my $k ( keys %$req ) {
next unless ( $k =~ /^prop_(.+)/ );
$rq->{'props'}->{$1} = $req->{$k};
}
}
## flat wrapper
sub postevent {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
flatten_props( $req, $rq );
$rq->{'props'}->{'interface'} = "flat";
my $rs = LJ::Protocol::do_request( "postevent", $rq, \$err, $flags );
unless ($rs) {
$res->{'success'} = "FAIL";
$res->{'errmsg'} = LJ::Protocol::error_message($err);
return 0;
}
$res->{'message'} = $rs->{'message'} if $rs->{'message'};
$res->{'success'} = "OK";
$res->{'itemid'} = $rs->{'itemid'};
$res->{'anum'} = $rs->{'anum'} if defined $rs->{'anum'};
$res->{'url'} = $rs->{'url'} if defined $rs->{'url'};
return 1;
}
## flat wrapper
sub editevent {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
flatten_props( $req, $rq );
my $rs = LJ::Protocol::do_request( "editevent", $rq, \$err, $flags );
unless ($rs) {
$res->{'success'} = "FAIL";
$res->{'errmsg'} = LJ::Protocol::error_message($err);
return 0;
}
$res->{message} = $rs->{message} if $rs->{message};
$res->{'success'} = "OK";
$res->{'itemid'} = $rs->{'itemid'};
$res->{'anum'} = $rs->{'anum'} if defined $rs->{'anum'};
$res->{'url'} = $rs->{'url'} if defined $rs->{'url'};
return 1;
}
## flat wrapper
sub sessiongenerate {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
my $rs = LJ::Protocol::do_request( 'sessiongenerate', $rq, \$err, $flags );
unless ($rs) {
$res->{success} = 'FAIL';
$res->{errmsg} = LJ::Protocol::error_message($err);
}
$res->{success} = 'OK';
$res->{ljsession} = $rs->{ljsession};
return 1;
}
## flat wrappre
sub sessionexpire {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
$rq->{expire} = [];
foreach my $k ( keys %$rq ) {
push @{ $rq->{expire} }, $1
if $k =~ /^expire_id_(\d+)$/;
}
my $rs = LJ::Protocol::do_request( 'sessionexpire', $rq, \$err, $flags );
unless ($rs) {
$res->{success} = 'FAIL';
$res->{errmsg} = LJ::Protocol::error_message($err);
}
$res->{success} = 'OK';
return 1;
}
## flat wrapper
sub getevents {
my ( $req, $res, $flags ) = @_;
my $err = 0;
my $rq = upgrade_request($req);
my $rs = LJ::Protocol::do_request( "getevents", $rq, \$err, $flags );
unless ($rs) {
$res->{success} = "FAIL";
$res->{errmsg} = LJ::Protocol::error_message($err);
return 0;
}
my $ect = 0;
my $pct = 0;
foreach my $evt ( @{ $rs->{events} } ) {
$ect++;
foreach my $f (
qw(itemid eventtime logtime security allowmask subject anum url poster converted_with_loss)
)
{
if ( defined $evt->{$f} ) {
$res->{"events_${ect}_$f"} = $evt->{$f};
}
}
$res->{"events_${ect}_event"} = LJ::eurl( $evt->{event} );
if ( $evt->{props} ) {
foreach my $k ( sort keys %{ $evt->{props} } ) {
$pct++;
$res->{"prop_${pct}_itemid"} = $evt->{itemid};
$res->{"prop_${pct}_name"} = $k;
$res->{"prop_${pct}_value"} = $evt->{props}->{$k};
}
}
}
unless ( $req->{noprops} ) {
$res->{prop_count} = $pct;
}
$res->{events_count} = $ect;
$res->{success} = "OK";
return 1;
}
sub populate_friends {
my ( $res, $pfx, $list ) = @_;
my $count = 0;
foreach my $f (@$list) {
$count++;
$res->{"${pfx}_${count}_name"} = $f->{'fullname'};
$res->{"${pfx}_${count}_user"} = $f->{'username'};
$res->{"${pfx}_${count}_birthday"} = $f->{'birthday'} if $f->{'birthday'};
$res->{"${pfx}_${count}_bg"} = $f->{'bgcolor'};
$res->{"${pfx}_${count}_fg"} = $f->{'fgcolor'};
if ( defined $f->{'groupmask'} ) {
$res->{"${pfx}_${count}_groupmask"} = $f->{'groupmask'};
}
if ( defined $f->{'type'} ) {
$res->{"${pfx}_${count}_type"} = $f->{'type'};
if ( $f->{'type'} eq 'identity' ) {
$res->{"${pfx}_${count}_identity_type"} = $f->{'identity_type'};
$res->{"${pfx}_${count}_identity_value"} = $f->{'identity_value'};
$res->{"${pfx}_${count}_identity_display"} = $f->{'identity_display'};
}
}
if ( defined $f->{'status'} ) {
$res->{"${pfx}_${count}_status"} = $f->{'status'};
}
}
$res->{"${pfx}_count"} = $count;
}
sub upgrade_request {
my $r = shift;
my $new = { %{$r} };
$new->{'username'} = $r->{'user'};
# but don't delete $r->{'user'}, as it might be, say, %FORM,
# that'll get reused in a later request in, say, update.bml after
# the login before postevent. whoops.
return $new;
}
## given a $res hashref and friend group subtree (arrayref), flattens it
sub populate_friend_groups {
my ( $res, $fr ) = @_;
my $maxnum = 0;
foreach my $fg (@$fr) {
my $num = $fg->{'id'};
$res->{"frgrp_${num}_name"} = $fg->{'name'};
$res->{"frgrp_${num}_sortorder"} = $fg->{'sortorder'};
if ( $fg->{'public'} ) {
$res->{"frgrp_${num}_public"} = 1;
}
if ( $num > $maxnum ) { $maxnum = $num; }
}
$res->{'frgrp_maxnum'} = $maxnum;
}
## given a $res hashref and trust group (arrayref), flattens it
sub populate_groups {
my ( $res, $pfx, $fr ) = @_;
my $maxnum = 0;
foreach my $fg (@$fr) {
my $num = $fg->{id};
$res->{"${pfx}_${num}_name"} = $fg->{name};
$res->{"${pfx}_${num}_sortorder"} = $fg->{sortorder};
$res->{"${pfx}_${num}_public"} = 1 if $fg->{public};
$maxnum = $num if ( $num > $maxnum );
}
$res->{"${pfx}_maxnum"} = $maxnum;
}
## given a menu tree, flattens it into $res hashref
sub populate_web_menu {
my ( $res, $menu, $numref ) = @_;
my $mn = $$numref; # menu number
my $mi = 0; # menu item
foreach my $it (@$menu) {
$mi++;
$res->{"menu_${mn}_${mi}_text"} = $it->{'text'};
if ( $it->{'text'} eq "-" ) { next; }
if ( $it->{'sub'} ) {
$$numref++;
$res->{"menu_${mn}_${mi}_sub"} = $$numref;
&populate_web_menu( $res, $it->{'sub'}, $numref );
next;
}
$res->{"menu_${mn}_${mi}_url"} = $it->{'url'};
}
$res->{"menu_${mn}_count"} = $mi;
}
1;