#!/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 from HTML-tag 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///; } } #### /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;