LJSUP-17669: Login.bml form refactoring
[livejournal.git] / cgi-bin / ljprotocol.pl
blobfce091c12e6121cbbc8f613a8b1d5777a17c03c4
1 #!/usr/bin/perl
4 use strict;
5 no warnings 'uninitialized';
7 use LJ::Constants;
8 use Class::Autouse qw(
9 LJ::Auth::Challenge
10 LJ::Auth::Checker
11 LJ::Auth::Method::ChallengeResponse
12 LJ::Auth::Method::LoginPassword::Clear
13 LJ::Auth::Method::LoginPassword::MD5
14 LJ::Event::JournalNewEntry
15 LJ::Event::UserNewEntry
16 LJ::EntriesIndex
17 LJ::Entry
18 LJ::Poll
19 LJ::EventLogRecord::NewEntry
20 LJ::EventLogRecord::EditEntry
21 LJ::Config
22 LJ::Comment
23 LJ::RateLimit
24 LJ::EmbedModule
25 LJ::DelayedEntry
26 LJ::PushNotification
27 LJ::Tidy
28 LJ::PersistentQueue
29 LJ::PersonalStats::Ratings::Posts
30 LJ::PersonalStats::Ratings::Journals
31 LJ::API::RateLimiter
32 LJ::Pay::Repost::Offer
33 LJ::MemCacheProxy
34 LJ::Entry::Repost
35 LJ::Tags
36 LJ::User::Groups
37 LJ::RelationService
40 use LJ::TimeUtil;
41 use POSIX;
43 LJ::Config->load;
45 use lib "$ENV{LJHOME}/cgi-bin";
47 # have to do this else mailgate will croak with email posting, but only want
48 # to do it if the site has enabled the hack
49 require "talklib.pl" if $LJ::NEW_ENTRY_CLEANUP_HACK;
51 # when posting or editing ping hubbub
52 require "ljfeed.pl";
54 #### New interface (meta handler) ... other handlers should call into this.
55 package LJ::Protocol;
57 # global declaration of this text since we use it in two places
58 our $CannotBeShown = '(cannot be shown)';
60 # error classes
61 use constant E_TEMP => 0;
62 use constant E_PERM => 1;
63 # maximum items for get_friends_page function
64 use constant FRIEND_ITEMS_LIMIT => 50;
66 my %e = (
67 # User Errors
68 "100" => E_PERM,
69 "101" => E_PERM,
70 "102" => E_PERM,
71 "103" => E_PERM,
72 "104" => E_TEMP,
73 "105" => E_PERM,
74 "150" => E_PERM,
75 "151" => E_TEMP,
76 "152" => E_PERM,
77 "153" => E_PERM,
78 "154" => E_PERM,
79 "155" => E_TEMP,
80 "156" => E_TEMP,
81 "157" => E_TEMP,
82 "158" => E_TEMP,
83 "159" => E_PERM,
84 "160" => E_TEMP,
85 "161" => E_PERM,
87 # Client Errors
88 "200" => E_PERM,
89 "201" => E_PERM,
90 "202" => E_PERM,
91 "203" => E_PERM,
92 "204" => E_PERM,
93 "205" => E_PERM,
94 "206" => E_PERM,
95 "207" => E_PERM,
96 "208" => E_PERM,
97 "209" => E_PERM,
98 "210" => E_PERM,
99 "211" => E_PERM,
100 "212" => E_PERM,
101 "213" => E_PERM,
102 "214" => E_PERM,
103 "215" => E_PERM,
104 "216" => E_PERM,
105 "217" => E_PERM,
106 "218" => E_PERM,
107 "219" => E_PERM,
108 "220" => E_PERM,
109 "221" => E_PERM,
110 "222" => E_PERM,
111 "223" => E_TEMP,
112 "224" => E_TEMP,
113 "225" => E_TEMP,
114 "226" => E_TEMP,
115 "227" => E_TEMP,
116 "228" => E_TEMP,
117 "229" => E_TEMP,
119 # Access Errors
120 "300" => E_TEMP,
121 "301" => E_TEMP,
122 "302" => E_TEMP,
123 "303" => E_TEMP,
124 "304" => E_TEMP,
125 "305" => E_TEMP,
126 "306" => E_TEMP,
127 "307" => E_PERM,
128 "308" => E_TEMP,
129 "309" => E_PERM,
130 "310" => E_TEMP,
131 "311" => E_TEMP,
132 "312" => E_TEMP,
133 "313" => E_TEMP,
134 "314" => E_PERM,
135 "315" => E_PERM,
136 "316" => E_TEMP,
137 "317" => E_TEMP,
138 "318" => E_TEMP,
139 "319" => E_TEMP,
140 "320" => E_TEMP,
141 "321" => E_TEMP,
142 "322" => E_PERM,
143 "323" => E_PERM,
144 "324" => E_PERM,
145 "325" => E_PERM,
146 "326" => E_PERM,
147 "327" => E_PERM,
148 "328" => E_PERM,
149 "329" => E_PERM,
150 "330" => E_PERM,
151 "331" => E_TEMP,
152 "332" => E_TEMP,
153 "333" => E_PERM,
154 "334" => E_PERM,
155 "335" => E_PERM,
156 "336" => E_TEMP,
157 "337" => E_TEMP,
159 # Limit errors
160 "402" => E_TEMP,
161 "404" => E_TEMP,
162 "405" => E_TEMP,
163 "406" => E_TEMP,
164 "407" => E_TEMP,
165 "408" => E_TEMP,
166 "409" => E_PERM,
167 "410" => E_PERM,
168 "411" => E_TEMP,
169 "412" => E_TEMP,
170 "413" => E_TEMP,
172 # Server Errors
173 "500" => E_TEMP,
174 "501" => E_TEMP,
175 "502" => E_TEMP,
176 "503" => E_TEMP,
177 "504" => E_PERM,
178 "505" => E_TEMP,
179 "506" => E_TEMP,
180 "507" => E_PERM,
181 "508" => E_PERM,
184 my %HANDLERS = (
185 login => \&login,
186 getfriendgroups => \&getfriendgroups,
187 getfriends => \&getfriends,
188 friendof => \&friendof,
189 checkfriends => \&checkfriends,
190 getdaycounts => \&getdaycounts,
191 postevent => \&postevent,
192 editevent => \&editevent,
193 syncitems => \&syncitems,
194 getevents => \&getevents,
195 createrepost => \&createrepost,
196 deleterepost => \&deleterepost,
197 getrepoststatus => \&getrepoststatus,
198 editfriends => \&editfriends,
199 editfriendgroups => \&editfriendgroups,
200 consolecommand => \&consolecommand,
201 getchallenge => \&getchallenge,
202 sessiongenerate => \&sessiongenerate,
203 sessionexpire => \&sessionexpire,
204 getusertags => \&getusertags,
205 getfriendspage => \&getfriendspage,
206 getinbox => \&getinbox,
207 sendmessage => \&sendmessage,
208 setmessageread => \&setmessageread,
209 addcomment => \&addcomment,
210 checksession => \&checksession,
212 getrecentcomments => \&getrecentcomments,
213 getcomments => \&getcomments,
214 deletecomments => \&deletecomments,
215 updatecomments => \&updatecomments,
216 editcomment => \&editcomment,
218 getuserpics => \&getuserpics,
219 createpoll => \&createpoll,
220 getpoll => \&getpoll,
221 editpoll => \&editpoll,
222 votepoll => \&votepoll,
223 registerpush => \&registerpush,
224 unregisterpush => \&unregisterpush,
225 pushsubscriptions => \&pushsubscriptions,
226 resetpushcounter => \&resetpushcounter,
227 getpushlist => \&getpushlist,
229 !$LJ::DISABLED{'xmlrpc_ratings'} ? (geteventsrating => \&geteventsrating) : (),
230 !$LJ::DISABLED{'xmlrpc_ratings'} ? (getusersrating => \&getusersrating) : (),
231 !$LJ::DISABLED{'xmlrpc_ratings'} ? (getratingcategories => \&getratingcategories) : (),
233 !$LJ::DISABLED{'xmlrpc_ratings'} ? (getdiscoveryfeed => \&getdiscoveryfeed) : (),
234 !$LJ::DISABLED{'xmlrpc_ratings'} ? (getdiscoverycategories => \&getdiscoverycategories): (),
235 !$LJ::DISABLED{'xmlrpc_ratings'} ? (getdiscoveryitem => \&getdiscoveryitem) : (),
238 sub translate
240 my ($u, $msg, $vars) = @_;
242 LJ::load_user_props($u, "browselang") unless $u->{'browselang'};
243 return LJ::Lang::get_text($u->{'browselang'}, "protocol.$msg", undef, $vars);
246 sub error_class
248 my $code = shift;
249 $code = $1 if $code =~ /^(\d\d\d):(.+)/;
250 return $e{$code};
253 sub error_is_transient
255 my $class = error_class($_[0]);
256 return defined $class ? ! $class+0 : undef;
259 sub error_is_permanent
261 return error_class($_[0]);
264 sub error_message
266 my $code = shift;
267 my $des;
268 ($code, $des) = ($1, $2) if $code =~ /^(\d\d\d):(.+)/;
270 my $prefix = "";
271 my $error = LJ::Lang::ml("xmlrpc.error.$code") || LJ::Lang::get_text(undef, "xmlrpc.error.$code") || "BUG: Unknown error code ($code)!";
272 $prefix = LJ::Lang::ml('xmlrpc.client_error') if $code >= 200;
273 $prefix = LJ::Lang::ml('xmlrpc.server_error') if $code >= 500;
274 my $totalerror = "$prefix $error";
275 $totalerror .= ": $des" if $des;
276 $totalerror =~ s/^\s+//;
277 return $totalerror;
280 sub do_request
282 # get the request and response hash refs
283 my ($method, $req, $err, $flags) = @_;
285 # xmlrpc rate limit
286 if (
287 ( grep { $method eq $_ } @LJ::API_RATE_LIMIT_LIMITED_METHODS ) # Only for specified methods.
288 && !$flags->{noauth} # Only for external calls.
289 && ( $req->{'props'}->{'interface'} eq 'xml-rpc' ) # Only for xml-rpc calls.
290 && LJ::Request->is_inited() # Only in web context.
292 $flags->{is_use_limits} = 1;
293 my $rate_limiter = LJ::API::RateLimiter->new(LJ::Request->request);
295 return fail($err,402) unless $rate_limiter->rate_point("xmlrpc.$method");
297 #-----------------#
299 if (ref $req eq "HASH") {
301 # if version isn't specified explicitly, it's version 0
302 $req->{'ver'} ||= $req->{'version'};
303 $req->{'ver'} = 0 unless defined $req->{'ver'};
305 # check specified language
306 if ($req->{'lang'} && not grep /^$req->{'lang'}$/, (@LJ::LANGS, 'en')) {
307 return fail($err, 221, $req->{'lang'} );
311 # set specified or default language
312 my $current_lang = LJ::Lang::current_language();
313 my $lang = $req->{'lang'} || $current_lang || $LJ::DEFAULT_LANG;
314 $lang = 'en_LJ' if $lang eq 'en';
315 LJ::Lang::current_language($lang) unless $lang eq $current_lang;
317 $flags ||= {};
318 $req->{'method'} = $method;
319 my @args = ($req, $err, $flags);
321 LJ::Request->notes("codepath" => "protocol.$method")
322 if LJ::Request->is_inited && ! LJ::Request->notes("codepath");
324 my $method_ref = $HANDLERS{$method};
326 if ($method_ref) {
327 my $result = $method_ref->(@args);
329 if ($result && exists $result->{'xc3'}) {
330 my $xc3 = delete $result->{'xc3'};
332 if ($req->{'props'}->{'interface'} eq 'xml-rpc') {
333 my $ua = eval { LJ::Request->header_in("User-Agent") };
334 Encode::from_to($ua, 'utf8', 'utf8') if $ua;
336 my ($ip_class, $country) = LJ::GeoLocation->ip_class();
338 my $args = {
339 function => $method || ''
342 if ($xc3->{'u'})
344 my $u = $xc3->{'u'};
345 $args->{'userid'} = $u->userid;
346 $args->{'usercaps'} = $u->caps;
349 $args->{'useragent'} = $ua if $ua;
350 $args->{'country'} = $country if $country;
351 $args->{'post'} = $xc3->{'post'} if $xc3->{'post'};
352 $args->{'comment'} = $xc3->{'comment'} if $xc3->{'comment'};
354 LJ::run_hooks("remote_procedure_call", $args);
358 return $result;
361 LJ::Request->notes("codepath" => "") if LJ::Request->is_inited;
363 return fail($err, 201);
366 sub getpoll
368 my ($req, $err, $flags) = @_;
369 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getpoll');
370 my $u = $flags->{'u'};
372 # check arguments
373 my $mode = $req->{mode} || 'all';
374 return fail($err, 203, 'mode') unless($mode =~ /enter|results|answers|all/);
376 my $pollid = $req->{pollid} + 0;
377 return fail($err, 200, 'pollid') unless($pollid);
379 # load poll object
380 my $poll = LJ::Poll->new($pollid);
381 return fail($err, 203, 'pollid') unless($poll && $poll->valid);
383 # question id
384 my $pollqid = $req->{'pollqid'} + 0;
386 my $res = {
387 xc3 => {
388 u => $u,
390 pollid => $pollid,
391 ditemid => $poll->ditemid,
392 name => $poll->name,
393 whovote => $poll->whovote,
394 whoview => $poll->whoview,
395 posterid => $poll->posterid,
396 journalid => $poll->journalid,
397 journal => $poll->journal->username,
398 poster => $poll->poster->username,
399 status => ($poll->is_closed ? 'close' : 'open'),
400 can_vote => $poll->can_vote($u),
401 can_view => $poll->can_view($u),
402 is_owner => $poll->is_owner($u),
403 mode => $mode,
406 my $time = $poll->get_time_user_submitted($u);
407 $res->{submitted_time} = $time if ($time);
408 $res->{pollqid} = $pollqid if($pollqid);
410 # Get all questions
411 my @questions = $poll->questions;
413 @questions = grep { $_->pollqid eq $pollqid } @questions if ($pollqid);
414 return fail($err, 203, 'pollqid') unless(@questions);
416 if ($req->{'asxml'}) {
417 my $tidy = LJ::Tidy->new();
419 foreach my $question (@questions) {
420 if ($question->{text}) {
421 $question->{text} = $tidy->clean( $question->{text} );
424 $res->{'name'} = $tidy->clean( $res->{'name'} );
428 # mode to show poll questions
429 if($mode =~ /enter|all/) {
430 # render_enter
431 @{$res->{questions}} = map { $_->get_hash } @questions;
434 if($mode =~ /results|all/) {
435 $poll->load_aggregated_results();
436 $res->{results} = $poll->{results};
439 if($mode =~ /answers|all/ && $poll->can_view($u)) {
440 foreach my $question (@questions) {
441 my @answers = map { my $user = LJ::load_userid($_->{userid});
442 if ($user) {
443 $_->{'username'} = $user->username;
444 if ($user->identity) {
445 my $i = $user->identity;
446 $_->{'identity_type'} = $i->pretty_type;
447 $_->{'identity_value'} = $i->value;
448 $_->{'identity_url'} = $i->url($user);
449 $_->{'identity_display'} = $user->display_name;
453 } map { delete $_->{pollqid}; $_ } $question->answers;
455 @{$res->{answers}{$question->pollqid}} = @answers;
460 return $res;
463 sub editpoll
465 my ($req, $err, $flags) = @_;
466 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'editpoll');
467 my $u = $flags->{'u'};
469 # check arguments
470 my $pollid = $req->{pollid} + 0;
471 return fail($err, 200, 'pollid') unless($pollid);
473 # load poll object
474 my $poll = LJ::Poll->new($pollid);
475 return fail($err, 203, 'pollid') unless($poll && $poll->valid);
477 my $is_super = $poll->prop('supermaintainer');
479 return fail($err, 103, 'xmlrpc.des.maintainer_poll') if($is_super);
481 my $status = $req->{status};
482 return fail($err, 200, 'status') unless($status);
483 return fail($err, 203, 'status') unless($status =~ /open|close/);
485 return fail($err, 103, 'xmlrpc.des.not_poll_owner') unless($poll->is_owner($u));
487 if($status eq 'open') {
488 $poll->open_poll();
489 } elsif ($status eq 'close') {
490 $poll->close_poll();
493 return {
494 pollid => $pollid,
495 status => ($poll->{status} eq 'X' ? 'close' : 'open') ,
496 xc3 => {
497 u => $u,
502 sub votepoll
504 my ($req, $err, $flags) = @_;
505 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'votepoll');
506 my $u = $flags->{'u'}; # remote_id
508 # check pollid
509 my $pollid = $req->{pollid} + 0;
510 return fail($err, 200, 'pollid') unless($pollid);
512 # load poll object
513 my $poll = LJ::Poll->new($pollid);
514 return fail($err, 203, 'pollid') unless($poll && $poll->valid);
516 # check answers parameter
517 my $answers = $req->{answers};
518 return fail($err, 200, 'answers') unless($answers);
519 return fail($err, 203, 'answers') unless(ref $answers eq 'HASH');
521 my @warnings;
522 my $errors;
524 unless (LJ::Poll->process_vote($u, $pollid, $answers, \$errors, \@warnings, wrong_value_as_error => 1)) {
525 return fail($err, 103, $errors);
528 return {
529 pollid => $pollid,
530 journalid => $poll->journalid,
531 posterid => $poll->posterid,
532 journal => $poll->journal->username,
533 poster => $poll->poster->username,
534 xc3 => {
535 u => $u,
540 sub checksession {
541 my ($req, $err, $flags) = @_;
542 return undef
543 unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'checksession');
545 my $u = $flags->{'u'};
547 my $session = $u->session;
549 return {
550 username => $u->username,
551 session => $u->id.":".$session->id.":".$session->auth,
552 caps => $u->caps,
553 usejournals => list_usejournals($u),
554 xc3 => {
555 u => $u
560 sub addcomment {
561 my ($req, $err, $flags) = @_;
563 $flags->{allow_anonymous} = 1;
564 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'addcomment');
566 my $u = $flags->{'u'};
568 return fail($err, 318) if $u && $u->is_readonly;
569 return fail($err, 200, "body") unless($req->{body});
570 return fail($err, 200, "ditemid") unless($req->{ditemid});
571 return fail($err, 200, "journal/journalid") unless($u || $req->{journal} || $req->{journalid});
573 my $journal;
574 if ($req->{journal}) {
575 return fail($err,100) unless LJ::canonical_username($req->{journal});
577 $journal = LJ::load_user($req->{journal}) or return fail($err, 100);
578 return fail($err,226)
579 if LJ::Talk::Post::require_captcha_test($u, $journal, $req->{body}, $req->{ditemid});
581 elsif ( $req->{journalid} ) {
582 $journal = LJ::load_userid($req->{journalid}) or return fail($err, 100);
583 return fail($err,226)
584 if LJ::Talk::Post::require_captcha_test($u, $journal, $req->{body}, $req->{ditemid});
586 else {
587 $journal = $u;
590 return fail($err, 318) if $journal && $journal->is_readonly;
592 # some additional checks
593 return fail($err,214) if LJ::Comment->is_text_spam( \ $req->{body} );
595 my $pk = $req->{prop_picture_keyword} || $req->{picture_keyword};
597 # create
598 my $comment;
599 eval {
600 $comment = LJ::Comment->create(
601 journal => $journal,
602 ditemid => $req->{ditemid},
603 parenttalkid => ($req->{parenttalkid} || int($req->{parent} / 256)),
605 poster => $u, # TODO: to allow poster to be undef
607 body => $req->{body},
608 subject => $req->{subject},
610 props => { picture_keyword => $pk }
614 return fail($err,337) unless $comment;
616 ## Counter "new_comment" for monitoring
617 LJ::run_hook ("update_counter", {
618 counter => "new_comment",
621 # OK
622 return {
623 status => "OK",
624 commentlink => $comment->url,
625 dtalkid => $comment->dtalkid,
626 xc3 => {
627 u => $u,
628 comment => {
629 toplevel => ($comment->parenttalkid == 0 ? 1 : 0),
635 sub getcomments {
636 my ($req, $err, $flags) = @_;
638 $flags->{allow_anonymous} = 1;
640 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getcomments');
641 my $u = $flags->{'u'};
643 my $journal;
644 if($req->{journal}) {
645 return fail($err,100) unless LJ::canonical_username($req->{journal});
646 $journal = LJ::load_user($req->{journal}) or return fail($err, 100);
647 } elsif ( $req->{journalid} ) {
648 $journal = LJ::load_userid($req->{journalid}) or return fail($err, 100);
649 } else {
650 $journal = $u;
653 return fail($err,200,"journal") unless($journal);
654 return fail($err,200,'xmlrpc.des.or', {'first'=>'ditemid', 'second'=>'itemid'}) unless($req->{ditemid} || $req->{itemid});
656 my $itemid = int($req->{ditemid} / 256);
657 $itemid ||= $req->{itemid} + 0;
659 # load root post
660 my $jitem = LJ::Talk::get_journal_item($journal, $itemid);
661 return fail($err,203,'xmlrpc.des.no_post_by_param', {'param'=>'ditemid'}) unless($jitem);
662 my $up = LJ::load_userid( $jitem->{'posterid'} );
664 # check permission to access root post
665 return fail($err,300) unless( LJ::can_view($u, $jitem));
667 my $talkid = int(($req->{dtalkid} + 0)/256); # talkid to load thread
669 my $page_size = $req->{page_size} + 0;
670 $page_size = 500 if($page_size <= 0 || $page_size > 500);
672 my $page = $req->{page} + 0; # page to show - defaut
673 my $view = $req->{view_ditemid} + 0; # ditemid - external comment id to show that page with it
675 my $skip = $req->{skip} + 0;
676 my $itemshow = $req->{itemshow} + 0;
678 my $expand = $req->{expand_strategy} ? $req->{expand_strategy} : 'default' ;
679 return fail($err, 203, 'expand_strategy') unless ($expand =~ /^mobile|mobile_thread|expand_all|by_level|detailed|default$/);
681 my $format = $req->{format} || 'thread'; # default value thread
682 return fail($err, 203, 'format') unless($format =~ /^thread|list$/ );
684 my $expand_all = 0;
685 if( $expand eq 'mobile_thread' || $expand eq 'expand_all'){
686 undef $expand;
687 $expand_all = 1;
690 my $expand_child;
691 my $expand_level;
692 if ($expand eq 'mobile') {
693 $expand_child = $req->{expand_child} + 0;
694 $expand_child = 3 if $expand_child > 500 || $expand_child <= 0
695 } elsif ($expand eq 'by_level') {
696 $expand_level = ($req->{expand_level} ? $req->{expand_level} + 0 : 1);
697 $expand_level = 1 if $expand_level > 128 || $expand_level < 0;
700 my $opts = {
701 page => $page, # page to get
702 view => $view,
703 expand_level => $expand_level+1,
704 expand_child => $expand_child,
705 expand_all => $expand_all,
706 init_comobj => 0, # do not init LJ::Comment objects in the function
707 up => $up, # author of root post
708 page_size => $page_size, # max comments returned per call!
709 strict_page_size => 1, # fix page size, do not extent it in case of less comments
712 # optional parameters
713 $opts->{thread} = $talkid if $talkid;
714 $opts->{expand_strategy} = $expand unless($expand eq 'default');
716 my @com = LJ::Talk::load_comments($journal, $u, "L", $itemid, $opts);
718 my %extra;
719 $extra{topitems} = $opts->{out_items};
720 $extra{topitem_first} = $opts->{out_itemfirst};
721 $extra{topitem_last} = $opts->{out_itemlast};
722 $extra{page_size} = $opts->{out_pagesize};
723 $extra{pages} = $opts->{out_pages};
724 $extra{page} = $opts->{out_page};
725 $extra{ditemid} = $jitem->{ditemid};
727 my @comments;
728 my @parent = ( \{ level => -1, children => \@comments } );
730 while (my $item = shift @com){
731 $item->{indent} ||= 0;
732 shift( @parent ) while $item->{indent} <= ${$parent[0]}->{level};
734 my $item_data = {
735 parentdtalkid => $item->{parenttalkid}?($item->{parenttalkid} * 256 + $jitem->{anum}):0,
736 postername => $item->{userpost},
737 level => $item->{indent},
738 posterid => $item->{posterid},
739 datepostunix => $item->{datepost_unix},
740 datepost => $item->{datepost},
741 dtalkid => $item->{talkid} * 256 + $jitem->{anum},
742 state => $item->{state},
743 is_show => $item->{_show},
744 is_loaded => ($item->{_loaded} ? 1 : 0),
747 unless($u || $item->{_show}) {
748 delete $item_data->{postername};
749 delete $item_data->{posterid};
750 delete $item_data->{datepost_unix};
751 delete $item_data->{datepost};
754 $item_data->{body} = $item->{body} if($item->{body} && $item->{_loaded});
756 if ($req->{get_users_info}){
757 LJ::EmbedModule->expand_lj_user(\$item_data->{body});
760 if ($req->{'asxml'}) {
761 my $tidy = LJ::Tidy->new();
762 $item_data->{body} = $tidy->clean( $item_data->{body} );
765 # add parameters to lj-embed
766 #LJ::EmbedModule->expand_entry($item->{upost}, \$item_data->{body}, get_video_id => 1) if($item->{upost} && $req->{get_video_ids});
768 $item_data->{subject} = $item->{subject} if($item->{subject} && $item->{_loaded});
770 if($item->{upost} && $item->{upost}->identity ){
771 my $i = $item->{upost}->identity;
772 $item_data->{'identity_type'} = $i->pretty_type;
773 $item_data->{'identity_value'} = $i->value;
774 $item_data->{'identity_url'} = $i->url($item->{upost});
775 $item_data->{'identity_display'} = $item->{upost}->display_name;
778 if ($item->{'_loaded'} && $req->{extra}) {
779 my $comment = LJ::Comment->new($journal, dtalkid => $item_data->{dtalkid});
781 my $userpic = $comment->userpic;
782 $item_data->{userpic} = $userpic && $userpic->url; # left here forawhile
783 $item_data->{poster_userpic_url} = $item_data->{userpic};
785 $item_data->{props} = { map {$item->{props}->{$_} ? ($_ => $item->{props}->{$_}) : ()}
786 qw(edit_time deleted_poster picture_keyword opt_preformatted) };
788 $item_data->{props}->{'poster_ip'} = $item->{'props'}->{'poster_ip'}
789 if $item->{'props'}->{'poster_ip'} && $u && ( $u->{'user'} eq $up->{'user'} || $u->can_manage($journal) );
791 $item_data->{privileges} = {};
792 $item_data->{privileges}->{'delete'} = LJ::Talk::can_delete($u, $journal, $up, $item->{userpost});
793 $item_data->{privileges}->{'edit'} = $comment->user_can_edit($u);
794 $item_data->{privileges}->{'freeze'} = (!$comment->is_frozen && LJ::Talk::can_freeze($u, $journal, $up, $item->{userpost}));
795 $item_data->{privileges}->{'unfreeze'} = ($comment->is_frozen && LJ::Talk::can_unfreeze($u, $journal, $up, $item->{userpost}));
796 my $pu = $comment->poster;
797 unless ($pu && $pu->is_suspended){
798 $item_data->{privileges}->{'screen'} = (!$comment->is_screened && LJ::Talk::can_screen($u, $journal, $up, $item->{userpost}));
799 $item_data->{privileges}->{'unscreen'} = ($comment->is_screened && LJ::Talk::can_unscreen($u, $journal, $up, $item->{userpost}));
801 $item_data->{privileges}->{'spam'} = (!$comment->is_spam && LJ::Talk::can_marked_as_spam($u, $journal, $up, $item->{userpost}));
802 $item_data->{privileges}->{'unspam'} = ($comment->is_spam && LJ::Talk::can_unmark_spam($u, $journal, $up, $item->{userpost}));
803 $item_data->{privileges}->{'reply'} = (LJ::Talk::Post::require_captcha_test($u, $journal, '', $req->{ditemid}) ? 0 : 1);
806 if ( $req->{calculate_count} ){
807 $item_data->{thread_count} = 0;
808 $$_->{thread_count}++ for @parent;
811 if( $req->{only_loaded} && !$item->{_loaded} ){
812 my $hc = \${$parent[0]}->{has_children};
813 $$hc = $$hc?$$hc+1:1;
814 next unless $req->{calculate_count};
815 }elsif( $format eq 'list' ){ # list or thread
816 push @comments, $item_data;
817 }else{
818 ${$parent[0]}->{children} = [] unless ${$parent[0]}->{children};
819 push @{${$parent[0]}->{children}}, $item_data;
822 my $children = $item->{children};
823 if($children && @$children){
824 $_->{indent} = $item->{indent} + 1 for @$children;
825 unshift @com, @$children;
826 unshift @parent, \$item_data;
827 undef $item->{children};
831 if($format eq 'list') {
832 $extra{items} = LJ::Talk::get_replycount($journal, $jitem->{jitemid});
833 $itemshow = $extra{items} unless ($itemshow && $itemshow <= $extra{items});
834 @comments = splice(@comments, $skip, $itemshow);
835 $extra{skip} = $skip;
836 $extra{itemshow} = $itemshow;
839 return {
840 comments => \@comments,
841 %extra,
842 xc3 => {
843 u => $flags->{'u'}
848 =head deletecomments
849 Delete specified comment, comments or thread(s) of comments in specified journal that current use
850 Parameters:
851 journal/journalid or current user's journal
852 dtalkid/dtalkids - ids of current
853 thread - bool
855 =cut
856 sub deletecomments {
857 my ($req, $err, $flags) = @_;
858 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'deletecomments');
860 my $u = $flags->{'u'};
861 my $journal;
862 if($req->{journal}) {
863 return fail($err,100) unless LJ::canonical_username($req->{journal});
864 $journal = LJ::load_user($req->{journal}) or return fail($err, 100);
865 } elsif($req->{journalid}) {
866 $journal = LJ::load_userid($req->{journalid}) or return fail($err, 100);
867 } else {
868 $journal = $u;
871 return fail($err, 200, 'xmlrpc.des.or', {'first'=>'dtalkid','second'=>'dtalkids'}) unless($req->{dtalkid} || $req->{dtalkids});
872 my @ids;
873 if ($req->{dtalkids}) {
874 foreach my $num (split(/\s*,\s*/, $req->{'dtalkids'})) {
875 return fail($err, 203, 'xmlrpc.des.non_arifmetic', {'param'=>'dtalkid','value'=>$num}) unless $num =~ /^\d+$/;
876 push @ids, $num;
878 } else {
879 my $num = $req->{dtalkid};
880 return fail($err, 203, 'xmlrpc.des.non_arifmetic', {'param'=>'dtalkid','value'=>$num}) unless $num =~ /^\d+$/;
881 push @ids, $num;
884 my $can_manage = $u->can_manage($journal);
886 my (%to_delauthor, %to_ban, %to_mark_spam);
888 my @comments;
889 foreach my $id (@ids) {
890 my $comm = LJ::Comment->new($journal, dtalkid => $id);
891 return fail($err, 203, 'xmlrpc.des.no_comment_by_param',{'param'=>'dtalkid'}) unless $comm && ($comm->dtalkid == $id);
892 return fail($err, 327, 'dtalkid:'.$comm->dtalkid) if $comm->is_deleted;
893 return fail($err, 326, 'dtalkid:'.$comm->dtalkid) unless $comm->user_can_delete($u);
895 if($req->{'delauthor'}) {
897 # they can delete all comments posted by the same author
898 # if they are the entry author, and the comment being deleted
899 # has not been posted anonymously
900 my $can_delauthor = $comm->poster && ( $can_manage || ( $u->userid == $comm->entry->poster->userid ) );
901 return fail($err, 328, 'dtalkid:'.$comm->dtalkid) unless $can_delauthor;
902 $to_delauthor{$comm->entry->jitemid}->{$comm->poster->userid} = 1;
905 if($req->{'ban'}) {
907 # they can ban the comment author if they are the journal owner
908 # and there is an author; also, they will not be able to ban
909 # themselves
910 my $can_sweep = ( $u && $comm->poster && $u->can_sweep($journal) );
911 my $can_ban = ( $can_manage || $can_sweep ) && $comm->poster && ( $u->userid != $comm->poster->userid );
912 return fail($err, 329, 'dtalkid:'.$comm->dtalkid) unless $can_ban;
913 $to_ban{$comm->poster->userid} = $comm->poster;
916 if($req->{'spam'}) {
918 # they can mark as spam unless the comment is their own;
919 # they don't need to be the community maintainer to do that
920 my $can_mark_spam = LJ::Talk::can_mark_spam($u, $journal, $comm->poster, $comm)
921 && $comm->poster && ( $u->userid != $comm->poster->userid );
922 return fail($err, 330, 'dtalkid:'.$comm->dtalkid) unless $can_mark_spam;
923 $to_mark_spam{$comm->jtalkid} = $comm;
926 push @comments, $comm;
929 my @to_delete;
930 if(!$req->{thread}){
931 push @to_delete, @comments;
932 } else {
933 my %map_delete;
934 foreach my $comment (@comments) {
935 my @comment_tree = $comment->entry->comment_list;
936 my @children = ($comment);
937 while(my $item = shift @children){
938 return fail($err, 326, 'xmlrpc.des.foreign_comment', {'dtalkid'=>$item->dtalkid}) unless $item->user_can_delete($u);
939 $map_delete{$item->dtalkid} = $item unless $item->is_deleted;
940 push @children, grep { $_->{parenttalkid} == $item->{jtalkid} } @comment_tree;
943 push @to_delete, values %map_delete;
946 # delete all comments
947 $_->delete for @to_delete;
949 # delete author comments (only for authors of root comment in thread)
950 foreach my $jitemid (keys %to_delauthor) {
951 foreach my $userid (keys %{$to_delauthor{$jitemid}}) {
952 LJ::Talk::delete_author( $journal, $jitemid, $userid );
956 # ban authors (only for authors of root comment in thread)
957 $journal->ban_user($_) for values %to_ban;
959 # mark comments as spam (only for root comment in thread)
960 foreach my $comment (values %to_mark_spam) {
961 my $poster = $comment->poster;
962 LJ::Talk::mark_comment_as_spam( $journal, $comment->jtalkid );
963 LJ::set_rel($journal, $poster, 'D');
965 LJ::User::UserlogRecord::SpamSet->create( $journal,
966 'spammerid' => $poster->userid, 'remote' => $u );
968 LJ::run_hook('auto_suspender_for_spam', $poster->{userid});
971 return {
972 status => 'OK',
973 result => @to_delete + 0,
974 dtalkids => [ map {$_->dtalkid} @to_delete ],
975 xc3 => {
976 u => $u,
981 =head updatecomments
982 Use that function to update comments statuses:
983 single or multiple
984 complete thread or root ony
985 =cut
987 sub updatecomments {
988 my ($req, $err, $flags) = @_;
989 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'updatecomments');
991 my $u = $flags->{'u'};
992 my $journal;
993 if($req->{journal}) {
994 return fail($err,100) unless LJ::canonical_username($req->{journal});
995 $journal = LJ::load_user($req->{journal}) or return fail($err, 100);
996 } elsif($req->{journalid}) {
997 $journal = LJ::load_userid($req->{journalid}) or return fail($err, 100);
998 } else {
999 $journal = $u;
1002 return fail($err, 200, 'xmlrpc.des.or',{'first'=>'dtalkid','second'=>'dtalkids'}) unless($req->{dtalkid} || $req->{dtalkids});
1004 my @ids;
1005 if ($req->{dtalkids}) {
1006 foreach my $num (split(/\s*,\s*/, $req->{'dtalkids'})) {
1007 return fail($err, 203, 'xmlrpc.des.non_arifmetic', {'param'=>'dtalkid', 'value'=>$num}) unless $num =~ /^\d+$/;
1008 push @ids, $num;
1010 } else {
1011 my $num = $req->{dtalkid};
1012 return fail($err, 203, 'xmlrpc.des.non_arifmetic', {'param'=>'dtalkid', 'value'=>$num}) unless $num =~ /^\d+$/;
1013 push @ids, $num;
1016 my $action = $req->{action};
1017 return fail($err, 200, "action") unless($action);
1018 return fail($err, 203, "action") unless($action =~ /^screen|unscreen|freeze|unfreeze|unspam$/);
1020 my $can_method = ($action =~ /unspam/ ? "LJ::Talk::can_unmark_spam" : "LJ::Talk::can_$action");
1021 $can_method = \&{$can_method};
1023 my @comments;
1024 foreach my $id (@ids) {
1025 my $comm = LJ::Comment->new($journal, dtalkid => $id);
1026 return fail($err, 203, 'xmlrpc.des.no_comment_by_param',{'param'=>'dtalkid'}) unless $comm && ($comm->dtalkid == $id);
1027 return fail($err, 327, 'dtalkid:'.$comm->dtalkid) if $comm->is_deleted;
1028 return fail($err, 326, 'dtalkid:'.$comm->dtalkid) unless $can_method->($u, $journal, $comm->entry->poster, $comm->poster);
1029 push @comments, $comm;
1032 # get first entry
1033 my $jitemid = $comments[0]->entry->jitemid;
1035 # get list of comments to process
1036 my @to_update;
1037 if(!$req->{thread} || $action =~ /freeze|unfreeze/) {
1038 push @to_update, @comments;
1039 } else { # get all elements from threads
1040 my %map_update;
1041 foreach my $comment (@comments) {
1042 my @comment_tree = $comment->entry->comment_list;
1043 my @children = ($comment);
1044 while(my $item = shift @children){
1045 next if $item->is_deleted;
1046 return fail($err, 326, 'dtalkid:'.$item->dtalkid) unless $can_method->($u, $journal, $item->entry->poster, $item->poster);
1047 $map_update{$item->dtalkid} = $item;
1048 push @children, grep { $_->{parenttalkid} == $item->{jtalkid} } @comment_tree;
1051 push @to_update, values %map_update;
1054 # process comments
1055 my $method;
1056 if ($action =~ /screen|unscreen|unspam/) {
1057 $method = \&{"LJ::Talk::$action".'_comment'};
1058 $method->($journal, $jitemid, map { $_->{jtalkid} } @to_update);
1059 } elsif ($action =~ /freeze|unfreeze/) {
1060 $method = \&{"LJ::Talk::$action".'_thread'};
1061 $method->($journal, $jitemid, map { $_->{jtalkid} } @to_update);
1064 return {
1065 status => 'OK',
1066 result => @to_update + 0,
1067 dtalkids => [ map {$_->dtalkid} @to_update ],
1068 xc3 => {
1069 u => $u,
1074 sub screencomments {
1075 my ($req, $err, $flags) = @_;
1076 return undef unless authenticate($req, $err, $flags);
1078 my $journal = $req->{journalid}?LJ::load_userid($req->{journalid}):$flags->{'u'};
1079 my $comment = LJ::Comment->new( $journal , dtalkid => $req->{dtalkid} );
1080 my $up = $comment->entry->poster;
1081 return fail($err, 300) unless LJ::Talk::can_screen($flags->{'u'}, $journal, $up, $comment->poster);
1083 my @to_screen;
1085 if( !$req->{recursive}){
1086 push @to_screen, $comment;
1087 }else{
1088 my @comment_tree = $comment->entry->comment_list;
1089 my @children = ($comment);
1090 while(my $item = shift @children){
1091 return fail($err, 300) unless LJ::Talk::can_screen($flags->{'u'}, $journal, $up, $item->poster);
1092 push @to_screen, $item;
1093 push @children, grep { $_->{parenttalkid} == $item->{jtalkid} } @comment_tree;
1096 LJ::Talk::screen_comment($journal, $comment->entry->jitemid, $_->{jtalkid}) for @to_screen;
1098 return {
1099 status => 'OK',
1100 result => @to_screen + 0,
1101 xc3 => {
1102 u => $flags->{'u'}
1107 sub unscreencomments {
1108 my ($req, $err, $flags) = @_;
1109 return undef unless authenticate($req, $err, $flags);
1111 my $journal = $req->{journalid}?LJ::load_userid($req->{journalid}):$flags->{'u'};
1112 my $comment = LJ::Comment->new( $journal , dtalkid => $req->{dtalkid} );
1113 my $up = $comment->entry->poster;
1114 return fail($err, 300) unless LJ::Talk::can_unscreen($flags->{'u'}, $journal, $up, $comment->poster);
1116 my @to_screen;
1118 if( !$req->{recursive}){
1119 push @to_screen, $comment;
1120 }else{
1121 my @comment_tree = $comment->entry->comment_list;
1122 my @children = ($comment);
1123 while(my $item = shift @children){
1124 return fail($err, 300) unless LJ::Talk::can_unscreen($flags->{'u'}, $journal, $up, $item->poster);
1125 push @to_screen, $item;
1126 push @children, grep { $_->{parenttalkid} == $item->{jtalkid} } @comment_tree;
1129 LJ::Talk::unscreen_comment($journal, $comment->entry->jitemid, $_->{jtalkid}) for @to_screen;
1131 return {
1132 status => 'OK',
1133 result => @to_screen + 0,
1134 xc3 => {
1135 u => $flags->{'u'}
1140 sub freezecomments {
1141 my ($req, $err, $flags) = @_;
1142 return undef unless authenticate($req, $err, $flags);
1144 my $journal = $req->{journalid}?LJ::load_userid($req->{journalid}):$flags->{'u'};
1145 my $comment = LJ::Comment->new( $journal , dtalkid => $req->{dtalkid} );
1146 my $up = $comment->entry->poster;
1147 return fail($err, 300) unless LJ::Talk::can_freeze($flags->{'u'}, $journal, $up, $comment->poster);
1149 LJ::Talk::freeze_thread($journal, $comment->entry->jitemid, $comment->{jtalkid});
1151 return {
1152 status => 'OK',
1153 result => 1,
1154 xc3 => {
1155 u => $flags->{'u'}
1160 sub unfreezecomments {
1161 my ($req, $err, $flags) = @_;
1162 return undef unless authenticate($req, $err, $flags);
1164 my $journal = $req->{journalid}?LJ::load_userid($req->{journalid}):$flags->{'u'};
1165 my $comment = LJ::Comment->new( $journal , dtalkid => $req->{dtalkid} );
1166 my $up = $comment->entry->poster;
1167 return fail($err, 300) unless LJ::Talk::can_unfreeze($flags->{'u'}, $journal, $up, $comment->poster);
1169 LJ::Talk::unfreeze_thread($journal, $comment->entry->jitemid, $comment->{jtalkid});
1171 return {
1172 status => 'OK',
1173 result => 1,
1174 xc3 => {
1175 u => $flags->{'u'}
1180 =head editcomment
1181 Edit one single comment, just content.
1182 To change statuses use other API functions.
1183 =cut
1184 sub editcomment {
1185 my ($req, $err, $flags) = @_;
1186 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'editcomment');
1188 my $remote = $flags->{'u'};
1189 return fail($err, 318) if $remote && $remote->is_readonly;
1191 my $journal;
1192 if($req->{journal}) {
1193 return fail($err, 100) unless LJ::canonical_username($req->{journal});
1194 $journal = LJ::load_user($req->{journal}) or return fail($err, 100);
1195 } elsif($req->{journalid}) {
1196 $journal = LJ::load_userid($req->{journalid}) or return fail($err, 100);
1197 } else {
1198 $journal = $remote;
1200 return fail($err, 319) if $journal && $journal->is_readonly;
1202 return fail($err, 200, "dtalkid") unless($req->{dtalkid});
1204 my $comment = LJ::Comment->new($journal, dtalkid => $req->{dtalkid});
1205 return fail($err, 203, 'xmlrpc.des.and', {first=>'dtalkid',second=>'journal(id)'}) unless $comment;
1207 my $entry = $comment->entry;
1208 return fail($err, 203, 'xmlrpc.des.and', {first=>'dtalkid',second=>'journal(id)'}) unless $entry;;
1209 return fail($err, 323) if $entry && $entry->is_suspended;
1211 my $up = $entry->poster;
1212 my $parent = $comment->parent;
1213 return fail($err, 324) if $parent && $parent->{state} eq 'F';
1215 my $new_comment = { map {$_ => $req->{$_}} grep { defined $req->{$_} } qw( picture_keyword preformat subject body) };
1216 $new_comment->{editid} = $req->{dtalkid};
1217 $new_comment->{body} = $comment->body_orig() unless($new_comment->{body}); # body can't be empty!
1218 $new_comment->{subject} = $comment->subject_orig() unless(defined $new_comment->{subject});
1219 $new_comment->{preformat} = $comment->prop('opt_preformatted') unless(defined $new_comment->{preformat});
1221 my $errref;
1222 return fail($err, 325, $errref)
1223 unless LJ::Talk::Post::edit_comment(
1224 $up, $journal, $new_comment, $parent, {itemid => $entry->jitemid}, \$errref, $remote);
1226 return {
1227 status => 'OK',
1228 commentlink => $comment->url,
1229 dtalkid => $comment->dtalkid,
1230 xc3 => {
1231 u => $flags->{'u'}
1236 sub getrecentcomments {
1237 my ($req, $err, $flags) = @_;
1238 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getrecentcomments');
1239 my $u = $flags->{'u'};
1241 my $journal;
1242 if($req->{journal}) {
1243 return fail($err,100) unless LJ::canonical_username($req->{journal});
1244 $journal = LJ::load_user($req->{journal}) or return fail($err, 100);
1245 } elsif ( $req->{journalid} ) {
1246 $journal = LJ::load_userid($req->{journalid}) or return fail($err, 100);
1247 } else {
1248 $journal = $u;
1250 return fail($err,200,"journal") unless($journal);
1252 my $count = $req->{itemshow};
1253 $count = 10 if !$count || ($count > 100) || ($count < 0);
1255 my @recv = $journal->get_recent_talkitems($count, remote => $u);
1256 my @recv_talkids = map { $_->{'jtalkid'} } @recv;
1257 my %recv_userids = map { $_->{'posterid'} => 1} @recv;
1258 my $comment_text = LJ::get_talktext2($journal, @recv_talkids);
1259 my $users = LJ::load_userids(keys(%recv_userids));
1260 foreach my $comment ( @recv ) {
1261 $comment->{subject} = $comment_text->{$comment->{jtalkid}}[0];
1262 $comment->{text} = $comment_text->{$comment->{jtalkid}}[1];
1264 $comment->{text} = LJ::trim_widgets(
1265 length => $req->{trim_widgets},
1266 img_length => $req->{widgets_img_length},
1267 text => $comment->{text},
1268 read_more => '<a href="' . $comment->url . '"> ...</a>',
1269 ) if $req->{trim_widgets};
1271 if ($req->{'asxml'} && $comment->{text}) {
1272 my $tidy = LJ::Tidy->new();
1273 $comment->{text} = $tidy->clean( $comment->{text} );
1276 # add parameters to lj-tags
1277 #LJ::EmbedModule->expand_entry($users->{$comment->{posterid}}, \$comment->{text}, get_video_id => 1) if($req->{get_video_ids});
1279 if ($req->{view}) {
1280 LJ::EmbedModule->expand_entry($users->{$comment->{posterid}}, \$comment->{text}, edit => 1) if $req->{view} eq 'stored';
1281 } elsif ($req->{parseljtags}) {
1282 $comment->{text} = LJ::convert_lj_tags_to_links(
1283 event => $comment->{text},
1284 embed_url => $comment->url );
1287 my $poster = $users->{$comment->{posterid}};
1288 $comment->{postername} = $poster->username if $poster;
1290 if ($poster && $poster->identity ) {
1291 my $i = $poster->identity;
1292 $comment->{'identity_type'} = $i->pretty_type;
1293 $comment->{'identity_value'} = $i->value;
1294 $comment->{'identity_url'} = $i->url($poster);
1295 $comment->{'identity_display'} = $poster->display_name;
1298 my $comm_obj = LJ::Comment->new($journal, jtalkid => $comment->{jtalkid});
1299 my $userpic = $comm_obj->userpic;
1300 $comment->{poster_userpic_url} = $userpic && $userpic->url;
1304 return {
1305 status => 'OK',
1306 comments => [ @recv ],
1307 xc3 => {
1308 u => $u
1313 sub getfriendspage
1315 my ($req, $err, $flags) = @_;
1316 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getfriendspage');
1317 my $u = $flags->{'u'};
1319 my $itemshow = (defined $req->{itemshow}) ? $req->{itemshow} : 100;
1320 return fail($err, 209, 'xmlrpc.des.bad_value', {'param'=>'itemshow'}) if $itemshow ne int($itemshow ) or $itemshow <= 0 or $itemshow > 100;
1321 my $skip = (defined $req->{skip}) ? $req->{skip} : 0;
1322 return fail($err, 209, 'xmlrpc.des.bad_value', {'param'=>'skip'}) if $skip ne int($skip ) or $skip < 0 or $skip > 100;
1324 my $lastsync = int $req->{lastsync};
1325 my $before = int $req->{before};
1326 my $before_count = 0;
1327 my $before_skip = 0;
1328 my $is_only_count = $req->{only_count};
1329 if ($before){
1330 $before_skip = $skip + 0;
1331 $skip = 0;
1334 my %get_params = (
1335 u => $u,
1336 userid => $u->{'userid'},
1337 remote => $u,
1338 dateformat => 'S2',
1339 itemshow => $itemshow,
1340 filter => $req->{groupmask},
1341 showtypes => $req->{journaltype},
1344 my @entries = LJ::get_friend_items({
1345 %get_params,
1346 'skip' => $skip,
1347 'filter_by_tags' => 1,
1350 if ( $lastsync && $is_only_count ) {
1351 return {
1352 count => scalar grep { $LJ::EndOfTime - $_->{rlogtime} >= $lastsync } @entries,
1356 my @attrs = qw/subject_raw event_raw journalid posterid ditemid security reply_count userpic props security/;
1358 my @uids;
1360 my @res = ();
1361 while (my $ei = shift @entries) {
1363 next unless $ei;
1365 # exit cycle if maximum friend items limit reached
1366 last
1367 if scalar @res >= FRIEND_ITEMS_LIMIT;
1369 # if passed lastsync argument - skip items with logtime less than lastsync
1370 if($lastsync) {
1371 next
1372 if $LJ::EndOfTime - $ei->{rlogtime} <= $lastsync;
1375 if($before) {
1376 last if @res >= $itemshow;
1377 push @entries, LJ::get_friend_items({
1378 %get_params,
1379 'skip' => $skip + ($before_count += $itemshow),
1380 'filter_by_tags' => 1,
1381 }) unless @entries;
1382 next if $LJ::EndOfTime - $ei->{rlogtime} > $before;
1383 next if $before_skip-- > 0;
1386 my $entry = LJ::Entry->new_from_item_hash($ei);
1387 next unless $entry;
1388 next unless $entry->visible_to($u);
1390 # event result data structure
1391 my %h = ();
1393 my $repost_props = { use_repost_signature => 0 };
1394 my ($original_entry, $repost_entry, $event_raw);
1395 my $opts = {original_post_obj => \$original_entry, repost_obj => \$repost_entry, event => \$event_raw};
1396 if (LJ::Entry::Repost->substitute_content( $entry, $opts, $repost_props )) {
1397 $h{repost} = 1;
1398 $entry = $original_entry;
1401 $entry->normalize_props() unless $flags->{'noauth'};
1403 # Add more data for public posts
1404 foreach my $method (@attrs) {
1405 $h{$method} = $entry->$method;
1408 if($h{repost}){
1409 $h{event_raw} = $event_raw;
1410 $h{original_entry_url} = $original_entry->url;
1411 $h{repostername} = $repost_entry->journal->username;
1412 $h{postername} = $original_entry->poster->username;
1413 $h{journalname} = $entry->journal->username;
1414 my $userpic = $original_entry->userpic;
1415 $h{poster_userpic_url} = $userpic && $userpic->url;
1419 $h{event_raw} = LJ::trim_widgets(
1420 length => $req->{trim_widgets},
1421 img_length => $req->{widgets_img_length},
1422 text => $h{event_raw},
1423 read_more => '<a href="' . $entry->url . '"> ...</a>',
1424 ) if $req->{trim_widgets};
1426 LJ::EmbedModule->expand_entry($entry->poster, \$h{event_raw}, get_video_id => 1) if $req->{get_video_ids};
1428 if ( $req->{get_polls} ) {
1429 my @polls_id = LJ::Poll->expand_entry(\$h{event_raw}, getpolls => 1, viewer => $u );
1431 foreach (@polls_id) {
1432 my $poll = LJ::Poll->new($_);
1434 next unless $poll->can_view($u);
1435 push @{$h{'polls'}->{$_}}, { $poll->aggr_results() };
1439 if ($req->{get_users_info}){
1440 LJ::EmbedModule->expand_lj_user(\$h{event_raw});
1443 if ($req->{view}) {
1444 LJ::EmbedModule->expand_entry($entry->poster, \$h{event_raw}, edit => 1) if $req->{view} eq 'stored';
1445 } elsif ($req->{parseljtags}) {
1446 $h{event_raw} = LJ::convert_lj_tags_to_links(
1447 event => $h{event_raw},
1448 embed_url => $entry->url)
1451 if ($req->{'asxml'}) {
1452 my $tidy = LJ::Tidy->new();
1453 $h{event_raw} = $tidy->clean( $h{event_raw} );
1456 #userpic
1457 $h{poster_userpic_url} = $h{userpic} && $h{userpic}->url;
1459 # log time value
1460 $h{logtime} = $LJ::EndOfTime - $ei->{rlogtime};
1461 $h{do_captcha} = LJ::Talk::Post::require_captcha_test($u, $entry->poster, '', $h{ditemid}, 1)?1:0;
1463 push @res, \%h;
1465 push @uids, $h{posterid}, $h{journalid};
1468 my $users = LJ::load_userids(@uids);
1470 foreach (@res) {
1471 $_->{journalname} = $users->{ $_->{journalid} }->{'user'};
1472 $_->{journaltype} = $users->{ $_->{journalid} }->{'journaltype'};
1473 $_->{journalurl} = $users->{ $_->{journalid} }->journal_base;
1474 delete $_->{journalid};
1475 $_->{postername} = $users->{ $_->{posterid} }->{'user'};
1476 $_->{postertype} = $users->{ $_->{posterid} }->{'journaltype'};
1477 $_->{posterurl} = $users->{ $_->{posterid} }->journal_base;
1478 if ($users->{ $_->{posterid} }->identity) {
1479 my $i = $users->{ $_->{posterid} }->identity;
1480 $_->{'identity_type'} = $i->pretty_type;
1481 $_->{'identity_value'} = $i->value;
1482 $_->{'identity_url'} = $i->url($users->{ $_->{posterid} });
1483 $_->{'identity_display'} = $users->{ $_->{posterid} }->display_name;
1485 delete $_->{posterid};
1486 delete $_->{props}->{repost_offer};
1489 LJ::run_hooks("getfriendspage", {userid => $u->userid, });
1491 return {
1492 entries => [ @res ],
1493 skip => $skip,
1494 xc3 => {
1495 u => $u
1501 sub getinbox
1503 my ($req, $err, $flags) = @_;
1504 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getinbox');
1505 my $u = $flags->{'u'};
1507 my $itemshow = (defined $req->{itemshow}) ? $req->{itemshow} : 100;
1508 return fail($err, 209, 'xmlrpc.des.bad_value', {'param'=>'itemshow'}) if $itemshow ne int($itemshow ) or $itemshow <= 0 or $itemshow > 100;
1509 my $skip = (defined $req->{skip}) ? $req->{skip} : 0;
1510 return fail($err, 209, 'xmlrpc.des.bad_value', {'param'=>'skip'}) if $skip ne int($skip ) or $skip < 0 or $skip > 100;
1512 # get the user's inbox
1513 my $inbox = $u->notification_inbox or return fail($err, 500, 'xmlrpc.des.inbox_fail');
1515 my %type_number = (
1516 Befriended => 1,
1517 Birthday => 2,
1518 CommunityInvite => 3,
1519 CommunityJoinApprove => 4,
1520 CommunityJoinReject => 5,
1521 CommunityJoinRequest => 6,
1522 Defriended => 7,
1523 InvitedFriendJoins => 8,
1524 JournalNewComment => 9,
1525 JournalNewEntry => 10,
1526 NewUserpic => 11,
1527 NewVGift => 12,
1528 OfficialPost => 13,
1529 PermSale => 14,
1530 PollVote => 15,
1531 SupOfficialPost => 16,
1532 UserExpunged => 17,
1533 UserMessageRecvd => 18,
1534 UserMessageSent => 19,
1535 UserNewComment => 20,
1536 UserNewEntry => 21,
1537 CommentReply => 22,
1539 my %number_type = reverse %type_number;
1541 my @notifications;
1543 my $sync_date;
1544 # check lastsync for valid date
1545 if ($req->{'lastsync'}) {
1546 $sync_date = int $req->{'lastsync'};
1547 return fail($err,203,'xmlrpc.des.date_unixtime',{'param'=>'syncitems'}) if $sync_date <= 0;
1550 my $before;
1551 # check before for valid date
1552 if ($req->{'before'}) {
1553 $before = int $req->{'before'};
1554 return fail($err,203,'xmlrpc.des.date_unixtime',{'param'=>'syncitems'}) if $before <= 0;
1557 if ($req->{gettype}) {
1558 $req->{gettype} = [$req->{gettype}] unless ref($req->{gettype});
1560 my %filter;
1561 $filter{"LJ::Event::" . $number_type{$_}} = 1 for @{$req->{gettype}};
1562 @notifications = grep { exists $filter{$_->event->class} } $inbox->items;
1564 } else {
1565 @notifications = $inbox->all_items;
1568 # By default, notifications are sorted as "oldest are the first"
1569 # Reverse it by "newest are the first"
1570 @notifications = reverse @notifications;
1572 @notifications = grep {
1573 (!$before || $_->when_unixtime <= $before) &&
1574 (!$sync_date || $_->when_unixtime >= $sync_date)
1575 } @notifications;
1577 $itemshow = scalar @notifications - $skip if scalar @notifications < $skip + $itemshow;
1579 my @res;
1580 foreach my $item (@notifications[$skip .. $itemshow + $skip - 1]) {
1581 my $raw = $item->event->raw_info($u, {extended => $req->{extended}});
1583 my $type_index = $type_number{$raw->{type}};
1584 if (defined $type_index) {
1585 $raw->{type} = $type_index;
1586 } else {
1587 $raw->{typename} = $raw->{type};
1588 $raw->{type} = 0;
1591 $raw->{state} = $item->{state};
1593 push @res, { %$raw,
1594 when => $item->when_unixtime,
1595 qid => $item->qid,
1599 return {
1600 'skip' => $skip,
1601 'items' => \@res,
1602 'login' => $u->user,
1603 'journaltype' => $u->journaltype,
1604 xc3 => {
1605 u => $u
1610 sub setmessageread {
1611 my ($req, $err, $flags) = @_;
1613 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'setmessageread');
1615 my $u = $flags->{'u'};
1617 # get the user's inbox
1618 my $inbox = $u->notification_inbox or return fail($err, 500, 'xmlrpc.des.inbox_fail');
1619 my @result;
1621 # passing requested ids for loading
1622 my @notifications = $inbox->all_items;
1624 # Try to select messages by qid if specified
1625 my @qids = @{$req->{qid}};
1626 if (scalar @qids) {
1627 foreach my $qid (@qids) {
1628 my $item = eval {LJ::NotificationItem->new($u, $qid)};
1629 $item->mark_read if $item;
1630 push @result, { qid => $qid, result => 'set read' };
1632 } else { # Else select it by msgid for back compatibility
1633 # make hash of requested message ids
1634 my %requested_items = map { $_ => 1 } @{$req->{messageid}};
1636 # proccessing only requested ids
1637 foreach my $item (@notifications) {
1638 my $msgid = $item->event->raw_info($u)->{msgid};
1639 next unless $requested_items{$msgid};
1640 # if message already read -
1641 if ($item->{state} eq 'R') {
1642 push @result, { msgid => $msgid, result => 'already red' };
1643 next;
1645 # in state no 'R' - marking as red
1646 $item->mark_read;
1647 push @result, { msgid => $msgid, result => 'set read' };
1651 return {
1652 result => \@result,
1653 xc3 => {
1654 u => $u
1659 sub sendmessage
1661 my ($req, $err, $flags) = @_;
1663 return fail($err, 315) if $LJ::DISABLED{user_messaging};
1665 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'sendmessage');
1666 my $u = $flags->{'u'};
1668 return fail($err, 305) if $u->statusvis eq 'S'; # suspended cannot send private messages
1670 my $msg_limit = LJ::get_cap($u, "usermessage_length");
1672 my @errors;
1674 my $subject_text = LJ::strip_html($req->{'subject'});
1675 return fail($err, 208, 'subject')
1676 unless LJ::text_in($subject_text);
1678 # strip HTML from body and test encoding and length
1679 my $body_text = LJ::strip_html($req->{'body'});
1680 return fail($err, 208, 'body')
1681 unless LJ::text_in($body_text);
1683 my ($msg_len_b, $msg_len_c) = LJ::text_length($body_text);
1684 return fail($err, 212, 'xmlrpc.des.message_long', {'len'=>LJ::commafy($msg_len_c), 'limit'=>LJ::commafy($msg_limit)})
1685 unless ($msg_len_c <= $msg_limit);
1688 return fail($err, 213, 'xmlrpc.des.message_empty', {'len'=>LJ::commafy($msg_len_c)})
1689 if ($msg_len_c <= 0);
1691 my @to = (ref $req->{'to'}) ? @{$req->{'to'}} : ($req->{'to'});
1692 return fail($err, 200) unless scalar @to;
1694 # remove duplicates
1695 my %to = map { lc($_), 1 } @to;
1696 @to = keys %to;
1698 my @msg;
1699 BML::set_language('en') unless BML::get_language();
1701 foreach my $to (@to) {
1702 my $tou = LJ::load_user($to);
1703 return fail($err, 100, $to)
1704 unless $tou;
1706 my $msg = LJ::Message->new({
1707 journalid => $u->userid,
1708 otherid => $tou->userid,
1709 subject => $subject_text,
1710 body => $body_text,
1711 parent_msgid => defined $req->{'parent'} ? $req->{'parent'} + 0 : undef,
1712 userpic => $req->{'userpic'} || undef,
1715 push @msg, $msg
1716 if $msg->can_send(\@errors);
1718 return fail($err, 203, join('; ', @errors))
1719 if scalar @errors;
1721 foreach my $msg (@msg) {
1722 $msg->send(\@errors);
1725 return {
1726 'sent_count' => scalar @msg,
1727 'msgid' => [ grep { $_ } map { $_->msgid } @msg ],
1728 (@errors ? ('last_errors' => \@errors) : () ),
1729 xc3 => {
1730 u => $u
1735 sub login
1737 my ($req, $err, $flags) = @_;
1738 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'login');
1739 return undef unless check_altusage($req, $err, $flags);
1741 my $u = $flags->{'u'};
1742 my $uowner = $flags->{'u_owner'} || $u;
1744 my $auth_meth = $req->{'auth_method'} || "clear";
1746 my $res = {
1747 xc3 => {
1748 u => $u
1751 my $ver = $req->{'ver'};
1753 ## check for version mismatches
1754 ## non-Unicode installations can't handle versions >=1
1756 return fail($err,207, 'xmlrpc.des.not_unicode')
1757 if $ver>=1 and not $LJ::UNICODE;
1759 # do not let locked people log in
1760 return fail($err, 308) if $u->{statusvis} eq 'L';
1762 ## return a message to the client to be displayed (optional)
1763 login_message($req, $res, $flags);
1764 LJ::text_out(\$res->{'message'}) if $ver>=1 and defined $res->{'message'};
1766 ## report what shared journals this user may post in
1767 $res->{'usejournals'} = list_usejournals($u) if (LJ::u_equals($u, $uowner));
1769 # identity users can only post to communities
1770 # return fail( $err, 150 )
1771 # if $u->is_identity and LJ::u_equals( $u, $uowner );
1774 ## return their friend groups
1775 if (LJ::u_equals($u, $uowner)) {
1776 $res->{'friendgroups'} = list_friendgroups($u);
1777 return fail($err, 502, 'xmlrpc.des.friend_groups_fail') unless $res->{'friendgroups'};
1778 if ($ver >= 1) {
1779 foreach (@{$res->{'friendgroups'}}) {
1780 LJ::text_out(\$_->{'name'});
1785 ## if they gave us a number of moods to get higher than, then return them
1786 if (defined $req->{'getmoods'}) {
1787 $res->{'moods'} = list_moods($req->{'getmoods'});
1788 if ($ver >= 1) {
1789 # currently all moods are in English, but this might change
1790 foreach (@{$res->{'moods'}}) { LJ::text_out(\$_->{'name'}) }
1794 ### picture keywords, if they asked for them.
1795 if ($req->{'getpickws'} || $req->{'getpickwurls'}) {
1796 my $pickws = list_pickws($uowner);
1797 @$pickws = sort { lc($a->[0]) cmp lc($b->[0]) } @$pickws;
1798 $res->{'pickws'} = [ map { $_->[0] } @$pickws ] if $req->{'getpickws'};
1799 if ($req->{'getpickwurls'}) {
1800 if ($uowner->{'defaultpicid'}) {
1801 $res->{'defaultpicurl'} = "$LJ::USERPIC_ROOT/$uowner->{'defaultpicid'}/$uowner->{'userid'}";
1803 $res->{'pickwurls'} = [ map {
1804 "$LJ::USERPIC_ROOT/$_->[1]/$uowner->{'userid'}"
1805 } @$pickws ];
1807 if ($ver >= 1) {
1808 # validate all text
1809 foreach(@{$res->{'pickws'}}) { LJ::text_out(\$_); }
1810 foreach(@{$res->{'pickwurls'}}) { LJ::text_out(\$_); }
1811 LJ::text_out(\$res->{'defaultpicurl'});
1814 ## return caps, if they asked for them
1815 if ($req->{'getcaps'} && $u->can_manage($uowner)) {
1816 $res->{'caps'} = $uowner->caps;
1819 ## return client menu tree, if requested
1820 if ($req->{'getmenus'} ) {
1821 $res->{'menus'} = hash_menus($uowner);
1822 if ($ver >= 1) {
1823 # validate all text, just in case, even though currently
1824 # it's all English
1825 foreach (@{$res->{'menus'}}) {
1826 LJ::text_out(\$_->{'text'});
1827 LJ::text_out(\$_->{'url'}); # should be redundant
1832 ## tell some users they can hit the fast servers later.
1833 $res->{'fastserver'} = 1 if LJ::get_cap($uowner, "fastserver");
1835 ## user info
1836 $res->{'username'} = $uowner->{'user'};
1837 $res->{'userid'} = $uowner->{'userid'};
1838 $res->{'fullname'} = $uowner->{'name'};
1839 LJ::text_out(\$res->{'fullname'}) if $ver >= 1;
1841 # Identity info
1842 if ($uowner->is_identity){
1843 my $i = $uowner->identity;
1844 $res->{'identity_type'} = $i->pretty_type;
1845 $res->{'identity_value'} = $i->value;
1846 $res->{'identity_url'} = $i->url($uowner);
1847 $res->{'identity_display'} = $uowner->display_name;
1848 } else {
1849 foreach (qw(identity_display identity_url identity_value identity_type)) {
1850 $res->{$_} = '';
1854 if ($req->{'clientversion'} =~ /^\S+\/\S+$/) {
1855 eval {
1856 LJ::Request->notes("clientver", $req->{'clientversion'});
1860 ## update or add to clientusage table
1861 if ($req->{'clientversion'} =~ /^\S+\/\S+$/ &&
1862 ! $LJ::DISABLED{'clientversionlog'})
1864 my $client = $req->{'clientversion'};
1866 return fail($err, 208, 'xmlrpc.des.bad_value', {'param'=>'clientversion'})
1867 if $ver >= 1 and not LJ::text_in($client);
1869 my $dbh = LJ::get_db_writer();
1870 my $qclient = $dbh->quote($client);
1871 my $cu_sql = "REPLACE INTO clientusage (userid, clientid, lastlogin) " .
1872 "SELECT $u->{'userid'}, clientid, NOW() FROM clients WHERE client=$qclient";
1873 my $sth = $dbh->prepare($cu_sql);
1874 $sth->execute;
1875 unless ($sth->rows) {
1876 # only way this can be 0 is if client doesn't exist in clients table, so
1877 # we need to add a new row there, to get a new clientid for this new client:
1878 $dbh->do("INSERT INTO clients (client) VALUES ($qclient)");
1879 # and now we can do the query from before and it should work:
1880 $sth = $dbh->prepare($cu_sql);
1881 $sth->execute;
1885 return $res;
1888 sub getfriendgroups
1890 my ($req, $err, $flags) = @_;
1891 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getfriendgroups');
1892 my $u = $flags->{'u'};
1893 my $res = {
1894 xc3 => {
1895 u => $u
1899 $res->{'friendgroups'} = list_friendgroups($u);
1900 return fail($err, 502, 'xmlrpc.des.friend_groups_fail') unless $res->{'friendgroups'};
1901 if ($req->{'ver'} >= 1) {
1902 foreach (@{$res->{'friendgroups'} || []}) {
1903 LJ::text_out(\$_->{'name'});
1907 return $res;
1910 sub getusertags
1912 my ($req, $err, $flags) = @_;
1913 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getusertags');
1914 return undef unless check_altusage($req, $err, $flags);
1916 my $u = $flags->{'u'};
1917 my $uowner = $flags->{'u_owner'} || $u;
1918 return fail($req, 502) unless $u && $uowner;
1920 my $tags = LJ::Tags::get_usertags($uowner, { remote => $u });
1921 return {
1922 tags => [ values %$tags ],
1923 xc3 => {
1924 u => $u
1929 sub getuserpics
1931 my ($req, $err, $flags) = @_;
1933 $flags->{'allow_anonymous'} = 1;
1934 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getuserpics');
1935 $flags->{'ignorecanuse'} = 1; # function return public info
1936 return undef unless check_altusage($req, $err, $flags);
1938 my $u = $flags->{'u'};
1939 my $uowner = $flags->{'u_owner'} || $u;
1940 return fail($err, 502) unless $uowner;
1942 my $res = {
1943 xc3 => {
1944 u => $u
1948 my $pickws = list_pickws($uowner);
1949 @$pickws = sort { lc($a->[0]) cmp lc($b->[0]) } @$pickws;
1950 $res->{'pickws'} = [ map { $_->[0] } @$pickws ];
1952 if ($uowner->{'defaultpicid'}) {
1953 $res->{'defaultpicurl'} = "$LJ::USERPIC_ROOT/$uowner->{'defaultpicid'}/$uowner->{'userid'}";
1955 $res->{'pickwurls'} = [ map {
1956 "$LJ::USERPIC_ROOT/$_->[1]/$uowner->{'userid'}"
1957 } @$pickws ];
1958 # validate all text
1959 foreach(@{$res->{'pickws'}}) { LJ::text_out(\$_); }
1960 foreach(@{$res->{'pickwurls'}}) { LJ::text_out(\$_); }
1961 LJ::text_out(\$res->{'defaultpicurl'});
1963 return $res;
1967 sub getfriends
1969 my ($req, $err, $flags) = @_;
1970 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getfriends');
1972 return fail($req,502) unless LJ::get_db_reader();
1973 my $u = $flags->{'u'};
1974 my $res = {
1975 xc3 => {
1976 u => $u
1980 if ($req->{'includegroups'}) {
1981 $res->{'friendgroups'} = list_friendgroups($u);
1982 return fail($err, 502, 'xmlrpc.des.friend_groups_fail') unless $res->{'friendgroups'};
1983 if ($req->{'ver'} >= 1) {
1984 foreach (@{$res->{'friendgroups'} || []}) {
1985 LJ::text_out(\$_->{'name'});
1989 # TAG:FR:protocol:getfriends_of
1990 if ($req->{'includefriendof'}) {
1991 $res->{'friendofs'} = list_friends($u, {
1992 'limit' => $req->{'friendoflimit'},
1993 'friendof' => 1,
1995 if ($req->{'ver'} >= 1) {
1996 foreach(@{$res->{'friendofs'}}) { LJ::text_out(\$_->{'fullname'}) };
1999 # TAG:FR:protocol:getfriends
2000 $res->{'friends'} = list_friends($u, {
2001 'limit' => $req->{'friendlimit'},
2002 'includebdays' => $req->{'includebdays'},
2004 if ($req->{'ver'} >= 1) {
2005 foreach(@{$res->{'friends'}}) { LJ::text_out(\$_->{'fullname'}) };
2008 return $res;
2011 sub friendof
2013 my ($req, $err, $flags) = @_;
2014 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'friendof');
2015 return fail($req,502) unless LJ::get_db_reader();
2016 my $u = $flags->{'u'};
2017 my $res = {
2018 xc3 => {
2019 u => $u
2023 # TAG:FR:protocol:getfriends_of2 (same as TAG:FR:protocol:getfriends_of)
2024 $res->{'friendofs'} = list_friends($u, {
2025 'friendof' => 1,
2026 'limit' => $req->{'friendoflimit'},
2028 if ($req->{'ver'} >= 1) {
2029 foreach(@{$res->{'friendofs'}}) { LJ::text_out(\$_->{'fullname'}) };
2032 return $res;
2035 sub checkfriends
2037 my ($req, $err, $flags) = @_;
2038 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'checkfriends');
2039 my $u = $flags->{'u'};
2040 my $res = {
2041 xc3 => {
2042 u => $u
2046 # return immediately if they can't use this mode
2047 unless (LJ::get_cap($u, "checkfriends")) {
2048 $res->{'new'} = 0;
2049 $res->{'interval'} = 36000; # tell client to bugger off
2050 return $res;
2053 ## have a valid date?
2054 my $lastupdate = $req->{'lastupdate'};
2055 if ($lastupdate) {
2056 return fail($err,203) unless
2057 ($lastupdate =~ /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/);
2058 } else {
2059 $lastupdate = "0000-00-00 00:00:00";
2062 my $interval = LJ::get_cap_min($u, "checkfriends_interval");
2063 $res->{'interval'} = $interval;
2065 my $mask;
2066 if ($req->{'mask'} and $req->{'mask'} !~ /\D/) {
2067 $mask = $req->{'mask'};
2070 my $memkey = [$u->{'userid'},"checkfriends:$u->{userid}:$mask"];
2071 my $update = LJ::MemCache::get($memkey);
2072 unless ($update) {
2073 # TAG:FR:protocol:checkfriends (wants reading list of mask, not "friends")
2074 my $fr = LJ::get_friends($u, $mask);
2075 unless ($fr && %$fr) {
2076 $res->{'new'} = 0;
2077 $res->{'lastupdate'} = $lastupdate;
2078 return $res;
2080 if (@LJ::MEMCACHE_SERVERS) {
2081 my $tu = LJ::get_timeupdate_multi({ memcache_only => 1 }, keys %$fr);
2082 my $max = 0;
2083 while ($_ = each %$tu) {
2084 $max = $tu->{$_} if $tu->{$_} > $max;
2086 $update = LJ::TimeUtil->mysql_time($max) if $max;
2087 } else {
2088 my $dbr = LJ::get_db_reader();
2089 unless ($dbr) {
2090 # rather than return a 502 no-db error, just say no updates,
2091 # because problem'll be fixed soon enough by db admins
2092 $res->{'new'} = 0;
2093 $res->{'lastupdate'} = $lastupdate;
2094 return $res;
2096 my $list = join(", ", map { int($_) } keys %$fr);
2097 if ($list) {
2098 my $sql = "SELECT MAX(timeupdate) FROM userusage ".
2099 "WHERE userid IN ($list)";
2100 $update = $dbr->selectrow_array($sql);
2103 LJ::MemCache::set($memkey,$update,time()+$interval) if $update;
2105 $update ||= "0000-00-00 00:00:00";
2107 if ($req->{'lastupdate'} && $update gt $lastupdate) {
2108 $res->{'new'} = 1;
2109 } else {
2110 $res->{'new'} = 0;
2113 $res->{'lastupdate'} = $update;
2115 return $res;
2118 sub getdaycounts
2120 my ($req, $err, $flags) = @_;
2121 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getdaycounts');
2122 return undef unless check_altusage($req, $err, $flags);
2124 my $u = $flags->{'u'};
2125 my $uowner = $flags->{'u_owner'} || $u;
2126 my $ownerid = $flags->{'ownerid'};
2128 my $res = {
2129 xc3 => {
2130 u => $u
2134 my $daycts = LJ::get_daycounts($uowner, $u);
2135 return fail($err,502) unless $daycts;
2137 foreach my $day (@$daycts) {
2138 my $date = sprintf("%04d-%02d-%02d", $day->[0], $day->[1], $day->[2]);
2139 push @{$res->{'daycounts'}}, { 'date' => $date, 'count' => $day->[3] };
2142 return $res;
2145 sub common_event_validation
2147 my ($req, $err, $flags) = @_;
2149 # clean up event whitespace
2150 # remove surrounding whitespace
2151 $req->{event} =~ s/^\s+//;
2152 $req->{event} =~ s/\s+$//;
2154 # convert line endings to unix format
2155 if ($req->{'lineendings'} eq "mac") {
2156 $req->{event} =~ s/\r/\n/g;
2157 } else {
2158 $req->{event} =~ s/\r//g;
2161 # date validation
2162 if ($req->{'year'} !~ /^\d\d\d\d$/ ||
2163 $req->{'year'} < 1970 || # before unix time started = bad
2164 $req->{'year'} > 2037) # after unix time ends = worse! :)
2166 return fail($err,203,'xmlrpc.des.bad_value',{'param'=>'year'});
2168 if ($req->{'mon'} !~ /^\d{1,2}$/ ||
2169 $req->{'mon'} < 1 ||
2170 $req->{'mon'} > 12)
2172 return fail($err,203,'xmlrpc.des.bad_value',{'param'=>'month'});
2174 if ($req->{'day'} !~ /^\d{1,2}$/ || $req->{'day'} < 1 ||
2175 $req->{'day'} > LJ::TimeUtil->days_in_month($req->{'mon'},
2176 $req->{'year'}))
2178 return fail($err,203,'xmlrpc.des.bad_value',{'param'=>'day of month'});
2180 if ($req->{'hour'} !~ /^\d{1,2}$/ ||
2181 $req->{'hour'} < 0 || $req->{'hour'} > 23)
2183 return fail($err,203,'xmlrpc.des.bad_value',{'param'=>'hour'});
2185 if ($req->{'min'} !~ /^\d{1,2}$/ ||
2186 $req->{'min'} < 0 || $req->{'min'} > 59)
2188 return fail($err,203,'xmlrpc.des.bad_value',{'param'=>'minute'});
2191 # column width
2192 # we only trim Unicode data
2194 if ($req->{'ver'} >=1 ) {
2195 $req->{'subject'} = LJ::text_trim($req->{'subject'}, LJ::BMAX_SUBJECT, LJ::CMAX_SUBJECT);
2196 $req->{'event'} = LJ::text_trim($req->{'event'}, LJ::BMAX_EVENT, LJ::CMAX_EVENT);
2197 foreach (keys %{$req->{'props'}}) {
2198 # do not trim this property, as it's magical and handled later
2199 next if $_ eq 'taglist';
2201 # Allow syn_links and syn_ids the full width of the prop, to avoid truncating long URLS
2202 if ($_ eq 'syn_link' || $_ eq 'syn_id' || $_ eq 'discovery') {
2203 $req->{'props'}->{$_} = LJ::text_trim($req->{'props'}->{$_}, LJ::BMAX_PROP);
2204 } elsif ( $_ eq 'current_music' || $_ eq 'current_location' ) {
2205 $req->{'props'}->{$_} = LJ::text_trim($req->{'props'}->{$_}, LJ::CMMAX_PROP);
2206 } else {
2207 $req->{'props'}->{$_} = LJ::text_trim($req->{'props'}->{$_}, LJ::BMAX_PROP, LJ::CMAX_PROP);
2213 # setup non-user meta-data. it's important we define this here to
2214 # 0. if it's not defined at all, then an editevent where a user
2215 # removes random 8bit data won't remove the metadata. not that
2216 # that matters much. but having this here won't hurt. false
2217 # meta-data isn't saved anyway. so the only point of this next
2218 # line is making the metadata be deleted on edit.
2219 $req->{'props'}->{'unknown8bit'} = 0;
2221 # we don't want attackers sending something that looks like gzipped data
2222 # in protocol version 0 (unknown8bit allowed), otherwise they might
2223 # inject a 100MB string of single letters in a few bytes.
2224 return fail($err,208,'xmlrpc.des.send_gzip_fail')
2225 if substr($req->{'event'},0,2) eq "\037\213";
2227 # non-ASCII?
2228 unless ( $flags->{'use_old_content'} || (
2229 LJ::is_ascii($req->{'event'}) &&
2230 LJ::is_ascii($req->{'subject'}) &&
2231 LJ::is_ascii(join(' ', values %{$req->{'props'}})) ))
2234 if ($req->{'ver'} < 1) { # client doesn't support Unicode
2235 ## Hack: some old clients do send valid UTF-8 data,
2236 ## but don't tell us about that.
2237 ## Check, if the event/subject are valid UTF-8 strings.
2238 my $tmp_event = $req->{'event'};
2239 my $tmp_subject = $req->{'subject'};
2240 Encode::from_to($tmp_event, "utf-8", "utf-8");
2241 Encode::from_to($tmp_subject, "utf-8", "utf-8");
2242 if ($tmp_event eq $req->{'event'} && $tmp_subject eq $req->{'subject'}) {
2243 ## ok, this looks like valid UTF-8
2244 } else {
2245 ## encoding is unknown - it's neither ASCII nor UTF-8
2246 # only people should have unknown8bit entries.
2247 my $uowner = $flags->{u_owner} || $flags->{u};
2248 return fail($err,207,'xmlrpc.des.need_unicode_client')
2249 if $uowner->{journaltype} ne 'P';
2251 # so rest of site can change chars to ? marks until
2252 # default user's encoding is set. (legacy support)
2253 $req->{'props'}->{'unknown8bit'} = 1;
2255 } else {
2256 return fail($err,207, 'xmlrpc.des.not_unicode') unless $LJ::UNICODE;
2257 # validate that the text is valid UTF-8
2258 if (!LJ::text_in($req->{'subject'}) ||
2259 !LJ::text_in($req->{'event'}) ||
2260 grep { !LJ::text_in($_) } values %{$req->{'props'}}) {
2261 return fail($err, 208, 'xmlrpc.des.not_valid_unicode');
2266 ## handle meta-data (properties)
2267 LJ::load_props("log");
2268 foreach my $pname (keys %{$req->{'props'}})
2270 my $p = LJ::get_prop("log", $pname);
2272 # does the property even exist?
2273 unless ($p) {
2274 $pname =~ s/[^\w]//g;
2275 return fail($err,205,$pname);
2278 # don't validate its type if it's 0 or undef (deleting)
2279 next unless ($req->{'props'}->{$pname});
2281 my $ptype = $p->{'datatype'};
2282 my $val = $req->{'props'}->{$pname};
2284 if ($ptype eq "bool" && $val !~ /^[01]$/) {
2285 return fail($err,204,'xmlrpc.des.non_boolean',{'param'=>$pname});
2287 if ($ptype eq "num" && $val =~ /[^\d]/) {
2288 return fail($err,204,'xmlrpc.des.non_arifmetic',{'param'=>$pname,'value'=>$val});
2290 if ($pname eq "current_coords" && ! eval { LJ::Location->new(coords => $val) }) {
2291 return fail($err,204,'xmlrpc.des.bad_value', {'param'=>'current_coords'});
2295 # check props for inactive userpic
2296 if (my $pickwd = $req->{'props'}->{'picture_keyword'}) {
2297 my $pic = LJ::get_pic_from_keyword($flags->{'u'}, $pickwd);
2299 # need to make sure they aren't trying to post with an inactive keyword, but also
2300 # we don't want to allow them to post with a keyword that has no pic at all to prevent
2301 # them from deleting the keyword, posting, then adding it back with editpics.bml
2302 delete $req->{'props'}->{'picture_keyword'} if ! $pic || $pic->{'state'} eq 'I';
2305 # validate incoming list of tags
2306 return fail($err, 211)
2307 if $req->{props}->{taglist} &&
2308 ! LJ::Tags::is_valid_tagstring($req->{props}->{taglist});
2310 return 1;
2313 sub postevent {
2314 my ($req, $err, $flags) = @_;
2315 un_utf8_request($req);
2317 my $post_noauth = LJ::run_hook('post_noauth', $req);
2319 return undef unless $post_noauth || authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'postevent');
2321 my $spam = 0;
2322 LJ::run_hook('spam_detector', $req, \$spam);
2323 return fail($err,320) if $spam;
2325 # if going through mod queue, then we know they're permitted to post at least this entry
2326 $flags->{'usejournal_okay'} = 1 if $post_noauth;
2327 return undef unless check_altusage($req, $err, $flags) || $flags->{nomod};
2329 my $u = $flags->{'u'};
2330 my $ownerid = $flags->{'ownerid'}+0;
2331 my $uowner = $flags->{'u_owner'} || $u;
2332 # Make sure we have a real user object here
2333 $uowner = LJ::want_user($uowner) unless LJ::isu($uowner);
2334 r($uowner) unless LJ::isu($uowner);
2335 my $clusterid = $uowner->{'clusterid'};
2337 my $dbh = LJ::get_db_writer();
2338 my $dbcm = LJ::get_cluster_master($uowner);
2340 return fail($err,306) unless $dbh && $dbcm && $uowner->writer;
2341 return fail($err,200) unless $req->{'event'} =~ /\S/;
2343 ### make sure community, shared, or news journals don't post
2344 ### note: shared and news journals are deprecated. every shared journal
2345 ## should one day be a community journal, of some form.
2346 return fail($err,150) if ($u->{'journaltype'} eq "C" ||
2347 $u->{'journaltype'} eq "S" ||
2348 $u->{'journaltype'} eq "N");
2350 # identity users can only post to communities
2351 return fail( $err, 150 )
2352 if $u->is_identity and LJ::u_equals( $u, $uowner );
2354 # underage users can't do this
2355 return fail($err,310) if $u->underage;
2357 # suspended users can't post
2358 return fail($err,305) if ($u->{'statusvis'} eq "S");
2360 # memorials can't post
2361 return fail($err,309) if $u->{statusvis} eq 'M';
2363 # locked accounts can't post
2364 return fail($err,308) if $u->{statusvis} eq 'L';
2366 # check the journal's read-only bit
2367 return fail($err,306) if LJ::get_cap($uowner, "readonly");
2369 # is the user allowed to post?
2370 return fail($err,404,$LJ::MSG_NO_POST) unless LJ::get_cap($u, "can_post");
2372 # is the user allowed to post?
2373 return fail($err,410) if LJ::get_cap($u, "disable_can_post");
2375 # read-only accounts can't post
2376 return fail($err,316) if $u->is_readonly;
2378 # read-only accounts can't be posted to
2379 return fail($err,317) if $uowner->is_readonly;
2381 # can't post to deleted/suspended community
2382 return fail($err,307) unless $uowner->{'statusvis'} eq "V";
2384 # user must have a validated email address to post to any journal - including its own,
2385 # except syndicated (rss, 'Y') journals
2386 # unless this is approved from the mod queue (we'll error out initially, but in case they change later)
2387 return fail($err, 155, LJ::Lang::ml('event.post.error.not_validated_email'))
2388 unless $flags->{'first_post'} || $u->{'status'} eq 'A' || $u->{'journaltype'} eq 'Y';
2390 $req->{'event'} =~ s/\r\n/\n/g; # compact new-line endings to more comfort chars count near 65535 limit
2392 # post content too large
2393 # NOTE: requires $req->{event} be binary data, but we've already
2394 # removed the utf-8 flag in the XML-RPC path, and it never gets
2395 # set in the "flat" protocol path.
2396 return fail($err,409) if length($req->{'event'}) >= LJ::BMAX_EVENT;
2398 my $time_was_faked = 0;
2399 my $offset = 0; # assume gmt at first.
2401 if (defined $req->{'tz'}) {
2402 if ($req->{tz} eq 'guess') {
2403 LJ::get_timezone($u, \$offset, \$time_was_faked);
2404 } elsif ($req->{'tz'} =~ /^[+\-]\d\d\d\d$/) {
2405 # FIXME we ought to store this timezone and make use of it somehow.
2406 $offset = $req->{'tz'} / 100.0;
2407 } else {
2408 return fail($err, 203, 'xmlrpc.des.bad_value', {'param'=>'tz'});
2412 if (defined $req->{'tz'} and not grep { defined $req->{$_} } qw(year mon day hour min)) {
2413 my @ltime = gmtime(time() + ($offset*3600));
2414 $req->{'year'} = $ltime[5]+1900;
2415 $req->{'mon'} = $ltime[4]+1;
2416 $req->{'day'} = $ltime[3];
2417 $req->{'hour'} = $ltime[2];
2418 $req->{'min'} = $ltime[1];
2419 $time_was_faked = 1;
2422 return undef
2423 unless common_event_validation($req, $err, $flags);
2425 # confirm we can add tags, at least
2426 return fail($err, 312)
2427 if $req->{props} &&
2428 defined $req->{props}->{taglist} &&
2429 $req->{props}->{taglist} ne '' &&
2430 ! LJ::Tags::can_add_tags($uowner, $u);
2432 my $event = $req->{'event'};
2434 if ($uowner->is_community) {
2435 delete $req->{'props'}->{'opt_backdated'};
2438 ### allow for posting to journals that aren't yours (if you have permission)
2439 my $posterid = $u->{'userid'}+0;
2441 # make the proper date format
2442 my $eventtime = sprintf("%04d-%02d-%02d %02d:%02d",
2443 $req->{'year'}, $req->{'mon'},
2444 $req->{'day'}, $req->{'hour'},
2445 $req->{'min'});
2446 my $qeventtime = $dbh->quote($eventtime);
2448 # load userprops all at once
2449 my @poster_props = qw(newesteventtime);
2450 my @owner_props = qw(newpost_minsecurity moderated);
2451 push @owner_props, 'opt_weblogscom' unless $req->{'props'}->{'opt_backdated'};
2453 LJ::load_user_props($u, @poster_props, @owner_props);
2454 if ($uowner->{'userid'} == $u->{'userid'}) {
2455 $uowner->{$_} = $u->{$_} foreach (@owner_props);
2456 } else {
2457 LJ::load_user_props($uowner, @owner_props);
2460 # are they trying to post back in time?
2461 if ($posterid == $ownerid && $u->{'journaltype'} ne 'Y' &&
2462 !LJ::is_enabled("delayed_entries") &&
2463 !$time_was_faked && $u->{'newesteventtime'} &&
2464 $eventtime lt $u->{'newesteventtime'} &&
2465 !$req->{'props'}->{'opt_backdated'}) {
2466 return fail($err, 153, 'xmlrpc.des.entry_time_conflict', {'newesteventtime'=>$u->{'newesteventtime'}});
2469 if ( $req->{sticky} &&
2470 $uowner->is_community() &&
2471 !$u->can_manage($uowner) )
2473 return fail($err, 158);
2476 my $qallowmask = $req->{'allowmask'}+0;
2477 my $security = "public";
2478 my $uselogsec = 0;
2479 if ($req->{'security'} eq "usemask" || $req->{'security'} eq "private") {
2480 $security = $req->{'security'};
2482 if ($req->{'security'} eq "usemask") {
2483 $uselogsec = 1;
2486 unless ( LJ::is_enabled('new_friends_and_subscriptions') ) {
2487 # can't specify both a custom security and 'friends-only'
2488 return fail($err, 203, 'xmlrpc.des.friends_security')
2489 if $qallowmask > 1 && $qallowmask % 2;
2492 ## if newpost_minsecurity is set, new entries have to be
2493 ## a minimum security level
2494 unless ($flags->{'entryrepost'}) {
2495 $security = "private"
2496 if $uowner->newpost_minsecurity eq "private";
2497 ($security, $qallowmask) = ("usemask", 1)
2498 if $uowner->newpost_minsecurity eq "friends"
2499 and $security eq "public";
2502 my $qsecurity = $dbh->quote($security);
2504 ### make sure user can't post with "custom/private security" on shared journals
2505 return fail($err,102)
2506 if ($ownerid != $posterid && # community post
2507 !($u && $u->can_manage($uowner)) && # poster is not admin
2508 ($req->{'security'} eq "private" ||
2509 ($req->{'security'} eq "usemask" && $qallowmask != 1 )));
2511 # make sure this user isn't banned from posting here (if
2512 # this is a community journal)
2513 return fail($err,151) if
2514 LJ::is_banned($posterid, $ownerid);
2516 if (!LJ::is_enabled("delayed_entries")) {
2517 # don't allow backdated posts in communities
2518 return fail($err,152) if
2519 ($req->{'props'}->{"opt_backdated"} &&
2520 $uowner->{'journaltype'} ne "P");
2523 # do processing of embedded polls (doesn't add to database, just
2524 # does validity checking)
2525 my @polls = ();
2526 if (LJ::Poll->contains_new_poll(\$event))
2528 return fail($err,301,'xmlrpc.des.poll_not_permitted')
2529 unless (LJ::get_cap($u, "makepoll")
2530 || ($uowner->{'journaltype'} eq "C"
2531 && LJ::get_cap($uowner, "makepoll")
2532 && LJ::can_manage_other($u, $uowner)));
2534 my $error = "";
2535 @polls = LJ::Poll->new_from_html(\$event, \$error, {
2536 'journalid' => $ownerid,
2537 'posterid' => $posterid,
2539 return fail($err,103,$error) if $error;
2542 my $repost_offer;
2543 if (LJ::is_enabled("paid_repost")) {
2544 my $error;
2546 $repost_offer = LJ::Pay::Repost::Offer->from_create_entry(
2547 \$event,
2548 {repost_budget => $req->{'repost_budget'},
2549 limit_sc => $req->{'repost_limit_sc'},
2550 journalid => $ownerid,
2551 userid => $posterid,
2552 targeting_gender => $req->{'repost_targeting_gender'},
2553 targeting_age => $req->{'repost_targeting_age'},
2554 targeting_country => $req->{'repost_targeting_country'},
2555 targeting_state => $req->{'repost_targeting_state'}},
2556 \$error
2559 return fail($err,222) if $repost_offer && ! $flags->{noauth};
2561 return fail($err,160,$error) if $error;
2564 # convert RTE lj-embeds to normal lj-embeds
2565 $event = LJ::EmbedModule->transform_rte_post($event);
2567 # process module embedding
2568 LJ::EmbedModule->parse_module_embed($uowner, \$event);
2570 my $now = $dbcm->selectrow_array("SELECT UNIX_TIMESTAMP()");
2571 my $anum = int(rand(256));
2573 # by default we record the true reverse time that the item was entered.
2574 # however, if backdate is on, we put the reverse time at the end of time
2575 # (which makes it equivalent to 1969, but get_recent_items will never load
2576 # it... where clause there is: < $LJ::EndOfTime). but this way we can
2577 # have entries that don't show up on friends view, now that we don't have
2578 # the hints table to not insert into.
2579 my $rlogtime = $LJ::EndOfTime;
2580 unless ($req->{'props'}->{'opt_backdated'}) {
2581 $rlogtime -= $now;
2584 my $dupsig = Digest::MD5::md5_hex(join('', map { $req->{$_} }
2585 qw(subject event usejournal security allowmask)));
2586 my $lock_key = "post-$ownerid";
2588 # release our duplicate lock
2589 my $release = sub { $dbcm->do("SELECT RELEASE_LOCK(?)", undef, $lock_key); };
2591 # our own local version of fail that releases our lock first
2592 my $fail = sub { $release->(); return fail(@_); };
2594 my $res = {};
2595 my $res_done = 0; # set true by getlock when post was duplicate, or error getting lock
2597 my $getlock = sub {
2598 my ($delayed) = @_;
2599 my $r = $dbcm->selectrow_array("SELECT GET_LOCK(?, 2)", undef, $lock_key);
2600 unless ($r) {
2601 $res = undef; # a failure case has an undef result
2602 fail($err,503); # set error flag to "can't get lock";
2603 $res_done = 1; # tell caller to bail out
2604 return;
2607 if ($delayed) {
2608 my $entry = LJ::DelayedEntry->dupsig_check($uowner, $posterid, $req);
2609 if ($entry) {
2610 $res->{'delayedid'} = $entry->delayedid;
2611 $res->{'type'} = 'delayed';
2612 $res->{'url'} = $entry->url;
2614 $res_done = 1;
2615 $release->();
2617 return;
2620 LJ::load_user_props($u, { use_master => 1, reload => 1 }, 'dupsig_post');
2622 my @parts = split(/:/, $u->{'dupsig_post'});
2623 if ($parts[0] eq $dupsig) {
2624 # duplicate! let's make the client think this was just the
2625 # normal firsit response.
2627 $res->{'itemid'} = $parts[1];
2628 $res->{'anum'} = $parts[2];
2630 my $dup_entry = LJ::Entry->new($uowner, jitemid => $res->{'itemid'}, anum => $res->{'anum'});
2631 $res->{'url'} = $dup_entry->url;
2633 $res_done = 1;
2634 $release->();
2638 if ( $req->{ver} > 3 && LJ::is_enabled("delayed_entries") ) {
2639 if ( $req->{'custom_time'} && LJ::DelayedEntry::is_future_date($req) ) {
2640 return fail($err, 215) unless $req->{tz};
2642 return fail($err, 159) if $repost_offer;
2644 # if posting to a moderated community, store and bail out here
2645 if ( !LJ::DelayedEntry::can_post_to($uowner, $u, $req)) {
2646 return fail($err, 322);
2649 $req->{ext}->{flags} = $flags;
2650 $req->{usejournal} = $req->{usejournal} || '';
2651 delete $req->{'custom_time'};
2653 $getlock->('delayed');
2654 return $res if $res_done;
2656 my $entry = LJ::DelayedEntry->create( $req, { journal => $uowner,
2657 poster => $u,} );
2658 if (!$entry) {
2659 return $fail->($err, 507);
2662 $res->{'delayedid'} = $entry->delayedid;
2663 $res->{'type'} = 'delayed';
2664 $res->{'url'} = $entry->url;
2666 $release->();
2667 return $res;
2669 else {
2670 $res->{type} = 'posted';
2674 my $need_moderated = ( $uowner->{'moderated'} =~ /^[1A]$/ ) ? 1 : 0;
2675 if ( $uowner->{'moderated'} eq 'F' ) {
2676 ## Scan post for spam
2677 LJ::run_hook('spam_community_detector', $uowner, $req, \$need_moderated);
2680 # if posting to a moderated community, store and bail out here
2681 if ($uowner->{'journaltype'} eq 'C' && $need_moderated && !$flags->{'nomod'}) {
2682 # don't moderate admins, moderators & pre-approved users
2683 my $dbh = LJ::get_db_writer();
2684 my $relcount = $dbh->selectrow_array("SELECT COUNT(*) FROM reluser ".
2685 "WHERE userid=$ownerid AND targetid=$posterid ".
2686 "AND type IN ('A','M','N')");
2687 unless ($relcount) {
2688 # moderation queue full?
2689 my $modcount = $dbcm->selectrow_array("SELECT COUNT(*) FROM modlog WHERE journalid=$ownerid");
2690 return fail($err, 407) if $modcount >= LJ::get_cap($uowner, "mod_queue");
2692 $modcount = $dbcm->selectrow_array("SELECT COUNT(*) FROM modlog ".
2693 "WHERE journalid=$ownerid AND posterid=$posterid");
2694 return fail($err, 408) if $modcount >= LJ::get_cap($uowner, "mod_queue_per_poster");
2696 $req->{'_moderate'}->{'authcode'} = LJ::make_auth_code(15);
2698 # create tag <lj-embed> from HTML-tag <embed>
2699 LJ::EmbedModule->parse_module_embed($uowner, \$req->{event});
2701 my $fr = $dbcm->quote(Storable::nfreeze($req));
2702 return fail($err, 409) if length($fr) > 200_000;
2704 # store
2705 my $modid = LJ::alloc_user_counter($uowner, "M");
2706 return fail($err, 501) unless $modid;
2708 $uowner->do("INSERT INTO modlog (journalid, modid, posterid, subject, logtime) ".
2709 "VALUES ($ownerid, $modid, $posterid, ?, NOW())", undef,
2710 LJ::text_trim($req->{'subject'}, 30, 0));
2711 return fail($err, 501) if $uowner->err;
2713 $uowner->do("INSERT INTO modblob (journalid, modid, request_stor) ".
2714 "VALUES ($ownerid, $modid, $fr)");
2715 if ($uowner->err) {
2716 $uowner->do("DELETE FROM modlog WHERE journalid=$ownerid AND modid=$modid");
2717 return fail($err, 501);
2720 # alert moderator(s), maintainers, owner
2721 my $mods = LJ::load_rel_user($dbh, $ownerid, 'M') || [];
2722 my $mains = LJ::load_rel_user($dbh, $ownerid, 'A') || [];
2723 my $super = LJ::load_rel_user($dbh, $ownerid, 'S') || [];
2724 my %mail_list = (map { $_ => 1 } (@$super, @$mods, @$mains));
2726 if (%mail_list) {
2727 # load up all these mods and figure out if they want email or not
2728 my $modlist = LJ::load_userids(keys %mail_list);
2730 my @emails;
2731 my $ct;
2732 foreach my $mod (values %$modlist) {
2733 last if $ct > 20; # don't send more than 20 emails.
2735 next unless $mod->is_visible;
2737 LJ::load_user_props($mod, 'opt_nomodemail');
2738 next if $mod->{opt_nomodemail};
2739 next if $mod->{status} ne "A";
2741 push @emails,
2743 to => $mod->email_raw,
2744 browselang => $mod->prop('browselang'),
2745 charset => $mod->mailencoding || 'utf-8',
2748 ++$ct;
2751 foreach my $to (@emails) {
2752 # TODO: html/plain text.
2753 my $body = LJ::Lang::get_text(
2754 $to->{'browselang'},
2755 'esn.moderated_submission.body', undef,
2757 user => $u->{'user'},
2758 subject => $req->{'subject'},
2759 community => $uowner->{'user'},
2760 modid => $modid,
2761 siteroot => $LJ::SITEROOT,
2762 sitename => $LJ::SITENAME,
2763 moderateurl => "$LJ::SITEROOT/community/moderate.bml?authas=$uowner->{'user'}&modid=$modid",
2764 viewurl => "$LJ::SITEROOT/community/moderate.bml?authas=$uowner->{'user'}",
2767 my $subject = LJ::Lang::get_text($to->{'browselang'},'esn.moderated_submission.subject');
2769 LJ::send_mail({
2770 'to' => $to->{to},
2771 'from' => $LJ::DONOTREPLY_EMAIL,
2772 'charset' => $to->{charset},
2773 'subject' => $subject,
2774 'body' => $body,
2779 my $msg = translate($u, "modpost", undef);
2780 return {
2781 'message' => $msg,
2782 xc3 => {
2783 u => $u,
2784 post => {
2785 coords => $req->{props}->{current_coords},
2786 has_images => ($req->{event} =~ /pics\.livejournal\.com/ ? 1 : 0),
2787 from_mobile => ($req->{event} =~ /m\.livejournal\.com/ ? 1 : 0)
2792 } # /moderated comms
2794 # posting:
2796 $getlock->();
2798 return $res if $res_done;
2800 # do rate-checking
2801 if ($u->{'journaltype'} ne "Y" && ! LJ::rate_log($u, "post", 1)) {
2802 return $fail->($err,405);
2805 my $jitemid = LJ::alloc_user_counter($uowner, "L");
2806 return $fail->($err,501,'xmlrpc.des.cannnot_generate_items') unless $jitemid;
2808 # bring in LJ::Entry with Class::Autouse
2809 LJ::Entry->can("dostuff");
2810 LJ::replycount_do($uowner, $jitemid, "init");
2812 # remove comments and logprops on new entry ... see comment by this sub for clarification
2813 LJ::Protocol::new_entry_cleanup_hack($u, $jitemid) if $LJ::NEW_ENTRY_CLEANUP_HACK;
2814 my $verb = $LJ::NEW_ENTRY_CLEANUP_HACK ? 'REPLACE' : 'INSERT';
2816 my $dberr;
2817 $uowner->log2_do(\$dberr, "INSERT INTO log2 (journalid, jitemid, posterid, eventtime, logtime, security, ".
2818 "allowmask, replycount, year, month, day, revttime, rlogtime, anum) ".
2819 "VALUES ($ownerid, $jitemid, $posterid, $qeventtime, FROM_UNIXTIME($now), $qsecurity, $qallowmask, ".
2820 "0, $req->{'year'}, $req->{'mon'}, $req->{'day'}, $LJ::EndOfTime-".
2821 "UNIX_TIMESTAMP($qeventtime), $rlogtime, $anum)");
2822 return $fail->($err,501,$dberr) if $dberr;
2824 # post become 'sticky post'
2825 if ( $req->{sticky} ) {
2826 $uowner->set_sticky_id($jitemid);
2827 my $state_date = POSIX::strftime("%Y-%m-%d", gmtime);
2829 my $postfix = '';
2830 if ($uowner->is_community) {
2831 $postfix = '_community';
2834 my $sticky_entry = "stat:sticky$postfix:$state_date";
2835 LJ::MemCache::incr($sticky_entry, 1) ||
2836 (LJ::MemCache::add($sticky_entry, 0), LJ::MemCache::incr($sticky_entry, 1));
2839 LJ::MemCache::incr([$ownerid, "log2ct:$ownerid"]);
2841 # set userprops.
2843 my %set_userprop;
2845 # keep track of itemid/anum for later potential duplicates
2846 $set_userprop{"dupsig_post"} = "$dupsig:$jitemid:$anum";
2848 # record the eventtime of the last update (for own journals only)
2849 $set_userprop{"newesteventtime"} = $eventtime
2850 if $posterid == $ownerid and not $req->{'props'}->{'opt_backdated'} and not $time_was_faked;
2852 $u->set_prop(\%set_userprop);
2855 # end duplicate locking section
2856 $release->();
2858 my $ditemid = $jitemid * 256 + $anum;
2860 ### finish embedding stuff now that we have the itemid
2862 ### this should NOT return an error, and we're mildly fucked by now
2863 ### if it does (would have to delete the log row up there), so we're
2864 ### not going to check it for now.
2866 my $error = "";
2867 foreach my $poll (@polls) {
2868 $poll->save_to_db(
2869 journalid => $ownerid,
2870 posterid => $posterid,
2871 ditemid => $ditemid,
2872 error => \$error,
2875 my $pollid = $poll->pollid;
2877 $event =~ s/<lj-poll-placeholder>/<lj-poll-$pollid>/;
2880 #### /embedding
2882 # record journal's disk usage
2883 my $bytes = length($event) + length($req->{'subject'});
2884 $uowner->dudata_set('L', $jitemid, $bytes);
2886 $uowner->do("$verb INTO logtext2 (journalid, jitemid, subject, event) ".
2887 "VALUES ($ownerid, $jitemid, ?, ?)", undef, $req->{'subject'},
2888 LJ::text_compress($event));
2889 if ($uowner->err) {
2890 my $msg = $uowner->errstr;
2891 LJ::delete_entry($uowner, $jitemid, undef, $anum); # roll-back
2892 return fail($err,501,"logtext:$msg");
2894 LJ::MemCache::set([$ownerid,"logtext:$clusterid:$ownerid:$jitemid"],
2895 [ $req->{'subject'}, $event ]);
2897 # keep track of custom security stuff in other table.
2898 if ($uselogsec) {
2899 $uowner->do("INSERT INTO logsec2 (journalid, jitemid, allowmask) ".
2900 "VALUES ($ownerid, $jitemid, $qallowmask)");
2901 if ($uowner->err) {
2902 my $msg = $uowner->errstr;
2903 LJ::delete_entry($uowner, $jitemid, undef, $anum); # roll-back
2904 return fail($err,501,"logsec2:$msg");
2908 # Entry tags
2909 if ($req->{props} && defined $req->{props}->{taglist}) {
2911 # slightly misnamed, the taglist is/was normally a string, but now can also be an arrayref.
2912 my $taginput = $req->{props}->{taglist};
2914 my $logtag_opts = {
2915 remote => $u,
2916 skipped_tags => [], # do all possible and report impossible
2919 if (ref $taginput eq 'ARRAY') {
2920 $logtag_opts->{set} = [@$taginput];
2921 $req->{props}->{taglist} = join(", ", @$taginput);
2922 } else {
2923 $logtag_opts->{set_string} = $taginput;
2926 my $rv = LJ::Tags::update_logtags($uowner, $jitemid, $logtag_opts);
2927 push @{$res->{warnings} ||= []}, LJ::Lang::ml('/update.bml.tags.skipped', { 'tags' => join(', ', @{$logtag_opts->{skipped_tags}}),
2928 'limit' => $uowner->get_cap('tags_max') } )
2929 if @{$logtag_opts->{skipped_tags}};
2932 ## copyright
2933 if (LJ::is_enabled('default_copyright', $u)) {
2934 $req->{'props'}->{'copyright'} = $u->prop('default_copyright')
2935 unless defined $req->{'props'}->{'copyright'};
2936 $req->{'props'}->{'copyright'} = 'P' # second try
2937 unless defined $req->{'props'}->{'copyright'};
2938 } else {
2939 delete $req->{'props'}->{'copyright'};
2942 ## give features
2943 if (LJ::is_enabled('give_features')) {
2944 $req->{'props'}->{'give_features'} = ($req->{'props'}->{'give_features'} eq 'enable') ? 1 :
2945 ($req->{'props'}->{'give_features'} eq 'disable') ? 0 :
2946 1; # LJSUP-9142: All users should be able to use give button
2949 my $entry = LJ::Entry->new($uowner, jitemid => $jitemid, anum => $anum);
2951 # meta-data
2952 if (%{$req->{'props'}}) {
2953 my $propset = {};
2955 foreach my $pname (keys %{$req->{'props'}}) {
2956 next unless $req->{'props'}->{$pname};
2957 next if $pname eq "revnum" || $pname eq "revtime";
2958 my $p = LJ::get_prop("log", $pname);
2959 next unless $p;
2960 next unless $req->{'props'}->{$pname};
2961 $propset->{$pname} = $req->{'props'}->{$pname};
2964 my %logprops;
2965 $entry->set_prop_multi( $propset, \%logprops );
2967 for my $key ( keys %logprops ) {
2968 next if $key =~ /^\d+$/;
2970 unless ( $LJ::CACHE_PROP{'log'}->{$key}->{'propid'} ) {
2971 delete $logprops{$key};
2973 else {
2974 $logprops{ $LJ::CACHE_PROP{'log'}->{$key}->{'propid'} } = delete $logprops{$key};
2978 # if set_prop_multi modified props above, we can set the memcache key
2979 # to be the hashref of modified props, since this is a new post
2980 LJ::MemCache::set([$uowner->{'userid'}, "logprop2:$uowner->{'userid'}:$jitemid"],
2981 \%logprops) if %logprops;
2984 # Paid Repost Offer
2985 if ($repost_offer) {
2986 my $error = '';
2988 $repost_offer->{jitemid} = $jitemid;
2990 my $offer_id = LJ::Pay::Repost::Offer->create(
2991 \$error,
2992 %$repost_offer,
2995 unless ( $offer_id ) {
2996 LJ::delete_entry($uowner, $jitemid, undef, $anum); # roll-back
2997 return fail($err,160,$error);
3001 $dbh->do("UPDATE userusage SET timeupdate=NOW(), lastitemid=$jitemid ".
3002 "WHERE userid=$ownerid") unless $flags->{'notimeupdate'};
3003 LJ::MemCache::set([$ownerid, "tu:$ownerid"], pack("N", time()), 30*60);
3005 # argh, this is all too ugly. need to unify more postpost stuff into async
3006 $u->invalidate_directory_record;
3008 # note this post in recentactions table
3009 LJ::note_recent_action($uowner, 'post');
3011 # if the post was public, and the user has not opted out, try to insert into the random table;
3012 # note we do INSERT INGORE since there will be lots of people posting every second, and that's
3013 # the granularity we use
3014 if ($security eq 'public' && LJ::u_equals($u, $uowner) && ! $u->prop('latest_optout')) {
3015 $u->do("INSERT IGNORE INTO random_user_set (posttime, userid) VALUES (UNIX_TIMESTAMP(), ?)",
3016 undef, $u->{userid});
3019 my @jobs; # jobs to add into TheSchwartz
3021 # notify weblogs.com of post if necessary
3022 if (!$LJ::DISABLED{'weblogs_com'} &&
3023 $u->{'opt_weblogscom'} &&
3024 LJ::get_cap($u, "weblogscom") &&
3025 $security eq "public" ) {
3026 push @jobs, TheSchwartz::Job->new_from_array("LJ::Worker::Ping::WeblogsCom", {
3027 'user' => $u->{'user'},
3028 'title' => $u->{'journaltitle'} || $u->{'name'},
3029 'url' => LJ::journal_base($u) . "/",
3033 my $ip = LJ::get_remote_ip();
3034 my $uniq = LJ::UniqCookie->current_uniq();
3036 LJ::EntriesIndex::update_index ($u, $ownerid, $ditemid, $ip, $uniq, $security)
3037 if $uowner->{'journaltype'} eq 'C';
3039 ## Counter "new_post" for monitoring
3040 LJ::run_hook ("update_counter", {
3041 counter => "new_post",
3044 # run local site-specific actions
3045 LJ::run_hooks("postpost", {
3046 'itemid' => $jitemid,
3047 'anum' => $anum,
3048 'journal' => $uowner,
3049 'poster' => $u,
3050 'event' => $event,
3051 'eventtime' => $eventtime,
3052 'subject' => $req->{'subject'},
3053 'security' => $security,
3054 'allowmask' => $qallowmask,
3055 'props' => $req->{'props'},
3056 'entry' => $entry,
3057 'jobs' => \@jobs, # for hooks to push jobs onto
3058 'req' => $req,
3059 'res' => $res,
3060 'entryrepost' => $flags->{'entryrepost'},
3061 'logtime' => $now,
3064 # cluster tracking
3065 LJ::mark_user_active($u, 'post');
3066 LJ::mark_user_active($uowner, 'post') unless LJ::u_equals($u, $uowner);
3068 $res->{'itemid'} = $jitemid; # by request of mart
3069 $res->{'anum'} = $anum;
3070 $res->{'ditemid'} = $ditemid;
3071 $res->{'url'} = $entry->url;
3073 if ($flags->{'entryrepost'}) {
3074 push @jobs, LJ::Event::JournalNewRepost->new($entry)->fire_job;
3075 } else {
3076 push @jobs, LJ::Event::JournalNewEntry->new($entry)->fire_job;
3078 if (!$LJ::DISABLED{'esn-userevents'} || $LJ::_T_FIRE_USERNEWENTRY) {
3079 push @jobs, LJ::Event::UserNewEntry->new($entry)->fire_job
3083 push @jobs, LJ::EventLogRecord::NewEntry->new($entry)->fire_job;
3085 # PubSubHubbub Support
3086 LJ::Feed::generate_hubbub_jobs($uowner, \@jobs) unless $uowner->is_syndicated;
3087 if (LJ::is_enabled('new_homepage_oftenread')) {
3088 push @jobs, TheSchwartz::Job->new(
3089 'funcname' => 'TheSchwartz::Worker::OftenRead',
3090 'arg' => {
3091 'journalid' => $uowner->userid,
3092 'jitemid' => $jitemid,
3097 my $sclient = LJ::theschwartz();
3098 if ($sclient && @jobs) {
3099 my @handles = $sclient->insert_jobs(@jobs);
3100 # TODO: error on failure? depends on the job I suppose? property of the job?
3103 $res->{xc3} = {
3104 u => $u,
3105 post => {
3106 url => $res->{url},
3107 coords => $req->{props}->{current_coords},
3108 has_images => ($req->{event} =~ /pics\.livejournal\.com/ ? 1 : 0),
3109 from_mobile => ($req->{event} =~ /m\.livejournal\.com/ ? 1 : 0)
3113 return $res;
3116 sub editevent {
3117 my ($req, $err, $flags) = @_;
3118 un_utf8_request($req);
3120 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'editevent');
3122 my $spam = 0;
3123 return undef unless LJ::run_hook('spam_detector', $req, \$spam);
3124 return fail($err,320) if $spam;
3126 # new rule from 14 march 2011: user is allowed to edit only if he is allowed to do new post
3127 # but is allowed to delete its own post
3128 return undef unless check_altusage($req, $err, $flags) or $req->{'event'} !~ /\S/;
3130 my $u = $flags->{'u'};
3132 # Ownerid - community/blog id
3133 my $ownerid = $flags->{'ownerid'};
3134 my $uowner = $flags->{'u_owner'} || $u;
3136 # Make sure we have a user object here
3137 $uowner = LJ::want_user($uowner) unless LJ::isu($uowner);
3138 my $clusterid = $uowner->{'clusterid'};
3140 # Posterid - the id of the author of the entry
3141 my $posterid = $u->{'userid'};
3142 my $qallowmask = $req->{'allowmask'}+0;
3143 my $sth;
3145 if ($uowner->is_community) {
3146 delete $req->{'props'}->{'opt_backdated'};
3149 my $itemid = $req->{'itemid'}+0;
3151 $itemid ||= int( ($req->{'ditemid'} + 0) / 256);
3153 # underage users can't do this
3154 return fail($err,310) if $u->underage;
3156 # check the journal's read-only bit
3157 return fail($err,306) if LJ::get_cap($uowner, "readonly");
3159 # can't edit in deleted/suspended community
3160 return fail($err,307) unless $uowner->{'statusvis'} eq "V" || $uowner->is_readonly;
3162 my $dbh = LJ::get_db_writer();
3163 my $dbcm = LJ::get_cluster_master($uowner);
3164 return fail($err,306) unless $dbcm && $dbh;
3166 unless ( LJ::is_enabled('new_friends_and_subscriptions') ) {
3167 # can't specify both a custom security and 'friends-only'
3168 return fail($err, 203, 'xmlrpc.des.friends_security')
3169 if $qallowmask > 1 && $qallowmask % 2;
3172 ### make sure user can't change a post to "custom/private security" on shared journals
3173 return fail($err,102)
3174 if ($ownerid != $posterid && # community post
3175 !($u && $u->can_edit_others_post($uowner)) && # it is not journal, there all members can edit any posts
3176 !($u && $u->can_manage($uowner)) && # poster is not admin
3177 ($req->{'security'} eq "private" ||
3178 ($req->{'security'} eq "usemask" && $qallowmask != 1 )));
3180 # make sure user can't change post in a certain community without being its member
3181 return fail($err,102)
3182 if ($LJ::JOURNALS_WITH_PROTECTED_CONTENT{ $uowner->{user} } &&
3183 !LJ::is_friend($uowner, $u));
3185 # make sure the new entry's under the char limit
3186 # NOTE: as in postevent, this requires $req->{event} to be binary data
3187 # but we've already removed the utf-8 flag in the XML-RPC path, and it
3188 # never gets set in the "flat" protocol path
3189 return fail($err, 409) if length($req->{event}) >= LJ::BMAX_EVENT;
3191 if ( $req->{ver} > 3 && LJ::is_enabled("delayed_entries") && $req->{delayedid} ) {
3192 my $delayedid = delete $req->{delayedid};
3193 my $res = {};
3195 if ( $delayedid ) {
3196 return fail( $err, 217 ) if $req->{itemid} || $req->{anum};
3197 return fail( $err, 215 ) unless $req->{tz};
3199 if ( $req->{repost_budget} || LJ::CleanHtml::Like->extract_repost_params(\$req->{event}) ) {
3200 return fail($err, 159);
3203 $req->{ext}->{flags} = $flags;
3204 $req->{usejournal} = $req->{usejournal} || '';
3206 my $entry = LJ::DelayedEntry->get_entry_by_id(
3207 $uowner,
3208 $delayedid,
3209 { userid => $posterid },
3212 return fail($err, 508) unless $entry;
3213 if ($req->{'event'} !~ /\S/ ) {
3214 $entry->delete();
3215 $res->{delayedid} = $delayedid;
3217 unless ( $flags->{'noauth'} ) {
3218 LJ::User::UserlogRecord::DeleteDelayedEntry->create(
3219 $uowner,
3220 'remote' => $u,
3221 'delayedid' => $delayedid,
3222 'method' => 'protocol',
3226 return $res;
3229 # updating an entry:
3230 return undef
3231 unless common_event_validation($req, $err, $flags);
3233 $entry->update($req);
3234 $res->{type} = 'delayed';
3235 $res->{delayedid} = $delayedid;
3238 return $res if $res->{type};
3241 if ( $req->{sticky} &&
3242 $uowner->is_community() &&
3243 !$u->can_manage($uowner) )
3245 return fail($err, 158);
3248 # don't moderate admins, moderators, pre-approved users & unsuspicious users
3249 my $is_unsuspicious_user = 0;
3250 LJ::run_hook('is_unsuspicious_user_in_comm', $posterid, \$is_unsuspicious_user);
3251 my $is_approved_user = LJ::RelationService->is_relation_type_to( $ownerid, $posterid, [ 'A','M','N' ] );
3252 unless ( $is_unsuspicious_user || $is_approved_user || !$uowner->check_non_whitelist_enabled() ) {
3254 my $entry = LJ::Entry->new($ownerid, jitemid => $itemid);
3255 my $modid_old = $entry->prop("mod_queue_id");
3257 my $need_moderated_old = 0;
3259 my $suspicious_list_old = {};
3260 LJ::run_hook('spam_community_detector', $uowner, { event => $entry->event_html }, \$need_moderated_old, $suspicious_list_old);
3262 my $need_moderated = 0;
3264 my $suspicious_list = {};
3265 LJ::run_hook('spam_community_detector', $uowner, $req, \$need_moderated, $suspicious_list);
3267 foreach ( keys %$suspicious_list_old ) {
3268 delete $suspicious_list->{$_};
3270 $need_moderated = scalar keys %$suspicious_list;
3272 if ($uowner->{'journaltype'} eq 'C' && !$flags->{'nomod'}) {
3275 if ($need_moderated) {
3277 $req->{'_moderate'}->{'authcode'} = LJ::make_auth_code(15);
3279 # create tag <lj-embed> from HTML-tag <embed>
3280 LJ::EmbedModule->parse_module_embed($uowner, \$req->{event});
3282 my $fr = $dbcm->quote(Storable::nfreeze($req));
3283 return fail($err, 409) if length($fr) > 200_000;
3285 # store
3286 my $modid = LJ::alloc_user_counter($uowner, "M");
3287 return fail($err, 501) unless $modid;
3289 $uowner->do("INSERT INTO modlog (journalid, modid, posterid, subject, logtime) ".
3290 "VALUES ($ownerid, $modid, $posterid, ?, NOW())", undef,
3291 LJ::text_trim($req->{'subject'}, 30, 0));
3292 return fail($err, 501) if $uowner->err;
3294 $uowner->do("INSERT INTO modblob (journalid, modid, request_stor) ".
3295 "VALUES ($ownerid, $modid, $fr)");
3296 if ($uowner->err) {
3297 $uowner->do("DELETE FROM modlog WHERE journalid=$ownerid AND modid=$modid");
3298 return fail($err, 501);
3301 if ($modid_old) {
3302 $uowner->do("DELETE FROM modlog WHERE journalid=$ownerid AND modid=$modid_old");
3303 return fail($err, 501) if $uowner->err;
3304 $uowner->do("DELETE FROM modblob WHERE journalid=$ownerid AND modid=$modid_old");
3305 return fail($err, 501) if $uowner->err;
3308 $entry->set_prop("mod_queue_id", $modid);
3310 my $suspicious_text = "";
3311 foreach ( sort keys %$suspicious_list ) {
3312 $suspicious_text .= " - $suspicious_list->{$_}->{type} - $suspicious_list->{$_}->{url}\n";
3315 # alert moderator(s), maintainers, owner
3316 my $mods = LJ::load_rel_user($dbh, $ownerid, 'M') || [];
3317 my $mains = LJ::load_rel_user($dbh, $ownerid, 'A') || [];
3318 my $super = LJ::load_rel_user($dbh, $ownerid, 'S') || [];
3319 my %mail_list = (map { $_ => 1 } (@$super, @$mods, @$mains));
3321 if (%mail_list) {
3322 # load up all these mods and figure out if they want email or not
3323 my $modlist = LJ::load_userids(keys %mail_list);
3325 my @emails;
3326 my $ct;
3327 foreach my $mod (values %$modlist) {
3328 last if $ct > 20; # don't send more than 20 emails.
3330 next unless $mod->is_visible;
3332 LJ::load_user_props($mod, 'opt_nomodemail');
3333 next if $mod->{opt_nomodemail};
3334 next if $mod->{status} ne "A";
3336 push @emails,
3338 to => $mod->email_raw,
3339 browselang => $mod->prop('browselang'),
3340 charset => $mod->mailencoding || 'utf-8',
3343 ++$ct;
3346 foreach my $to (@emails) {
3347 # TODO: html/plain text.
3348 my $body = LJ::Lang::get_text(
3349 $to->{'browselang'},
3350 'esn.moderated_edited_submission.body', undef,
3352 user => $u->{'user'},
3353 subject => $req->{'subject'},
3354 community => $uowner->{'user'},
3355 modid => $modid,
3356 siteroot => $LJ::SITEROOT,
3357 sitename => $LJ::SITENAME,
3358 moderateurl => "$LJ::SITEROOT/community/moderate.bml?authas=$uowner->{'user'}&modid=$modid",
3359 viewurl => "$LJ::SITEROOT/community/moderate.bml?authas=$uowner->{'user'}",
3360 susp_list => $suspicious_text,
3363 my $subject = LJ::Lang::get_text($to->{'browselang'},'esn.moderated_edited_submission.subject');
3365 LJ::send_mail({
3366 'to' => $to->{to},
3367 'from' => $LJ::DONOTREPLY_EMAIL,
3368 'charset' => $to->{charset},
3369 'subject' => $subject,
3370 'body' => $body,
3375 my $msg = translate($u, "modpost", undef);
3376 return {
3377 'message' => $msg,
3378 xc3 => {
3379 u => $u,
3380 post => {
3381 coords => $req->{props}->{current_coords},
3382 has_images => ($req->{event} =~ /pics\.livejournal\.com/ ? 1 : 0),
3383 from_mobile => ($req->{event} =~ /m\.livejournal\.com/ ? 1 : 0)
3387 } elsif ($modid_old) {
3388 $uowner->do("DELETE FROM modlog WHERE journalid=? and modid=?", undef, $ownerid, $modid_old);
3389 $uowner->do("DELETE FROM modblob WHERE journalid=? and modid=?", undef, $ownerid, $modid_old);
3390 $entry->set_prop("mod_queue_id", undef);
3395 # fetch the old entry from master database so we know what we
3396 # really have to update later. usually people just edit one part,
3397 # not every field in every table. reads are quicker than writes,
3398 # so this is worth it.
3399 my $oldevent = $dbcm->selectrow_hashref
3400 ("SELECT journalid AS 'ownerid', posterid, eventtime, logtime, ".
3401 "compressed, security, allowmask, year, month, day, ".
3402 "rlogtime, anum FROM log2 WHERE journalid=$ownerid AND jitemid=$itemid");
3404 ($oldevent->{subject}, $oldevent->{event}) = $dbcm->selectrow_array
3405 ("SELECT subject, event FROM logtext2 ".
3406 "WHERE journalid=$ownerid AND jitemid=$itemid");
3408 LJ::text_uncompress(\$oldevent->{'event'});
3410 # use_old_content indicates the subject and entry are not changing
3411 if ($flags->{'use_old_content'}) {
3412 $req->{'event'} = $oldevent->{event};
3413 $req->{'subject'} = $oldevent->{subject};
3416 # kill seconds in eventtime, since we don't use it, then we can use 'eq' and such
3417 $oldevent->{'eventtime'} =~ s/:00$//;
3419 ### make sure this user is allowed to edit this entry
3420 return fail($err,302)
3421 unless ($ownerid == $oldevent->{'ownerid'});
3423 ### what can they do to somebody elses entry? (in shared journal)
3424 ### can edit it if they own or maintain the journal, but not if the journal is read-only
3425 if ($posterid != $oldevent->{'posterid'} || $u->is_readonly || $uowner->is_readonly) {
3426 ## deleting.
3427 return fail($err,304)
3428 if ($req->{'event'} !~ /\S/ && !
3429 ($ownerid == $u->{'userid'} ||
3430 # community account can delete it (ick)
3432 LJ::can_manage_other($posterid, $ownerid) ||
3433 # if user is a community maintainer they can delete
3434 # it too (good)
3436 $u->can_edit_others_post($uowner)
3439 ## editing:
3440 if ($req->{'event'} =~ /\S/) {
3441 return fail($err,303) if $posterid != $oldevent->{'posterid'} && !$u->can_edit_others_post($uowner);
3442 return fail($err,318) if $u->is_readonly;
3443 return fail($err,319) if $uowner->is_readonly;
3447 # simple logic for deleting an entry
3448 if (!$flags->{'use_old_content'} && $req->{'event'} !~ /\S/) {
3449 ## 23.11.2009. Next code added due to some hackers activities
3450 ## that use trojans to delete user's entries in theirs journals.
3451 if ($LJ::DELETING_ENTRIES_IS_DISABLED
3452 && $u->is_person and $u->userid eq $oldevent->{ownerid}
3454 my $qsecurity = $uowner->quote('private');
3455 my $dberr;
3456 LJ::run_hooks('report_entry_update', $ownerid, $itemid, { write_to_userlog => 1 } );
3457 $uowner->log2_do(\$dberr, "UPDATE log2 SET security=$qsecurity " .
3458 "WHERE journalid=$ownerid AND jitemid=$itemid");
3459 return fail($err,501,$dberr) if $dberr;
3460 return fail($err, 321);
3463 # if their newesteventtime prop equals the time of the one they're deleting
3464 # then delete their newesteventtime.
3465 if ($u->{'userid'} == $uowner->{'userid'}) {
3466 LJ::load_user_props($u, { use_master => 1 }, "newesteventtime");
3467 if ($u->{'newesteventtime'} eq $oldevent->{'eventtime'}) {
3468 $u->clear_prop('newesteventtime');
3472 # log this event, unless noauth is on, which means it is being done internally and we should
3473 # rely on them to log why they're deleting the entry if they need to. that way we don't have
3474 # double entries, and we have as much information available as possible at the location the
3475 # delete is initiated.
3476 unless ( $flags->{'noauth'} ) {
3477 LJ::User::UserlogRecord::DeleteEntry->create( $uowner,
3478 'remote' => $u,
3479 'ditemid' => $itemid * 256 + $oldevent->{'anum'},
3480 'method' => 'protocol',
3484 # We must use property 'dupsig_post' in author of entry to be deleted, not in
3485 # remote user or journal owner!
3486 my $item = LJ::get_log2_row($uowner, $itemid);
3487 my $poster = $item ? LJ::want_user($item->{'posterid'}) : '';
3489 if ($req->{delspam}) {
3490 if ($uowner) {
3491 LJ::mark_entry_as_spam($uowner, $itemid);
3493 if ($poster) {
3494 if (my $remote = LJ::get_remote()) {
3495 LJ::User::UserlogRecord::SpamSet->create(
3496 $uowner,
3497 remote => $remote,
3498 spammerid => $poster->userid,
3502 $uowner->ban_user($poster);
3503 LJ::set_rel($uowner, $poster, 'D');
3508 LJ::delete_entry($uowner, $itemid, 'quick', $oldevent->{'anum'});
3510 # clear their duplicate protection, so they can later repost
3511 # what they just deleted. (or something... probably rare.)
3512 $poster->clear_prop('dupsig_post') if $poster && LJ::get_cluster_reader($poster);
3514 my $res = {
3515 'itemid' => $itemid,
3516 'anum' => $oldevent->{'anum'},
3517 xc3 => {
3518 u => $u
3522 if ( $itemid == $uowner->get_sticky_entry_id() ) {
3523 $uowner->remove_sticky_entry_id();
3526 $dbh->do("UPDATE userusage SET timeupdate=NOW() ".
3527 "WHERE userid=$ownerid");
3528 LJ::MemCache::set([$ownerid, "tu:$ownerid"], pack("N", time()), 30*60);
3530 return $res;
3533 # now make sure the new entry text isn't $CannotBeShown
3534 return fail($err, 210)
3535 if $req->{event} eq $CannotBeShown;
3537 if (!LJ::is_enabled("delayed_entries")) {
3538 # don't allow backdated posts in communities
3539 return fail($err,152) if
3540 ($req->{'props'}->{"opt_backdated"} &&
3541 $uowner->{'journaltype'} ne "P");
3544 # make year/mon/day/hour/min optional in an edit event,
3545 # and just inherit their old values
3547 $oldevent->{'eventtime'} =~ /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d)/;
3548 $req->{'year'} = $1 unless defined $req->{'year'};
3549 $req->{'mon'} = $2+0 unless defined $req->{'mon'};
3550 $req->{'day'} = $3+0 unless defined $req->{'day'};
3551 $req->{'hour'} = $4+0 unless defined $req->{'hour'};
3552 $req->{'min'} = $5+0 unless defined $req->{'min'};
3555 # updating an entry:
3556 return undef
3557 unless common_event_validation($req, $err, $flags);
3559 ### load existing meta-data
3560 my %curprops;
3561 LJ::load_log_props2($dbcm, $ownerid, [ $itemid ], \%curprops);
3564 # create, edit, revoke repost offer
3565 my ($repost_offer, $repost_offer_action);
3567 if (LJ::is_enabled("paid_repost") && $req->{'event'} =~ /\S/) {
3568 my $error;
3570 ($repost_offer, $repost_offer_action) = LJ::Pay::Repost::Offer->from_edit_entry(
3571 \$req->{event},
3573 current => $curprops{$itemid}->{repost_offer},
3574 userid => $posterid,
3575 journalid => $ownerid,
3576 jitemid => $itemid,
3577 budget => $req->{repost_budget},
3578 limit_sc => $req->{repost_limit_sc},
3579 revoke => !$req->{paid_repost_on},
3580 targeting_gender => $req->{repost_targeting_gender},
3581 targeting_age => $req->{repost_targeting_age},
3582 targeting_country => $req->{repost_targeting_country},
3583 targeting_state => $req->{repost_targeting_state}
3585 \$error,
3588 unless ($flags->{noauth}) {
3589 # cannot create or edit repost offer via api
3590 return fail($err,222) if $repost_offer && $repost_offer_action =~ /create|edit/;
3592 # do not revoke repost offer via api
3593 undef $repost_offer if $repost_offer && $repost_offer_action =~ /revoke/;
3596 return fail($err,160,$error) if $error;
3599 # make post sticky
3600 if ( $req->{sticky} ) {
3601 if( $uowner->get_sticky_entry_id() != $itemid ) {
3602 $uowner->set_sticky_id($itemid);
3604 my $state_date = POSIX::strftime("%Y-%m-%d", gmtime);
3605 my $postfix = '';
3606 if ($uowner->is_community) {
3607 $postfix = '_community';
3610 my $sticky_entry = "stat:sticky$postfix:$state_date";
3611 LJ::MemCache::incr($sticky_entry, 1) ||
3612 (LJ::MemCache::add($sticky_entry, 0), LJ::MemCache::incr($sticky_entry, 1));
3615 elsif ( $itemid == $uowner->get_sticky_entry_id() ) {
3616 $uowner->remove_sticky_entry_id();
3619 ## give features
3620 my $give_features = $req->{'props'}->{'give_features'};
3621 if ($give_features) {
3622 $req->{'props'}->{'give_features'} = ($give_features eq 'enable') ? 1 : 0;
3625 my $event = $req->{'event'};
3626 my $owneru = LJ::load_userid($ownerid);
3627 $event = LJ::EmbedModule->transform_rte_post($event);
3628 LJ::EmbedModule->parse_module_embed($owneru, \$event);
3630 my $bytes = length($event) + length($req->{'subject'});
3632 my $eventtime = sprintf("%04d-%02d-%02d %02d:%02d",
3633 map { $req->{$_} } qw(year mon day hour min));
3634 my $qeventtime = $dbcm->quote($eventtime);
3636 # preserve old security by default, use user supplied if it's understood
3637 my $security = $oldevent->{security};
3638 $security = $req->{security}
3639 if $req->{security} &&
3640 $req->{security} =~ /^(?:public|private|usemask)$/;
3642 $qallowmask = $oldevent->{allowmask} unless defined $req->{'allowmask'};
3644 my $do_tags = $req->{props} && defined $req->{props}->{taglist};
3645 if ($oldevent->{security} ne $security || $qallowmask != $oldevent->{allowmask}) {
3646 # FIXME: this is a hopefully temporary hack which deletes tags from the entry
3647 # when the security has changed. the real fix is to make update_logtags aware
3648 # of security changes so it can update logkwsum appropriately.
3650 unless ($do_tags) {
3651 # we need to fix security on this entry's tags, but the user didn't give us a tag list
3652 # to work with, so we have to go get the tags on the entry, and construct a tag list,
3653 # in order to pass to update_logtags down at the bottom of this whole update
3654 my $tags = LJ::Tags::get_logtags($uowner, $itemid);
3655 $tags = $tags->{$itemid};
3656 $req->{props}->{taglist} = join(',', sort values %{$tags || {}});
3657 $do_tags = 1; # bleh, force the update later
3660 LJ::Tags::delete_logtags($uowner, $itemid);
3663 my $qyear = $req->{'year'}+0;
3664 my $qmonth = $req->{'mon'}+0;
3665 my $qday = $req->{'day'}+0;
3667 if ($eventtime ne $oldevent->{'eventtime'} ||
3668 $security ne $oldevent->{'security'} ||
3669 (!$curprops{$itemid}->{opt_backdated} && $req->{props}{opt_backdated}) ||
3670 $qallowmask != $oldevent->{'allowmask'})
3672 # are they changing their most recent post?
3673 LJ::load_user_props($u, "newesteventtime");
3674 if ($u->{userid} == $uowner->{userid} &&
3675 $u->{newesteventtime} eq $oldevent->{eventtime}) {
3676 if (!$curprops{$itemid}->{opt_backdated} && $req->{props}{opt_backdated}) {
3677 # if they set the backdated flag, then we no longer know
3678 # the newesteventtime.
3679 $u->clear_prop('newesteventtime');
3680 } elsif ($eventtime ne $oldevent->{eventtime}) {
3681 # otherwise, if they changed time on this event,
3682 # the newesteventtime is this event's new time.
3683 $u->set_prop( 'newesteventtime' => $eventtime );
3687 my $qsecurity = $uowner->quote($security);
3688 my $dberr;
3689 LJ::run_hooks('report_entry_update', $ownerid, $itemid, { write_to_userlog => 1 } );
3690 $uowner->log2_do(\$dberr, "UPDATE log2 SET eventtime=$qeventtime, revttime=$LJ::EndOfTime-".
3691 "UNIX_TIMESTAMP($qeventtime), year=$qyear, month=$qmonth, day=$qday, ".
3692 "security=$qsecurity, allowmask=$qallowmask WHERE journalid=$ownerid ".
3693 "AND jitemid=$itemid");
3694 return fail($err,501,$dberr) if $dberr;
3696 # update memcached
3697 my $sec = $qallowmask;
3698 $sec = 0 if $security eq 'private';
3699 $sec = 2**31 if $security eq 'public';
3701 my $row = pack("NNNNN", $oldevent->{'posterid'},
3702 LJ::TimeUtil->mysqldate_to_time($eventtime, 1),
3703 LJ::TimeUtil->mysqldate_to_time($oldevent->{'logtime'}, 1),
3704 $sec,
3705 $itemid*256 + $oldevent->{'anum'});
3707 LJ::MemCache::set([$ownerid, "log2:$ownerid:$itemid"], $row);
3708 LJ::Entry->reset_singletons; ## flush cached LJ::Entry objects
3711 if ($security ne $oldevent->{'security'} ||
3712 $qallowmask != $oldevent->{'allowmask'})
3714 if ($security eq "public" || $security eq "private") {
3715 $uowner->do("DELETE FROM logsec2 WHERE journalid=$ownerid AND jitemid=$itemid");
3717 else {
3718 $uowner->do("REPLACE INTO logsec2 (journalid, jitemid, allowmask) ".
3719 "VALUES ($ownerid, $itemid, $qallowmask)");
3721 return fail($err,501,$dbcm->errstr) if $uowner->err;
3724 LJ::MemCache::set([$ownerid,"logtext:$clusterid:$ownerid:$itemid"],
3725 [ $req->{'subject'}, $event ]);
3727 if (!$flags->{'use_old_content'} && (
3728 $event ne $oldevent->{'event'} ||
3729 $req->{'subject'} ne $oldevent->{'subject'}))
3731 LJ::run_hooks('report_entry_text_update', $ownerid, $itemid);
3732 if ( $oldevent->{'posterid'} != $ownerid ) {
3733 LJ::User::UserlogRecord::ChangeEntryText->create(LJ::load_userid($ownerid), journalid => $ownerid, jitemid => $itemid );
3735 $uowner->do("UPDATE logtext2 SET subject=?, event=? ".
3736 "WHERE journalid=$ownerid AND jitemid=$itemid", undef,
3737 $req->{'subject'}, LJ::text_compress($event));
3738 return fail($err,501,$uowner->errstr) if $uowner->err;
3740 # update disk usage
3741 $uowner->dudata_set('L', $itemid, $bytes);
3744 # up the revision number
3745 $req->{'props'}->{'revnum'} = ($curprops{$itemid}->{'revnum'} || 0) + 1;
3746 $req->{'props'}->{'revtime'} = time();
3748 my $res = { 'itemid' => $itemid };
3750 # update or create repost offer
3751 if ($repost_offer) {
3752 my ($error, $warning);
3754 if($repost_offer_action eq 'create') {
3756 my $offer_id = LJ::Pay::Repost::Offer->create(\$error, %$repost_offer) or
3757 fail(\$warning,160,$error);
3759 } elsif($repost_offer_action eq 'edit') {
3760 $repost_offer->edit(\$error,
3761 add_budget => $repost_offer->{add_budget},
3762 limit_sc => $repost_offer->{limit_sc},
3763 targeting_opt => $repost_offer->{targeting_opt},
3764 ) or fail(\$warning,160,$error);
3766 } elsif($repost_offer_action eq 'revoke') {
3768 $repost_offer->revoke(\$error) or
3769 fail(\$warning,161,$error);
3772 push @{$res->{warnings} ||= []}, error_message($warning) if $warning;
3776 # handle tags if they're defined
3777 if ($do_tags) {
3778 my $tagerr = "";
3779 my $skipped_tags = [];
3780 my $rv = LJ::Tags::update_logtags($uowner, $itemid, {
3781 set_string => $req->{props}->{taglist},
3782 remote => $u,
3783 err_ref => \$tagerr,
3784 skipped_tags => $skipped_tags, # do all possible and report impossible
3786 push @{$res->{warnings} ||= []}, LJ::Lang::ml('/update.bml.tags.skipped', { 'tags' => join(', ', @$skipped_tags),
3787 'limit' => $uowner->get_cap('tags_max') } )
3788 if @$skipped_tags;
3791 if (LJ::is_enabled('default_copyright', $u)) {
3792 unless (defined $req->{'props'}->{'copyright'}) { # try 1: previous value
3793 $req->{'props'}->{'copyright'} = $curprops{$itemid}->{'copyright'};
3796 unless (defined $req->{'props'}->{'copyright'}) { # try 2: global setting
3797 $req->{'props'}->{'copyright'} = $uowner->prop('default_copyright');
3800 unless (defined $req->{'props'}->{'copyright'}) { # try 3: allow
3801 $req->{'props'}->{'copyright'} = 'P';
3804 else { # disabled feature
3805 delete $req->{'props'}->{'copyright'};
3808 my $entry = LJ::Entry->new($ownerid, jitemid => $itemid);
3810 # handle the props
3812 my $propset = {};
3813 foreach my $pname (keys %{$req->{'props'}}) {
3814 my $p = LJ::get_prop("log", $pname);
3815 next unless $p;
3816 $propset->{$pname} = $req->{'props'}->{$pname};
3818 $entry->set_prop_multi($propset);
3820 if ($req->{'props'}->{'copyright'} ne $curprops{$itemid}->{'copyright'}) {
3821 LJ::Entry->new($ownerid, jitemid => $itemid)->put_logprop_in_history('copyright', $curprops{$itemid}->{'copyright'},
3822 $req->{'props'}->{'copyright'});
3826 # compatible with depricated 'opt_backdated'
3827 if ($req->{'props'}->{'opt_backdated'} eq "1" &&
3828 $oldevent->{'rlogtime'} != $LJ::EndOfTime) {
3829 my $dberr;
3830 LJ::run_hooks('report_entry_update', $ownerid, $itemid, { write_to_userlog => 1 } );
3831 $uowner->log2_do(undef, "UPDATE log2 SET rlogtime=$LJ::EndOfTime WHERE ".
3832 "journalid=$ownerid AND jitemid=$itemid");
3833 return fail($err,501,$dberr) if $dberr;
3836 if ($req->{'props'}->{'opt_backdated'} eq "0" &&
3837 $oldevent->{'rlogtime'} == $LJ::EndOfTime) {
3838 my $dberr;
3839 LJ::run_hooks('report_entry_update', $ownerid, $itemid, { write_to_userlog => 1 } );
3840 $uowner->log2_do(\$dberr, "UPDATE log2 SET rlogtime=$LJ::EndOfTime-UNIX_TIMESTAMP(logtime) ".
3841 "WHERE journalid=$ownerid AND jitemid=$itemid");
3842 return fail($err,501,$dberr) if $dberr;
3844 return fail($err,501,$dbcm->errstr) if $dbcm->err;
3846 if (defined $oldevent->{'anum'}) {
3847 $res->{'anum'} = $oldevent->{'anum'};
3848 $res->{'url'} = LJ::item_link($uowner, $itemid, $oldevent->{'anum'});
3849 $res->{'ditemid'} = $itemid * 256 + $oldevent->{'anum'};
3852 $dbh->do("UPDATE userusage SET timeupdate=NOW() ".
3853 "WHERE userid=$ownerid");
3854 LJ::MemCache::set([$ownerid, "tu:$ownerid"], pack("N", time()), 30*60);
3856 LJ::EventLogRecord::EditEntry->new($entry)->fire;
3857 my @jobs; # jobs to insert into TheSchwartz
3858 LJ::run_hooks("editpost", $entry, $oldevent, \@jobs);
3860 # PubSubHubbub Support
3861 LJ::Feed::generate_hubbub_jobs($uowner, \@jobs) unless $uowner->is_syndicated;
3863 my $sclient = LJ::theschwartz();
3864 if ($sclient && @jobs) {
3865 my @handles = $sclient->insert_jobs(@jobs);
3866 # TODO: error on failure? depends on the job I suppose? property of the job?
3869 $res->{xc3} = {
3870 u => $u,
3871 post => {
3872 url => $res->{url},
3873 coords => $req->{props}->{current_coords},
3874 has_images => ($req->{event} =~ /pics\.livejournal\.com/ ? 1 : 0),
3875 from_mobile => ($req->{event} =~ /m\.livejournal\.com/ ? 1 : 0)
3879 return $res;
3882 sub getevents {
3883 my ($req, $err, $flags) = @_;
3885 $flags->{allow_anonymous} = 1;
3886 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getevents');
3888 $flags->{'ignorecanuse'} = 1; # later we will check security levels, so allow some access to communities
3889 return undef unless check_altusage($req, $err, $flags);
3891 my $u = $flags->{'u'};
3892 my $uowner = $flags->{'u_owner'} || $u;
3894 ### shared-journal support
3895 my $posterid = ($u ? $u->{'userid'} : 0);
3896 my $ownerid = $flags->{'ownerid'};
3898 if( $req->{journalid} ){
3899 $ownerid = $req->{journalid};
3900 $uowner = LJ::load_userid( $req->{journalid} );
3903 my $sticky_id = $uowner->prop("sticky_entry_id") || undef;
3904 my $dbr = LJ::get_db_reader();
3905 my $sth;
3907 # can't pull events from deleted/suspended journal
3908 return fail($err, 307) unless $uowner->{'statusvis'} eq "V" || $uowner->is_readonly;
3910 my $dbcr = LJ::get_cluster_reader($uowner);
3911 return fail($err, 502) unless $dbcr && $dbr;
3913 my $reject_code = $LJ::DISABLE_PROTOCOL{getevents};
3915 if (ref $reject_code eq "CODE") {
3916 my $errmsg = $reject_code->($req, $flags, eval { LJ::request->request });
3918 return fail($err, "311", $errmsg) if $errmsg;
3921 my $can_manage = $u && $u->can_manage($uowner);
3922 my $secmask = 0;
3924 if ($u && ($u->{'journaltype'} eq "P" || $u->{'journaltype'} eq "I") && $posterid != $ownerid) {
3925 $secmask = LJ::get_groupmask($ownerid, $posterid);
3928 # decide what level of security the remote user can see
3929 # 'getevents' used in small count of places and we will not pass 'viewall' through their call chain
3930 my $secwhere = "";
3932 if ($can_manage) {
3933 # no extra where restrictions... user can see all their own stuff
3935 elsif ($secmask) {
3936 # can see public or things with them in the mask
3937 # and own posts in non-sensitive communities
3938 if ($LJ::JOURNALS_WITH_PROTECTED_CONTENT{ $uowner->{user} }) {
3939 $secwhere = "AND (security='public' OR (security='usemask' AND allowmask & $secmask != 0))";
3941 else {
3942 $secwhere = "AND (security='public' OR (security='usemask' AND allowmask & $secmask != 0) OR posterid=$posterid)";
3945 else {
3946 # not a friend? only see public.
3947 # and own posts in non-sensitive communities
3948 if ($LJ::JOURNALS_WITH_PROTECTED_CONTENT{ $uowner->{user} } || !$posterid) {
3949 $secwhere = "AND (security='public')";
3951 else{
3952 $secwhere = "AND (security='public' OR posterid=$posterid)";
3956 # if this is on, we sort things different (logtime vs. posttime)
3957 # to avoid timezone issues
3958 my $is_community = ($uowner->{'journaltype'} eq "C" ||
3959 $uowner->{'journaltype'} eq "S");
3961 # in some cases we'll use the master, to ensure there's no
3962 # replication delay. useful cases: getting one item, use master
3963 # since user might have just made a typo and realizes it as they
3964 # post, or wants to append something they forgot, etc, etc. in
3965 # other cases, slave is pretty sure to have it.
3966 my $use_master = 0;
3968 # just synonym
3969 if ($req->{'itemshow'}){
3970 $req->{'selecttype'} = 'lastn' unless $req->{'selecttype'};
3971 $req->{'howmany'} = $req->{'itemshow'};
3974 my $skip = $req->{'skip'} + 0;
3976 $skip = 500 if $skip > 500;
3978 my $sort_order = $req->{'sort_order'};
3979 $sort_order = ($sort_order && $sort_order =~ /asc|desc|default/ ? $sort_order : 'default');
3981 if ( $req->{ver} > 3 && LJ::is_enabled("delayed_entries") ) {
3982 my $res = {};
3984 if ( $req->{delayed} ) {
3985 return fail( $err, 220 ) if $req->{view} && $req->{view} ne 'stored';
3987 if ( $req->{selecttype} eq 'lastn' ) {
3988 my $uid = $u->userid;
3989 my $howmany = $req->{'howmany'} || 20;
3990 if ($howmany > 50) { $howmany = 50; }
3992 my $ids = LJ::DelayedEntry->get_entries_by_journal(
3993 $uowner,
3994 { 'skip' => $req->{skip} || 0,
3995 'show' => $howmany,
3996 'userid' => $uid, });
3998 for my $did ( @$ids ) {
3999 my $entry = LJ::DelayedEntry->get_entry_by_id(
4000 $uowner,
4001 $did,
4002 { 'userid' => $uid, },
4005 if (!$entry) {
4006 next;
4009 my $re = {};
4011 $re->{$_} = $entry->$_ for qw(delayedid subject event logtime);
4012 my $props = $entry->props;
4013 foreach my $key (keys %$props) {
4014 if (!$props->{$key}) {
4015 delete $props->{$key};
4019 $re->{props} = $props;
4020 $re->{eventtime} = $entry->posttime;
4021 $re->{event_timestamp} = $entry->system_posttime;
4022 $re->{url} = $entry->url;
4023 $re->{security} = $entry->security;
4024 $re->{allowmask} = $entry->allowmask;
4025 $re->{posterid} = $entry->poster->userid;
4026 $re->{poster} = $entry->poster->username;
4028 push @{$res->{events}}, $re;
4031 elsif ( $req->{selecttype} eq 'one' ) {
4032 return fail( $err, 218) unless $req->{delayedid};
4033 my $uid = $u->userid;
4035 my $entry = LJ::DelayedEntry->get_entry_by_id(
4036 $uowner,
4037 $req->{delayedid},
4038 { 'userid' => $uid, },
4041 my $re = {};
4043 if (!$entry) {
4044 next;
4047 $re->{$_} = $entry->$_ for qw(delayedid subject event logtime);
4048 my $props = $entry->props;
4049 foreach my $key (keys %$props) {
4050 if (!$props->{$key}) {
4051 delete $props->{$key};
4055 $re->{props} = $props;
4056 $re->{eventtime} = $entry->posttime;
4057 $re->{event_timestamp} = $entry->system_posttime;
4058 $re->{url} = $entry->url;
4059 $re->{security} = $entry->security;
4060 $re->{allowmask} = $entry->allowmask;
4061 $re->{posterid} = $entry->poster->userid;
4062 $re->{poster} = $entry->poster->username;
4064 push @{$res->{events}}, $re;
4065 } elsif ( $req->{selecttype} eq 'multiple' ) {
4066 return fail( $err, 218) unless $req->{delayedids};
4067 my $uid = $u->userid;
4070 for my $did ( @{$req->{delayedids} }) {
4071 my $entry = LJ::DelayedEntry->get_entry_by_id(
4072 $uowner,
4073 $did,
4074 { 'userid' => $uid, },
4077 if (!$entry) {
4078 next;
4081 my $re = {};
4083 $re->{$_} = $entry->$_ for qw(delayedid subject event logtime);
4084 my $props = $entry->props;
4085 foreach my $key (keys %$props) {
4086 if (!$props->{$key}) {
4087 delete $props->{$key};
4091 $re->{props} = $props;
4092 $re->{eventtime} = $entry->posttime;
4093 $re->{event_timestamp} = $entry->system_posttime;
4094 $re->{url} = $entry->url;
4095 $re->{security} = $entry->security;
4096 $re->{allowmask} = $entry->allowmask;
4097 $re->{posterid} = $entry->poster->userid;
4098 $re->{poster} = $entry->poster->username;
4099 push @{$res->{events}}, $re;
4102 else {
4103 return fail( $err, 218 );
4106 return $res;
4110 # build the query to get log rows. each selecttype branch is
4111 # responsible for either populating the following 3 variables
4112 # OR just populating $sql
4113 my ($orderby, $where, $limit, $offset);
4114 my $sql;
4116 if ($req->{'selecttype'} eq "day") {
4117 return fail($err,203)
4118 unless ($req->{'year'} =~ /^\d\d\d\d$/ &&
4119 $req->{'month'} =~ /^\d\d?$/ &&
4120 $req->{'day'} =~ /^\d\d?$/ &&
4121 $req->{'month'} >= 1 && $req->{'month'} <= 12 &&
4122 $req->{'day'} >= 1 && $req->{'day'} <= 31);
4124 my $qyear = $dbr->quote($req->{'year'});
4125 my $qmonth = $dbr->quote($req->{'month'});
4126 my $qday = $dbr->quote($req->{'day'});
4127 $where = "AND year=$qyear AND month=$qmonth AND day=$qday";
4128 $limit = "LIMIT 200"; # FIXME: unhardcode this constant (also in ljviews.pl)
4130 # see note above about why the sort order is different
4131 $orderby = $is_community ? "ORDER BY logtime" : "ORDER BY eventtime";
4133 elsif ($req->{'selecttype'} eq "lastn") {
4134 my $howmany = $req->{'howmany'} || 20;
4136 if ($howmany > 50) { $howmany = 50; }
4138 $howmany = $howmany + 0;
4139 $limit = "LIMIT $howmany";
4141 $offset = "OFFSET $skip";
4143 # okay, follow me here... see how we add the revttime predicate
4144 # even if no beforedate key is present? you're probably saying,
4145 # that's retarded -- you're saying: "revttime > 0", that's like
4146 # saying, "if entry occurred at all." yes yes, but that hints
4147 # mysql's braindead optimizer to use the right index.
4148 my $rtime_after = 0;
4149 my $rtime_what = $is_community ? "rlogtime" : "revttime";
4151 if ($req->{'beforedate'}) {
4152 return fail($err,203,'xmlrpc.des.bad_value',{'param'=>'beforedate'})
4153 unless ($req->{'beforedate'} =~
4154 /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/);
4155 my $qd = $dbr->quote($req->{'beforedate'});
4156 $rtime_after = "$LJ::EndOfTime-UNIX_TIMESTAMP($qd)";
4159 $where .= "AND $rtime_what > $rtime_after ";
4160 $orderby = "ORDER BY $rtime_what";
4161 unless ($sort_order eq 'default') {
4162 $orderby .= ' '.uc($sort_order);
4165 unless ($skip) {
4166 $where .= "OR ( journalid=$ownerid $secwhere $where AND jitemid=$sticky_id)" if defined $sticky_id;
4169 elsif ($req->{'selecttype'} eq "one" && ($req->{'itemid'} eq "-1" || $req->{'ditemid'} eq "-1")) {
4170 $use_master = 1; # see note above.
4171 $limit = "LIMIT 1";
4172 $orderby = "ORDER BY rlogtime";
4174 elsif ($req->{'selecttype'} eq "one") {
4175 $req->{'itemid'} = int(($req->{'ditemid'} + 0) / 256) unless($req->{'itemid'});
4176 my $id = $req->{'itemid'} + 0;
4177 $where = "AND jitemid=$id";
4179 elsif ($req->{'selecttype'} eq "syncitems") {
4180 return fail($err, 506) if $LJ::DISABLED{'syncitems'};
4182 my $date = $req->{'lastsync'} || "0000-00-00 00:00:00";
4183 return fail($err, 203, 'xmlrpc.des.bad_value',{'param'=>'syncitems'})
4184 unless ($date =~ /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/);
4186 return fail($err, 301, 'xmlrpc.des.syncitems_unavailable') unless($uowner || $u);
4188 my $now = time();
4190 # broken client loop prevention
4191 # TODO: Just add rate limits here instead of that old stuff
4192 my $u_req = ($u ? $u : $uowner);
4193 # if ($req->{'lastsync'}) {
4194 # my $pname = "rl_syncitems_getevents_loop";
4195 # LJ::load_user_props($u_req, $pname);
4197 # # format is: time/date/time/date/time/date/... so split
4198 # # it into a hash, then delete pairs that are older than an hour
4199 # my %reqs = split(m!/!, $u_req->{$pname});
4201 # foreach (grep { $_ < $now - 60*60 } keys %reqs) { delete $reqs{$_}; }
4202 # my $count = grep { $_ eq $date } values %reqs;
4203 # $reqs{$now} = $date;
4205 # if ($count >= 2) {
4206 # # 2 prior, plus this one = 3 repeated requests for same synctime.
4207 # # their client is busted. (doesn't understand syncitems semantics)
4208 # return fail($err,406);
4211 # $u_req->set_prop( $pname => join( '/', map { $_ => $reqs{$_} }
4212 # sort { $b <=> $a }
4213 # keys %reqs ) );
4216 my %item;
4217 $sth = $dbcr->prepare("SELECT jitemid, logtime FROM log2 WHERE ".
4218 "journalid=? and logtime > ? $secwhere");
4219 $sth->execute($ownerid, $date);
4221 while (my ($id, $dt) = $sth->fetchrow_array) {
4222 $item{$id} = $dt;
4225 if ( $req->{'only_count'} ) {
4226 return {
4227 count => scalar keys %item,
4231 my $p_revtime = LJ::get_prop("log", "revtime");
4232 $sth = $dbcr->prepare("SELECT jitemid, FROM_UNIXTIME(value) ".
4233 "FROM logprop2 WHERE journalid=? ".
4234 "AND propid=$p_revtime->{'id'} ".
4235 "AND value+0 > UNIX_TIMESTAMP(?)");
4236 $sth->execute($ownerid, $date);
4238 while (my ($id, $dt) = $sth->fetchrow_array) {
4239 $item{$id} = $dt;
4242 my $limit = 100;
4243 my @ids = sort { $item{$a} cmp $item{$b} } keys %item;
4245 if (@ids > $limit) { @ids = @ids[0..$limit-1]; }
4247 my $in = join(',', @ids) || "0";
4248 $where = "AND jitemid IN ($in)";
4250 elsif ($req->{'selecttype'} eq "multiple") {
4251 my @ids;
4252 if($req->{'itemids'}) {
4253 foreach my $num (split(/\s*,\s*/, $req->{'itemids'})) {
4254 return fail($err, 203, 'xmlrpc.des.non_arifmetic', {'param'=>'itemid', 'value'=>$num}) unless $num =~ /^\d+$/;
4255 push @ids, $num;
4257 } elsif ($req->{'ditemids'}) {
4258 foreach my $num (split(/\s*,\s*/, $req->{'ditemids'})) {
4259 return fail($err, 203, 'xmlrpc.des.non_arifmetic', {'param'=>'itemid', 'value'=>$num}) unless $num =~ /^\d+$/;
4260 push @ids, int(($num+0)/256);
4263 my $limit = 100;
4264 return fail($err, 209, 'xmlrpc.des.entries_limit', {'limit'=>$limit}) if @ids > $limit;
4266 my $in = join(',', @ids);
4267 $where = "AND jitemid IN ($in)";
4269 elsif ($req->{'selecttype'} eq 'before') {
4270 my $before = $req->{'before'};
4271 my $itemshow = $req->{'howmany'};
4272 my $itemselect = $itemshow + $skip;
4274 $orderby = "ORDER BY jitemid";
4275 unless ($sort_order eq 'default') {
4276 $orderby .= ' '.uc($sort_order);
4279 my %item;
4280 $sth = $dbcr->prepare("SELECT jitemid, logtime FROM log2 WHERE ".
4281 "journalid=? AND logtime < ? $secwhere $orderby LIMIT $itemselect");
4282 $sth->execute($ownerid, $before);
4284 while (my ($id, $dt) = $sth->fetchrow_array) {
4285 $item{$id} = $dt;
4288 my $p_revtime = LJ::get_prop("log", "revtime");
4290 $sth = $dbcr->prepare("SELECT jitemid, FROM_UNIXTIME(value) ".
4291 "FROM logprop2 WHERE journalid=? ".
4292 "AND propid=$p_revtime->{'id'} ".
4293 "AND value+0 < ? LIMIT $itemselect");
4294 $sth->execute($ownerid, $before);
4296 while (my ($id, $dt) = $sth->fetchrow_array) {
4297 $item{$id} = $dt;
4300 my @ids = sort { $item{$a} cmp $item{$b} } keys %item;
4302 if (@ids > $skip) {
4303 @ids = @ids[$skip..(@ids-1)];
4304 @ids = @ids[0..$itemshow-1] if @ids > $itemshow;
4306 else {
4307 @ids = ();
4310 my $in = join(',', @ids) || "0";
4311 $where = "AND jitemid IN ($in)";
4313 elsif ($req->{'selecttype'} eq 'tag') {
4315 my $empty_res = {
4316 skip => $skip,
4317 xc3 => { u => $u },
4318 events => [],
4321 my $howmany = $req->{'howmany'} || 20;
4322 if ($howmany > 50) { $howmany = 50; }
4323 $howmany = $howmany + 0;
4325 $limit = "LIMIT $howmany";
4326 $offset = "OFFSET $skip";
4328 my $rtime_what = $is_community ? "rlogtime" : "revttime";
4329 $orderby = "ORDER BY $rtime_what";
4331 unless ($sort_order eq 'default') {
4332 $orderby .= ' '.uc($sort_order);
4335 my $jitemids;
4337 my ($tagids, $tagnames, $tags, $known_tags) = ([], [], {}, {});
4339 return fail($err,225)
4340 unless LJ::Tags::is_valid_tagstring($req->{'tags'}, $tagnames, { omit_underscore_check => 1 });
4342 $tags = LJ::Tags::get_usertags($uowner, { remote => $u });
4344 return $empty_res unless $tags && %$tags;
4346 while ( my ($tid, $tag) = each %$tags ) {
4347 $known_tags->{LJ::Text->normalize_tag_name($tag->{name})} = $tid;
4350 my $tagmode = lc $req->{'tagmode'};
4352 $tagids = [ map {
4353 my $tid = $known_tags->{LJ::Text->normalize_tag_name($_)};
4354 $tid ?
4355 $tid :
4356 ( $tagmode eq 'and' ? return $empty_res : () );
4357 } @$tagnames ];
4359 return $empty_res unless $tagids && @$tagids;
4361 if ($tagmode eq 'and') {
4363 my $limit = $LJ::TAG_INTERSECTION;
4364 $#{$tagids} = $limit - 1 if @{$tagids} > $limit;
4365 my $in = join(',', map { $_+0 } @{$tagids});
4366 my $sth = $dbcr->prepare("SELECT jitemid, kwid FROM logtagsrecent WHERE journalid = ? AND kwid IN ($in)");
4367 $sth->execute($ownerid);
4369 my %mix;
4370 while (my $row = $sth->fetchrow_arrayref) {
4371 my ($jitemid, $kwid) = @$row;
4372 $mix{$jitemid}++;
4375 my $need = @{$tagids};
4376 foreach my $jitemid (keys %mix) {
4377 delete $mix{$jitemid} if $mix{$jitemid} < $need;
4380 $jitemids = [keys %mix];
4381 } else { # mode: 'or'
4382 # select jitemids uniquely
4383 my $in = join(',', map { $_+0 } @{$tagids});
4384 $jitemids = $dbcr->selectcol_arrayref(qq{
4385 SELECT DISTINCT jitemid FROM logtagsrecent WHERE journalid = ? AND kwid IN ($in)
4386 }, undef, $ownerid);
4389 return $empty_res unless @$jitemids;
4391 $where = " AND jitemid IN (" .
4392 join(',', map { $_ + 0 } @$jitemids) .
4393 ")";
4395 else {
4396 return fail($err,200,'xmlrpc.des.bad_value',{'param'=>'selecttype'});
4399 if (my $posterid = int($req->{'posterid'})) {
4400 $where .= " AND posterid=$posterid";
4403 # common SQL template:
4404 unless ($sql) {
4405 $sql = "SELECT jitemid, eventtime, security, allowmask, anum, posterid, replycount, UNIX_TIMESTAMP(eventtime), logtime ".
4406 "FROM log2 WHERE journalid=$ownerid $secwhere $where $orderby $limit $offset";
4409 # whatever selecttype might have wanted us to use the master db.
4410 $dbcr = LJ::get_cluster_def_reader($uowner) if $use_master;
4412 return fail($err, 502) unless $dbcr;
4414 ## load the log rows
4415 ($sth = $dbcr->prepare($sql))->execute;
4416 return fail($err, 501, $dbcr->errstr) if $dbcr->err;
4418 my $count = 0;
4419 my @itemids = ();
4421 my $res = {
4422 skip => $skip,
4423 xc3 => {
4424 u => $u
4428 my $events = $res->{'events'} = [];
4429 my %evt_from_itemid;
4431 while (my ($itemid, $eventtime, $sec, $mask, $anum, $jposterid, $replycount, $event_timestamp, $logtime) = $sth->fetchrow_array) {
4432 $count++;
4435 # construct LJ::Entry object from row
4437 my $evt = {};
4438 my $entry = LJ::Entry->new_from_row(
4439 'journalid' => $ownerid,
4440 'jitemid' => $itemid,
4441 'allowmask' => $mask,
4442 'posterid' => $jposterid,
4443 'eventtime' => $eventtime,
4444 'security' => $sec,
4445 'anum' => $anum,
4449 # final_ownerid, final_anum and $final_itemid could be different
4450 # from ownerid if entry is a repost
4452 my $final_ownerid = $ownerid;
4453 my $final_itemid = $itemid;
4454 my $final_anum = $anum;
4457 # repost_text and repost_subject are using for repost only
4459 my $repost_text;
4460 my $repost_subject;
4463 # prepare list of variables to substiture values
4465 my $repost_entry;
4466 my $content = { 'original_post_obj' => \$entry,
4467 'repost_obj' => \$repost_entry,
4468 'journalid' => \$final_ownerid,
4469 'itemid' => \$final_itemid,
4470 'allowmask' => \$mask,
4471 'posterid' => \$jposterid,
4472 'eventtime' => \$eventtime,
4473 'security' => \$sec,
4474 'anum' => \$final_anum,
4475 'event' => \$repost_text,
4476 'subject' => \$repost_subject,
4477 'reply_count' => \$replycount, };
4480 # use repost signnture before event text
4482 my $repost_props = { use_repost_signature => 0 };
4484 if (LJ::Entry::Repost->substitute_content( $entry, $content, $repost_props )) {
4485 $evt->{'substitute_text'} = $repost_text;
4486 $evt->{'substitute_subject'} = $repost_subject;
4487 $evt->{'repost_ownerid'} = $final_ownerid;
4488 $evt->{'repost_itemid'} = $final_itemid;
4489 $evt->{'repost_anum'} = $final_anum;
4490 $evt->{'repost_ditemid'} = $final_itemid * 256 + $final_anum;
4491 $evt->{'repost_props'} = $entry->props;
4492 $evt->{'original_entry_url'} = $entry->url;
4493 $evt->{'repostername'} = $repost_entry->poster->username;
4494 $evt->{'journalname'} = $entry->journal->username if $entry->journal;
4495 if ($entry->poster) {
4496 $evt->{'postername'} = $entry->poster->username;
4497 my $userpic = $entry->userpic;
4498 $evt->{'poster_userpic_url'} = $userpic && $userpic->url;
4502 my $substitute_text;
4503 if (LJ::is_web_context()) {
4504 LJ::run_hooks('substitute_entry_content', $entry, \$substitute_text, {});
4507 if ($substitute_text) {
4508 $evt->{'substitute_text'} = $substitute_text;
4509 $evt->{'substitute_subject'} = '';
4512 # now my own post, so need to check for suspended prop
4513 if ($jposterid != $posterid) {
4514 next if($entry->is_suspended_for($u));
4517 $evt->{'itemid'} = $itemid;
4518 push @itemids, $itemid;
4520 $evt_from_itemid{$itemid} = $evt;
4522 $evt->{"eventtime"} = $eventtime;
4523 $evt->{"event_timestamp"} = $event_timestamp;
4524 $evt->{"logtime"} = $logtime;
4526 if ($sec ne "public") {
4527 $evt->{'security'} = $sec;
4528 $evt->{'allowmask'} = $mask if $sec eq "usemask";
4531 $evt->{'anum'} = $anum;
4532 $evt->{'ditemid'} = $itemid * 256 + $anum;
4534 if ($jposterid != $final_ownerid) {
4535 my $uposter = LJ::load_userid($jposterid);
4536 $evt->{'poster'} = $uposter->username;
4537 $evt->{'poster_id'} = $uposter->id;
4539 if ( my $userpic = $uposter->userpic() ) {
4540 $evt->{'poster_userpic_url'} = $userpic->url();
4543 if ($uposter->identity) {
4544 my $i = $uposter->identity;
4545 $evt->{'identity_type'} = $i->pretty_type;
4546 $evt->{'identity_value'} = $i->value;
4547 $evt->{'identity_url'} = $i->url($uposter);
4548 $evt->{'identity_display'} = $uposter->display_name;
4553 # There is using final_ variabled to get correct link
4555 $evt->{'url'} = LJ::item_link(LJ::load_userid($final_ownerid),
4556 $final_itemid,
4557 $final_anum);
4559 $evt->{'reply_count'} = $replycount;
4561 $evt->{'can_comment'} = $u ? $entry->remote_can_comment($u) : $entry->everyone_can_comment;
4563 if ( $itemid == $sticky_id && $req->{'selecttype'} eq "lastn") {
4564 unshift @$events, $evt,
4566 else {
4567 push @$events, $evt;
4571 # load properties. Even if the caller doesn't want them, we need
4572 # them in Unicode installations to recognize older 8bit non-UF-8
4573 # entries.
4574 unless ($req->{'noprops'} && !$LJ::UNICODE) {
4575 ### do the properties now
4576 $count = 0;
4577 my %props = ();
4578 LJ::load_log_props2($dbcr, $ownerid, \@itemids, \%props);
4580 # load the tags for these entries, unless told not to
4581 unless ($req->{notags}) {
4582 # construct %idsbycluster for the multi call to get these tags
4583 my $tags = LJ::Tags::get_logtags($uowner, \@itemids);
4585 # add to props
4586 foreach my $itemid (@itemids) {
4587 next unless $tags->{$itemid};
4588 $props{$itemid}->{taglist} = join(', ', values %{$tags->{$itemid}});
4592 foreach my $itemid (keys %props) {
4593 # 'replycount' is a pseudo-prop, don't send it.
4594 # FIXME: this goes away after we restructure APIs and
4595 # replycounts cease being transferred in props
4596 delete $props{$itemid}->{'replycount'};
4598 unless ($flags->{noauth}) {
4599 delete $props{$itemid}->{repost_offer};
4602 my $evt = $evt_from_itemid{$itemid};
4603 $evt->{'props'} = {};
4605 foreach my $name (keys %{$props{$itemid}}) {
4607 my $value = $props{$itemid}->{$name};
4608 $value =~ s/\n/ /g;
4610 # normalize props
4611 unless ($flags->{'noauth'}) {
4612 my $prop = LJ::get_prop("log", $name);
4613 my $ptype = $prop->{'datatype'};
4615 if ($ptype eq "bool" && $value !~ /^[01]$/) {
4616 $value = $value ? 1 : 0;
4618 if ($ptype eq "num" && $value =~ /[^\d]/) {
4619 $value = int $value;
4623 $evt->{'props'}->{$name} = $value;
4626 if ( $itemid == $sticky_id ) {
4627 $evt->{'props'}->{'sticky'} = 1;
4632 ## load the text
4633 my $text = LJ::cond_no_cache($use_master, sub {
4634 return LJ::get_logtext2($uowner, @itemids);
4637 foreach my $i (@itemids) {
4638 my $t = $text->{$i};
4639 my $evt = $evt_from_itemid{$i};
4641 my $real_uowner = $uowner;
4643 if ($evt->{'substitute_text'}) {
4644 $t->[0] = delete $evt->{'substitute_subject'};
4645 $t->[1] = delete $evt->{'substitute_text'};
4647 $evt->{'props'} = delete $evt->{'repost_props'}
4648 unless $req->{'noprops'};
4650 delete $evt->{'props'}{'repost_offer'} if $evt->{'props'};
4652 $evt->{'itemid'} = delete $evt->{'repost_itemid'};
4653 $evt->{'anum'} = delete $evt->{'repost_anum'};
4654 $evt->{'ownerid'} = delete $evt->{'repost_ownerid'};
4655 $evt->{'repost'} = 1;
4657 $real_uowner = LJ::want_user($evt->{'ownerid'});
4661 # if they want subjects to be events, replace event
4662 # with subject when requested.
4663 if ($req->{'prefersubject'} && length($t->[0])) {
4664 $t->[1] = $t->[0]; # event = subject
4665 $t->[0] = undef; # subject = undef
4668 # now that we have the subject, the event and the props,
4669 # auto-translate them to UTF-8 if they're not in UTF-8.
4670 if ($LJ::UNICODE && $req->{'ver'} >= 1 &&
4671 $evt->{'props'}->{'unknown8bit'}) {
4672 my $error = 0;
4673 $t->[0] = LJ::text_convert($t->[0], $real_uowner, \$error);
4674 $t->[1] = LJ::text_convert($t->[1], $real_uowner, \$error);
4676 foreach (keys %{$evt->{'props'}}) {
4677 $evt->{'props'}->{$_} = LJ::text_convert($evt->{'props'}->{$_}, $real_uowner, \$error);
4680 return fail($err, 208, 'xmlrpc.des.cannnot_display_post',{'siteroot'=>$LJ::SITEROOT})
4681 if $error;
4684 if ($LJ::UNICODE && $req->{'ver'} < 1 && !$evt->{'props'}->{'unknown8bit'}) {
4685 unless ( LJ::is_ascii($t->[0]) &&
4686 LJ::is_ascii($t->[1]) &&
4687 LJ::is_ascii(join(' ', values %{$evt->{'props'}}) )) {
4688 # we want to fail the client that wants to get this entry
4689 # but we make an exception for selecttype=day, in order to allow at least
4690 # viewing the daily summary
4692 if ($req->{'selecttype'} eq 'day') {
4693 $t->[0] = $t->[1] = $CannotBeShown;
4695 else {
4696 return fail($err, 207, 'xmlrpc.des.not_unicode_client', {'siteroot'=>$LJ::SITEROOT});
4701 if ($t->[0]) {
4702 $t->[0] =~ s/[\r\n]/ /g;
4703 $evt->{'subject'} = $t->[0];
4706 $t->[1] = LJ::trim_widgets(
4707 'length' => $req->{trim_widgets},
4708 'img_length' => $req->{widgets_img_length},
4709 'text' => $t->[1],
4710 'read_more' => '<a href="' . $evt->{url} . '"> ...</a>',
4711 ) if $req->{trim_widgets};
4713 LJ::EmbedModule->expand_entry($real_uowner, \$t->[1], get_video_id => 1) if($req->{get_video_ids});
4715 if ( $req->{get_polls} ) {
4716 my @polls_id = LJ::Poll->expand_entry(\$t->[1], getpolls => 1, viewer => $u);
4718 foreach (@polls_id) {
4719 my $poll = LJ::Poll->new($_);
4721 next unless $poll->can_view($u);
4722 push @{$evt->{'polls'}->{$_}}, { $poll->aggr_results() };
4726 if ($req->{get_users_info}){
4727 LJ::EmbedModule->expand_lj_user(\$t->[1]);
4730 if ($req->{view}) {
4731 LJ::EmbedModule->expand_entry($real_uowner, \$t->[1], edit => 1) if $req->{view} eq 'stored';
4733 elsif ($req->{parseljtags}) {
4734 $t->[1] = LJ::convert_lj_tags_to_links(
4735 event => $t->[1],
4736 embed_url => $evt->{url});
4740 # truncate
4741 if ($req->{'truncate'} >= 4) {
4742 my $original = $t->[1];
4744 if ($req->{'ver'} > 1) {
4745 $t->[1] = LJ::text_trim($t->[1], $req->{'truncate'} - 3, 0);
4747 else {
4748 $t->[1] = LJ::text_trim($t->[1], 0, $req->{'truncate'} - 3);
4751 # only append the elipsis if the text was actually truncated
4752 $t->[1] .= "..." if $t->[1] ne $original;
4755 # line endings
4756 $t->[1] =~ s/\r//g;
4758 if ($req->{'asxml'}) {
4759 my $tidy = LJ::Tidy->new();
4760 $evt->{'subject'} = $tidy->clean( $evt->{'subject'} );
4761 $t->[1] = $tidy->clean( $t->[1] );
4764 if ($req->{'lineendings'} eq "unix") {
4765 # do nothing. native format.
4767 elsif ($req->{'lineendings'} eq "mac") {
4768 $t->[1] =~ s/\n/\r/g;
4770 elsif ($req->{'lineendings'} eq "space") {
4771 $t->[1] =~ s/\n/ /g;
4773 elsif ($req->{'lineendings'} eq "dots") {
4774 $t->[1] =~ s/\n/ ... /g;
4776 else { # "pc" -- default
4777 $t->[1] =~ s/\n/\r\n/g;
4780 $evt->{'event'} = $t->[1];
4783 # maybe we don't need the props after all
4784 if ($req->{'noprops'}) {
4785 foreach(@$events) { delete $_->{'props'}; }
4788 $res->{lastsync} = LJ::TimeUtil->mysql_time();
4790 return $res;
4793 sub createrepost {
4794 my ($req, $err, $flags) = @_;
4795 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'createrepost');
4797 my $u = $flags->{'u'};
4799 my $timezone = $req->{'tz'} || 'guess';
4800 unless ($timezone eq 'guess' ||
4801 $timezone =~ /^[+\-]\d\d\d\d$/) {
4802 return fail($err, 203, 'xmlrpc.des.bad_value', {'param'=>'tz'});
4805 my $url = $req->{'url'} || return fail($err,200,"url");
4806 my $entry = LJ::Entry->new_from_url($url);
4808 return fail($err, 203, 'url') unless $entry && $entry->valid;
4809 return fail($err, 227) unless $entry->visible_to($u);
4811 my $result = LJ::Entry::Repost->create(
4812 'journalu' => $u,
4813 'source_entry' => $entry,
4814 'timezone' => $timezone,
4817 if ( my $error = $result->{error} ) {
4818 return fail($err, 228, $error->{error_message});
4821 $result->{result}{status} = 'OK';
4823 return $result->{result};
4826 sub deleterepost {
4827 my ($req, $err, $flags) = @_;
4828 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'deleterepost');
4830 my $u = $flags->{'u'};
4832 my $url = $req->{'url'} || return fail($err,200,"url");
4833 my $entry = LJ::Entry->new_from_url($url);
4835 return fail($err, 203, 'url') unless $entry && $entry->valid;
4837 my $result = LJ::Entry::Repost->delete( $u, # destination journal
4838 $entry,); # entry to be reposted
4840 if ( my $error = $result->{error} ) {
4841 return fail($err, 229, $error->{error_message});
4844 $result->{status} = 'OK';
4846 return $result;
4849 sub getrepoststatus {
4850 my ($req, $err, $flags) = @_;
4852 $flags->{allow_anonymous} = 1;
4853 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getevents');
4855 my $u = $flags->{'u'};
4857 my $url = $req->{'url'} || return fail($err,200,"url");
4858 my $entry = LJ::Entry->new_from_url($url);
4860 return fail($err, 203, 'url') unless $entry && $entry->valid;
4861 return fail($err, 227) unless $entry->visible_to($u);
4863 my $result = LJ::Entry::Repost->get_status($entry, $u);
4865 $result->{status} = 'OK';
4867 return $result;
4871 # DONT PUT ANY RELATION LOGIC IN THIS METHOD
4872 # ALL IN LJ::User::Relations::Friends::remove_friend
4874 sub editfriends {
4875 my ($req, $err, $flags) = @_;
4876 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'editfriends');
4878 my $u = $flags->{'u'};
4879 my $userid = $u->{'userid'};
4881 # do not let locked people do this
4882 return fail($err, 308) if $u->{statusvis} eq 'L';
4884 my $dbh = LJ::get_db_writer();
4886 return fail($err,306) unless $dbh;
4888 my $res = {
4889 xc3 => {
4890 u => $u
4892 added => [
4896 ## first, figure out who the current friends are to save us work later
4897 my $error_flag = 0;
4898 my $friend_count = $u->friends_count();
4899 my $friends_changed = 0;
4901 # perform the deletions
4902 DELETEFRIEND:
4903 foreach my $username (@{$req->{'delete'}}) {
4904 next unless $username;
4906 if (my $friend = LJ::load_user($username)) {
4907 if ($u->remove_friend($friend)) {
4908 $friend_count--;
4909 $friends_changed = 1;
4914 my $fail = sub {
4915 return fail($err, $_[0], $_[1]);
4918 # only people, shared journals, and owned syn feeds can add friends
4919 return $fail->(104, 'xmlrpc.des.friends_add_not_allowed')
4920 unless ($u->{'journaltype'} eq 'P' ||
4921 $u->{'journaltype'} eq 'S' ||
4922 $u->{'journaltype'} eq 'I' ||
4923 ($u->{'journaltype'} eq "Y" && $u->has_password));
4925 # Don't let suspended users add friend
4926 return $fail->(305, 'xmlrpc.des.suspended_add_friend')
4927 if ($u->is_suspended);
4929 # perform the adds
4930 ADDFRIEND:
4931 foreach my $fa (@{$req->{'add'}}) {
4932 unless (ref $fa eq "HASH") {
4933 $fa = {
4934 'username' => $fa
4938 my $fg = $fa->{'fgcolor'} || "#000000";
4939 my $bg = $fa->{'bgcolor'} || "#FFFFFF";
4940 my $gmask = $fa->{'groupmask'};
4942 if ($fg !~ /^\#[0-9A-F]{6,6}$/i || $bg !~ /^\#[0-9A-F]{6,6}$/i) {
4943 return $fail->(203, 'xmlrpc.des.bad_value', {'param'=>'color'});
4946 my $row = LJ::load_user(
4947 $fa->{username}
4950 unless ($row) {
4951 $error_flag = 1;
4952 next ADDFRIEND;
4955 if ($u->is_friend($row)) {
4956 $u->update_friend(
4957 $row,
4958 fgcolor => LJ::color_todb($fg),
4959 bgcolor => LJ::color_todb($bg),
4960 ($gmask ? (groupmask => $gmask) : ())
4962 next ADDFRIEND;
4965 if ($row->{'statusvis'} ne "V") {
4966 $error_flag = 1;
4967 next ADDFRIEND;
4970 if ($row->{'journaltype'} eq "R") {
4971 return $fail->(154);
4974 my $err;
4976 unless (LJ::is_enabled('new_friends_and_subscriptions')) {
4977 return $fail->(104, "$err")
4978 unless $u->can_add_friends(\$err, { 'numfriends' => $friend_count, friend => $fa });
4981 # force bit 0 on.
4982 if ($gmask) {
4983 $gmask |= 1;
4986 my $opts = {
4987 fgcolor => $fg,
4988 bgcolor => $bg,
4989 groupmask => $gmask,
4990 defaultview => 1
4993 if (LJ::is_enabled('new_friends_and_subscriptions')) {
4994 if ($flags->{noauth}) {
4995 unless ($u->to_offer_friendship($row)) {
4996 next ADDFRIEND;
4998 } else {
4999 unless ($u->subscribe_to_user($row)) {
5000 next ADDFRIEND;
5003 } else {
5004 unless ($u->add_friend($row, $opts)) {
5005 next ADDFRIEND;
5009 $friend_count++;
5010 $friends_changed = 1;
5012 my $added = {
5013 'fgcolor' => $fg,
5014 'bgcolor' => $bg,
5015 'username' => LJ::canonical_username(
5016 $fa->{username}
5018 'fullname' => $row->{'name'},
5019 'groupmask' => $gmask,
5020 'journaltype' => $row->{journaltype},
5021 'defaultpicurl' => (
5022 $row->{'defaultpicid'} && "$LJ::USERPIC_ROOT/$row->{'defaultpicid'}/$row->{'userid'}"
5026 if ($req->{'ver'} >= 1) {
5027 LJ::text_out(
5028 \$added->{'fullname'}
5032 if ($row->identity) {
5033 if (my $i = $row->identity) {
5034 $added->{'identity_url'} = $i->url($row);
5035 $added->{'identity_type'} = $i->pretty_type;
5036 $added->{'identity_value'} = $i->value;
5037 $added->{'identity_display'} = $row->display_name;
5041 if ($row->{'journaltype'} ne 'P') {
5042 $added->{type} = {
5043 'C' => 'community',
5044 'Y' => 'syndicated',
5045 'N' => 'news',
5046 'S' => 'shared',
5047 'I' => 'identity',
5048 }->{
5049 $row->{'journaltype'}
5053 push @{
5054 $res->{'added'}
5055 }, $added;
5058 return $fail->(104) if $error_flag;
5060 if ($friends_changed) {
5061 LJ::run_hooks(
5062 'friends_changed',
5063 LJ::load_userid($userid)
5067 return $res;
5070 sub editfriendgroups {
5071 my ($req, $err, $flags) = @_;
5072 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'editfriendgroups');
5074 my $u = $flags->{'u'};
5075 my $userid = $u->{'userid'};
5076 my ($db, $fgtable, $bmax, $cmax) = $u->{dversion} > 5 ?
5077 ($u->writer, 'friendgroup2', LJ::BMAX_GRPNAME2, LJ::CMAX_GRPNAME2) :
5078 (LJ::get_db_writer(), 'friendgroup', LJ::BMAX_GRPNAME, LJ::CMAX_GRPNAME);
5079 my $sth;
5081 return fail($err,306) unless $db;
5083 # do not let locked people do this
5084 return fail($err, 308) if $u->{statusvis} eq 'L';
5086 my $res = {};
5088 ## make sure tree is how we want it
5089 $req->{'groupmasks'} = {} unless
5090 (ref $req->{'groupmasks'} eq "HASH");
5091 $req->{'set'} = {} unless
5092 (ref $req->{'set'} eq "HASH");
5093 $req->{'delete'} = [] unless
5094 (ref $req->{'delete'} eq "ARRAY");
5096 ## before we perform any DB operations, validate input text
5097 # (groups' names) for correctness so we can fail gracefully
5098 if ($LJ::UNICODE) {
5099 foreach my $bit (keys %{$req->{'set'}}) {
5100 my $name = $req->{'set'}->{$bit}->{'name'};
5101 return fail($err,207,'xmlrpc.des.not_ascii')
5102 if $req->{'ver'} < 1 and not LJ::is_ascii($name);
5103 return fail($err,208,'xmlrpc.des.invalid_group',{'siteroot'=>$LJ::SITEROOT})
5104 unless LJ::text_in($name);
5108 ## do additions/modifications ('set' hash)
5109 my %added;
5110 my @foradd = ();
5112 foreach my $bit (keys %{$req->{'set'}}) {
5113 $bit += 0;
5115 next unless ($bit >= 1 && $bit <= 30);
5117 my $sa = $req->{'set'}->{$bit};
5119 next unless $sa;
5121 my $name = LJ::text_trim($sa->{'name'}, $bmax, $cmax);
5122 my $sort = defined $sa->{'sort'} ? ($sa->{'sort'}+0) : 50;
5123 my $public = defined $sa->{'public'} ? ($sa->{'public'}+0) : 0;
5125 # can't end with a slash
5126 $name =~ s!/$!!;
5128 # setting it to name is like deleting it.
5129 unless ($name =~ /\S/) {
5130 push @{$req->{'delete'}}, $bit;
5131 next;
5134 $added{$bit} = 1;
5136 push @foradd, {
5137 id => $bit,
5138 sort => $sort,
5139 name => $name,
5140 public => $public,
5144 LJ::User::Groups->update_groups(
5145 $u, \@foradd
5148 my @fordelete = grep {
5149 ! $added{$_}
5150 } @{
5151 $req->{'delete'}
5154 LJ::User::Groups->remove_groups(
5155 $u, \@fordelete
5158 my @forupdate = ();
5160 foreach my $friend (keys %{$req->{'groupmasks'}}) {
5161 my $mask = int($req->{'groupmasks'}->{$friend}) | 1;
5162 my $friendid = LJ::get_userid($friend);
5164 next unless $mask;
5165 next unless $friendid;
5167 my $friend = LJ::load_userid($friendid);
5169 push @forupdate, [
5170 $friend, $mask
5174 LJ::User::Groups->update_groupmasks(
5175 $u, \@forupdate
5178 # return value for this is nothing.
5179 return {
5180 xc3 => {
5181 u => $u
5186 sub sessionexpire {
5187 my ($req, $err, $flags) = @_;
5188 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'sessionexpire');
5189 my $u = $flags->{u};
5190 my $res = {
5191 xc3 => {
5192 u => $u
5196 # expunge one? or all?
5197 if ($req->{expireall}) {
5198 $u->kill_all_sessions;
5199 return $res;
5202 # just expire a list
5203 my $list = $req->{expire} || [];
5205 return $res unless @$list;
5207 return fail($err,502) unless $u->writer;
5208 $u->kill_sessions(@$list);
5210 return $res;
5213 sub sessiongenerate {
5214 # generate a session
5215 my ($req, $err, $flags) = @_;
5216 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'sessiongenerate');
5218 # sanitize input
5219 $req->{expiration} = 'short' unless $req->{expiration} eq 'long';
5220 my $boundip;
5221 $boundip = LJ::get_remote_ip() if $req->{bindtoip};
5223 my $u = $flags->{u};
5224 my $sess_opts = {
5225 exptype => $req->{expiration},
5226 ipfixed => $boundip,
5229 # do not let locked people do this
5230 return fail($err, 308) if $u->{statusvis} eq 'L';
5232 my $sess = LJ::Session->create($u, %$sess_opts);
5234 # return our hash
5235 return {
5236 ljsession => $sess->master_cookie_string,
5237 xc3 => {
5238 u => $u
5243 sub list_friends {
5244 my ($u, $opts) = @_;
5245 my $us;
5247 unless ($opts->{'friendof'}) {
5248 $us = $u->friends(
5249 with_attr => 1
5251 } else {
5252 $us = $u->friendsof(
5253 filters => [{
5254 type => 'exclude',
5255 edge => 'B'
5260 my $res = [];
5261 my $limitnum = $opts->{'limit'} + 0;
5263 foreach my $f (values %$us) {
5264 next if $opts->{'friendof'} && $u->{'statusvis'} ne 'V';
5266 my $r = {
5267 'username' => $f->{'user'},
5268 'fullname' => $f->{'name'},
5271 if (my $i = $f->identity) {
5272 $r->{'identity_url'} = $i->url($f);
5273 $r->{'identity_type'} = $i->pretty_type;
5274 $r->{'identity_value'} = $i->value;
5275 $r->{'identity_display'} = $f->display_name;
5278 if ($opts->{'includebdays'} &&
5279 $f->{'bdate'} &&
5280 $f->{'bdate'} ne "0000-00-00" &&
5281 $f->can_show_full_bday)
5283 $r->{'birthday'} = $f->{'bdate'};
5286 if ($f->{'statusvis'} ne 'V') {
5287 $r->{"status"} = {
5288 'D' => "deleted",
5289 'S' => "suspended",
5290 'X' => "purged",
5291 }->{$f->{'statusvis'}};
5294 if ($f->{'journaltype'} ne 'P') {
5295 $r->{"type"} = {
5296 'C' => 'community',
5297 'Y' => 'syndicated',
5298 'N' => 'news',
5299 'S' => 'shared',
5300 'I' => 'identity',
5301 }->{$f->{'journaltype'}};
5304 if ($opts->{'friendof'}) {
5305 $r->{'fgcolor'} = "#000000";
5306 $r->{'bgcolor'} = "#ffffff";
5307 } else {
5308 my $uid = $f->id;
5309 my $attr = $u->get_friend_attributes($f);
5311 if ($attr) {
5312 if ($attr->{fgcolor}) {
5313 $r->{'fgcolor'} = $attr->{fgcolor};
5316 if ($attr->{bgcolor}) {
5317 $r->{'bgcolor'} = $attr->{bgcolor};
5320 if ($attr->{groupmask} != 1) {
5321 $r->{'groupmask'} = $attr->{groupmask};
5326 if ($f->{'defaultpicid'}) {
5327 $r->{defaultpicurl} = "$LJ::USERPIC_ROOT/$u->{'defaultpicid'}/$f->{'userid'}";
5330 push @$res, $r;
5332 # won't happen for zero limit (which means no limit)
5333 last if @$res == $limitnum;
5336 return $res;
5339 sub syncitems {
5340 my ($req, $err, $flags) = @_;
5341 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'syncitems');
5342 return undef unless check_altusage($req, $err, $flags);
5343 return fail($err, 506) if $LJ::DISABLED{'syncitems'};
5345 my $ownerid = $flags->{'ownerid'};
5346 my $uowner = $flags->{'u_owner'} || $flags->{'u'};
5347 my $sth;
5349 my $db = LJ::get_cluster_reader($uowner);
5350 return fail($err, 502) unless $db;
5352 ## have a valid date?
5353 my $date = $req->{'lastsync'};
5355 if ($date) {
5356 return fail($err, 203, 'xmlrpc.des.bad_value', {'param'=>'date'})
5357 unless ($date =~ /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/);
5358 } else {
5359 $date = "0000-00-00 00:00:00";
5362 my $LIMIT = 500;
5363 my $type = $req->{type} || 'posted';
5364 my ( $table, $idfield ) = ( '', 'jitemid');
5366 my $external_ids = $req->{'use_external_ids'};
5368 if ( $req->{ver} > 3 && LJ::is_enabled("delayed_entries") ) {
5369 if ( $type eq 'posted' ) {
5370 $table = '';
5371 $idfield = 'jitemid';
5373 elsif ( $type eq 'delayed' ) {
5374 $table = 'delayed';
5375 $idfield = 'delayedid';
5376 } else {
5377 return fail( $err, 216 );
5381 my %item;
5382 $sth = $db->prepare("SELECT ${idfield}, logtime FROM ${table}log2 WHERE ".
5383 "journalid=? and logtime > ?");
5384 $sth->execute($ownerid, $date);
5385 while (my ($id, $dt, $anum) = $sth->fetchrow_array) {
5386 $item{$id} = [ 'L', $id, $dt, "create", $anum ];
5389 my %cmt;
5391 unless ( $type eq 'delayed' ) {
5392 my $p_calter = LJ::get_prop("log", "commentalter");
5393 my $p_revtime = LJ::get_prop("log", "revtime");
5394 $sth = $db->prepare("SELECT jitemid, propid, FROM_UNIXTIME(value) ".
5395 "FROM logprop2 WHERE journalid=? ".
5396 "AND propid IN ($p_calter->{'id'}, $p_revtime->{'id'}) ".
5397 "AND value+0 > UNIX_TIMESTAMP(?)");
5399 $sth->execute($ownerid, $date);
5400 while (my ($id, $prop, $dt) = $sth->fetchrow_array) {
5401 my $entry = LJ::Entry->new($ownerid, jitemid => $id);
5403 ## sometimes there is no row in log2 table, while there are rows in logprop2
5404 ## it's either corrupted db (replication/failover problem) or lazy/slow deletion of an entry
5405 ## calling $entry->anum on such an entry is a fatal error
5406 next unless $entry && $entry->valid;
5408 if ($prop == $p_calter->{'id'}) {
5409 $cmt{$id} = [ 'C', $id, $dt, "update", $entry->anum ];
5410 } elsif ($prop == $p_revtime->{'id'}) {
5411 $item{$id} = [ 'L', $id, $dt, "update", $entry->anum ];
5415 my @ev = sort { $a->[2] cmp $b->[2] } (values %item, values %cmt);
5417 my $res = {
5418 xc3 => {
5419 u => $flags->{'u'}
5423 my $list = $res->{'syncitems'} = [];
5424 $res->{'total'} = scalar @ev;
5425 my $ct = 0;
5427 while (my $ev = shift @ev) {
5428 $ct++;
5429 push @$list, {
5430 'item' => "$ev->[0]-$ev->[1]",
5431 'time' => $ev->[2],
5432 'action' => $ev->[3],
5433 ( $external_ids ? (ditemid => $ev->[1]*256 + $ev->[4]) : () )
5435 last if $ct >= $LIMIT;
5438 $res->{'count'} = $ct;
5440 return $res;
5443 sub consolecommand {
5444 my ($req, $err, $flags) = @_;
5446 # logging in isn't necessary, but most console commands do require it
5447 LJ::set_remote($flags->{'u'}) if authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'consolecommand');
5449 my $res = {
5450 xc3 => {
5451 u => $flags->{'u'}
5454 my $cmdout = $res->{'results'} = [];
5456 require LJ::Console;
5457 foreach my $cmd (@{$req->{'commands'}}) {
5458 # callee can pre-parse the args, or we can do it bash-style
5459 my @args = ref $cmd eq "ARRAY" ? @$cmd
5460 : LJ::Console->parse_line($cmd);
5461 my $c = LJ::Console->parse_array(@args);
5462 my $rv = $c->execute_safely;
5464 my @output;
5465 push @output, [$_->status, $_->text] foreach $c->responses;
5467 push @{$cmdout}, {
5468 'success' => $rv,
5469 'output' => \@output,
5473 return $res;
5476 sub getchallenge
5478 my ($req, $err, $flags) = @_;
5479 my $res = {};
5480 my $now = time();
5481 my $challenge_lifetime = 60;
5482 return {
5483 challenge => LJ::Auth::Challenge->generate($challenge_lifetime),
5484 server_time => $now,
5485 expire_time => $now + $challenge_lifetime,
5486 auth_scheme => "c0", # fixed for now, might support others later
5487 xc3 => {},
5491 sub login_message
5493 my ($req, $res, $flags) = @_;
5494 my $u = $flags->{'u'};
5496 my $msg = sub {
5497 my $code = shift;
5498 my $args = shift || {};
5499 $args->{'sitename'} = $LJ::SITENAME;
5500 $args->{'siteroot'} = $LJ::SITEROOT;
5501 my $pre = delete $args->{'pre'};
5502 $res->{'message'} = $pre . translate($u, $code, $args);
5505 return $msg->("readonly") if LJ::get_cap($u, "readonly");
5506 return $msg->("not_validated") if ($u->{'status'} eq "N" and not $LJ::EVERYONE_VALID);
5507 return $msg->("must_revalidate") if ($u->{'status'} eq "T" and not $LJ::EVERYONE_VALID);
5509 return $msg->("old_win32_client") if $req->{'clientversion'} =~ /^Win32-MFC\/(1.2.[0123456])$/;
5510 return $msg->("old_win32_client") if $req->{'clientversion'} =~ /^Win32-MFC\/(1.3.[01234])\b/;
5511 return $msg->("hello_test") if grep { $u->{user} eq $_ } @LJ::TESTACCTS;
5514 sub list_friendgroups
5516 my $u = shift;
5518 # get the groups for this user, return undef if error
5519 my $groups = LJ::get_friend_group($u);
5520 return undef unless $groups;
5522 # we got all of the groups, so put them into an arrayref sorted by the
5523 # group sortorder; also note that the map is used to construct a new hashref
5524 # out of the old group hashref so that we have all of the field names converted
5525 # to a format our callers can recognize
5526 my @res = map {{
5527 id => $_->{groupnum},
5528 name => $_->{groupname},
5529 public => $_->{is_public},
5530 sortorder => $_->{sortorder},
5531 }} sort {
5532 $a->{sortorder} <=> $b->{sortorder}
5533 } values %$groups;
5535 return \@res;
5538 sub list_usejournals {
5539 my $u = shift;
5541 my @us = $u->posting_access_list;
5542 my @unames = map { $_->{user} } @us;
5544 return \@unames;
5547 sub hash_menus
5549 my $u = shift;
5550 my $user = $u->{'user'};
5552 my $menu = [
5553 { 'text' => "Recent Entries",
5554 'url' => "$LJ::SITEROOT/users/$user/", },
5555 { 'text' => "Calendar View",
5556 'url' => "$LJ::SITEROOT/users/$user/calendar", },
5557 { 'text' => "Friends View",
5558 'url' => "$LJ::SITEROOT/users/$user/friends", },
5559 { 'text' => "-", },
5560 { 'text' => "Your Profile",
5561 'url' => "$LJ::SITEROOT/userinfo.bml?user=$user", },
5562 { 'text' => "Your To-Do List",
5563 'url' => "$LJ::SITEROOT/todo/?user=$user", },
5564 { 'text' => "-", },
5565 { 'text' => "Change Settings",
5566 'sub' => [ { 'text' => "Personal Info",
5567 'url' => "$LJ::SITEROOT/manage/profile/", },
5568 { 'text' => "Customize Journal",
5569 'url' =>"$LJ::SITEROOT/customize/", }, ] },
5570 { 'text' => "-", },
5571 { 'text' => "Support",
5572 'url' => "$LJ::SITEROOT/support/", }
5575 LJ::run_hooks("modify_login_menu", {
5576 'menu' => $menu,
5577 'u' => $u,
5578 'user' => $user,
5581 return $menu;
5584 sub list_pickws
5586 my $u = shift;
5588 my $pi = LJ::get_userpic_info($u);
5589 my @res;
5591 my %seen; # mashifiedptr -> 1
5593 # FIXME: should be a utf-8 sort
5594 foreach my $kw (sort keys %{$pi->{'kw'}}) {
5595 my $pic = $pi->{'kw'}{$kw};
5596 $seen{$pic} = 1;
5597 next if $pic->{'state'} eq "I";
5598 push @res, [ $kw, $pic->{'picid'} ];
5601 # now add all the pictures that don't have a keyword
5602 foreach my $picid (keys %{$pi->{'pic'}}) {
5603 my $pic = $pi->{'pic'}{$picid};
5604 next if $seen{$pic};
5605 push @res, [ "pic#$picid", $picid ];
5608 return \@res;
5611 sub list_moods
5613 my $mood_max = int(shift);
5614 LJ::load_moods();
5616 my $res = [];
5617 return $res if $mood_max >= $LJ::CACHED_MOOD_MAX;
5619 for (my $id = $mood_max+1; $id <= $LJ::CACHED_MOOD_MAX; $id++) {
5620 next unless defined $LJ::CACHE_MOODS{$id};
5621 my $mood = $LJ::CACHE_MOODS{$id};
5622 next unless $mood->{'name'};
5623 push @$res, { 'id' => $id,
5624 'name' => $mood->{'name'},
5625 'parent' => $mood->{'parent'} };
5628 return $res;
5631 sub check_altusage
5633 my ($req, $err, $flags) = @_;
5635 # see note in LJ::can_use_journal about why we return
5636 # both 'ownerid' and 'u_owner' in $flags
5638 my $alt = $req->{'usejournal'} || $req->{'journal'};
5639 my $u = $flags->{'u'};
5641 unless ($u) {
5642 my $username = $req->{'username'};
5643 if ($username) {
5644 my $dbr = LJ::get_db_reader();
5645 return fail($err,502) unless $dbr;
5646 $u = $flags->{'u'} = LJ::load_user($username);
5647 } else {
5648 if ($flags->{allow_anonymous}) {
5649 return fail($err,200) unless $alt;
5650 my $uowner = LJ::load_user($alt);
5651 return fail($err,206) unless $uowner;
5652 $flags->{'u_owner'} = $uowner;
5653 $flags->{'ownerid'} = $uowner->{'userid'};
5654 return 1;
5656 return fail($err,200);
5660 $flags->{'ownerid'} = $u->{'userid'};
5662 # all good if not using an alt journal
5663 return 1 unless $alt;
5665 # complain if the username is invalid
5666 my $uowner = LJ::load_user($alt);
5667 return fail($err,206) unless $uowner;
5669 # allow usage if we're told explicitly that it's okay
5670 if ($flags->{'usejournal_okay'}) {
5671 $flags->{'u_owner'} = $uowner;
5672 $flags->{'ownerid'} = $uowner->{'userid'};
5673 LJ::Request->notes("journalid" => $flags->{'ownerid'}) if LJ::Request->is_inited && !LJ::Request->notes("journalid");
5674 return 1;
5677 # otherwise, check for access:
5678 my $info = {};
5679 my $canuse = LJ::can_use_journal($u->{'userid'}, $alt, $info);
5680 $flags->{'ownerid'} = $info->{'ownerid'};
5681 $flags->{'u_owner'} = $info->{'u_owner'};
5682 LJ::Request->notes("journalid" => $flags->{'ownerid'}) if LJ::Request->is_inited && !LJ::Request->notes("journalid");
5684 return 1 if $canuse || $flags->{'ignorecanuse'};
5686 # not allowed to access it
5687 return fail($err,300);
5690 sub authenticate
5692 my ($req, $err, $flags) = @_;
5694 my $u;
5695 my $auth_meth = $req->{'auth_method'} || "clear";
5696 my $username = $req->{'username'} || '';
5698 my $check_user = sub {
5699 return fail($err,100) unless $u;
5700 return fail($err,100) if ($u->{'statusvis'} eq "X");
5701 return fail($err,505) unless $u->{'clusterid'};
5702 return 1;
5705 unless ($auth_meth eq "oauth") {
5707 # add flag to avoid authentication
5708 if (!$username && $flags->{'allow_anonymous'}) {
5709 undef $flags->{'u'};
5710 return 1;
5713 return fail($err,200) unless $username;
5714 return fail($err,100) unless LJ::canonical_username($username);
5716 $u = $flags->{'u'};
5717 unless ($u) {
5718 my $dbr = LJ::get_db_reader();
5719 return fail($err,502) unless $dbr;
5720 $u = LJ::load_user($username);
5723 return unless $check_user->();
5726 my $auth_check = sub {
5727 if ($auth_meth eq "clear") {
5728 my $auth_ok;
5729 if (defined $req->{'hpassword'}) {
5730 $auth_ok = LJ::Auth::Method::LoginPassword::MD5->check($u, { password_md5 => $req->{'hpassword'} });
5732 else {
5733 $auth_ok = LJ::Auth::Method::LoginPassword::Clear->check($u, { password => $req->{'password'} });
5736 unless ($auth_ok) {
5737 return fail($err, 101);
5740 LJ::Session->record_login($u);
5741 return 1;
5743 elsif ($auth_meth eq "challenge") {
5744 my $challenge_result = {};
5745 my $auth_ok = LJ::Auth::Method::ChallengeResponse->check($u, {
5746 challenge => $req->{'auth_challenge'},
5747 response => $req->{'auth_response'},
5748 output => $challenge_result,
5749 } );
5751 unless ($auth_ok) {
5752 return fail($err, 105) if $challenge_result->{'expired'};
5753 return fail($err, 101);
5756 LJ::Session->record_login($u);
5757 return 1;
5759 elsif ($auth_meth eq "cookie") {
5760 return unless LJ::Request->is_inited && LJ::Request->header_in("X-LJ-Auth") eq "cookie";
5761 my $remote = LJ::get_remote();
5762 return $remote && $remote->{'user'} eq $username ? 1 : 0;
5764 elsif ($auth_meth eq "oauth"){
5765 my $rate_limiter =
5766 LJ::Request->is_inited ?
5767 LJ::API::RateLimiter->new(LJ::Request->request) :
5768 LJ::API::RateLimiter->new();
5770 my $oauth = LJ::OAuth->new(rate_limiter => $rate_limiter);
5772 my $result = $oauth->have_access;
5773 unless ($result->{http_status} == 200) {
5774 return fail($err,331,$result->{oauth_problem}) if $result->{http_status} == 400;
5775 return fail($err,332,$result->{oauth_problem}) if $result->{http_status} == 401;
5776 return fail($err,334,$result->{oauth_problem}) if $result->{http_status} == 403;
5777 return fail($err,413,$result->{oauth_problem}) if $result->{http_status} == 503;
5778 return fail($err,101);
5780 $u = $result->{user};
5781 return unless $check_user->();
5782 $flags->{'user_access'} = $result->{access};
5783 LJ::Session->record_login($u);
5787 unless ($flags->{'nopassword'} || $flags->{'noauth'}) {
5789 # check for rate limit of fault login attempts
5790 if ($u && LJ::Auth::Checker::login_ip_banned($u)) {
5791 return fail($err,402);
5794 my $auth_ok = $auth_check->();
5795 unless ($auth_ok) {
5796 return undef if $$err;
5797 return fail($err, 101);
5801 return 1 if $flags->{'allow_anonymous'} && !$u;
5803 # if there is a require TOS revision, check for it now
5804 return fail($err, 156) unless $u->tosagree_verify;
5806 # remember the user record for later.
5807 $flags->{'u'} = $u;
5809 if (LJ::Request->is_inited) {
5810 LJ::Request->notes("ljuser" => $u->{'user'}) unless LJ::Request->notes("ljuser");
5811 LJ::Request->notes("journalid" => $u->{'userid'}) unless LJ::Request->notes("journalid");
5814 #check on suspisious login from api (LJSUP-16353)
5815 LJ::run_hook('api_auth', $u, $req->{'props'}->{'interface'}, $req->{'auth_method'}, $req->{'method'});
5817 return 1;
5820 sub authorize
5822 my ($req, $err, $flags, $method) = @_;
5824 ### Use rate limits.
5825 if ( $flags->{is_use_limits} ) {
5827 my $rate_limiter = LJ::API::RateLimiter->new();
5829 if ( my $user = $flags->{u} ) {
5831 if ( !$rate_limiter->rate_point( "xmlrpc.$method.user", [ $user->user ] ) ) {
5832 return fail($err,402);
5835 } else {
5837 my $ip = LJ::get_remote_ip();
5839 if ( !$rate_limiter->rate_point( "xmlrpc.$method.anonym", [ $ip ] ) ) {
5840 return fail($err,402);
5845 #---------------------------#
5847 my $auth_method = $req->{'auth_method'};
5849 return 1 if ($flags->{noauth} || $flags->{nopassword});
5851 if ($auth_method eq 'oauth') {
5853 return fail($err,333) unless $flags->{'user_access'};
5854 return fail($err,333) unless defined $LJ::XMLRPC_USER_ACCESS{$method};
5856 my $access_required = ref $LJ::XMLRPC_USER_ACCESS{$method} ? $LJ::XMLRPC_USER_ACCESS{$method} : [$LJ::XMLRPC_USER_ACCESS{$method}];
5858 my %user_access = map {$_ => 1} @{$flags->{'user_access'}};
5860 foreach my $p (@$access_required){
5861 return fail($err,333) unless ( $user_access{$p} || ($p =~ /(.+)_ro$/) && $user_access{"$1_rw"} );
5865 if ($LJ::XMLRPC_VALIDATION_METHOD{$method}) {
5866 # Deny access for accounts that have not validated their email
5867 my $u = $flags->{'u'} || LJ::load_user($req->{'username'});
5868 unless ($u){
5869 return fail($err,335);
5871 unless ($u->is_validated) {
5872 return fail($err,336);
5876 return 1;
5879 sub fail
5881 my $err = shift;
5882 my $code = shift;
5883 my $des = shift;
5884 my $vars = shift;
5885 $code .= ":".($des =~ /^xmlrpc\.des\./ ? LJ::Lang::ml($des, $vars) : $des) if $des;
5886 $$err = $code if (ref $err eq "SCALAR");
5887 return undef;
5890 # PROBLEM: a while back we used auto_increment fields in our tables so that we could have
5891 # automatically incremented itemids and such. this was eventually phased out in favor of
5892 # the more portable alloc_user_counter function which uses the 'counter' table. when the
5893 # counter table has no data, it finds the highest id already in use in the database and adds
5894 # one to it.
5896 # a problem came about when users who last posted before alloc_user_counter went
5897 # and deleted all their entries and posted anew. alloc_user_counter would find no entries,
5898 # this no ids, and thus assign id 1, thinking it's all clean and new. but, id 1 had been
5899 # used previously, and now has comments attached to it.
5901 # the comments would happen because there was an old bug that wouldn't delete comments when
5902 # an entry was deleted. this has since been fixed. so this all combines to make this
5903 # a necessity, at least until no buggy data exist anymore!
5905 # this code here removes any comments that happen to exist for the id we're now using.
5906 sub new_entry_cleanup_hack {
5907 my ($u, $jitemid) = @_;
5909 # sanitize input
5910 $jitemid += 0;
5911 return unless $jitemid;
5912 my $ownerid = LJ::want_userid($u);
5913 return unless $ownerid;
5915 # delete logprops
5916 $u->do("DELETE FROM logprop2 WHERE journalid=$ownerid AND jitemid=$jitemid");
5918 # delete comments
5919 my $ids = LJ::Talk::get_talk_data($u, 'L', $jitemid);
5920 return unless ref $ids eq 'HASH' && %$ids;
5921 my $list = join ',', map { $_+0 } keys %$ids;
5922 $u->do("DELETE FROM talk2 WHERE journalid=$ownerid AND jtalkid IN ($list)");
5923 $u->do("DELETE FROM talktext2 WHERE journalid=$ownerid AND jtalkid IN ($list)");
5924 $u->do("DELETE FROM talkprop2 WHERE journalid=$ownerid AND jtalkid IN ($list)");
5927 sub un_utf8_request {
5928 my $req = shift;
5929 $req->{$_} = LJ::no_utf8_flag($req->{$_}) foreach qw(subject event);
5930 my $props = $req->{props} || {};
5931 foreach my $k (keys %$props) {
5932 next if ref $props->{$k}; # if this is multiple levels deep? don't think so.
5933 $props->{$k} = LJ::no_utf8_flag($props->{$k});
5937 # registerpush: adding push-notification params to user prop
5938 # specific for each mobile platform (windows phone 7, android, iOS)
5940 # takes:
5941 # - platform: wp7 / android / ios
5942 # - registrationid: argument which we use in communication with notification
5943 # servers, specific for each OS
5944 # - deviceid: id of registred device (not use yet)
5946 # returns: { status => 'OK'} if success
5948 sub registerpush {
5949 my ($req, $err, $flags) = @_;
5951 return undef
5952 unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'registerpush');
5954 my $u = $flags->{u};
5956 return fail($err, 200)
5957 unless $u && $req->{platform} && $req->{deviceid};
5959 my $error = LJ::PushNotification->subscribe($u, $req);
5960 return fail($error, 412) if $error;
5962 return { status => 'OK' }
5965 # unregisterpush: deletes subscription on push notification and clears user prop
5966 # with notification servers connection arguments
5968 # takes:
5969 # - platform: wp7 / android / ios
5970 # - deviceid: id of registred device (not use yet)
5972 # returns: { status => 'OK'} if success
5974 sub unregisterpush {
5975 my ($req, $err, $flags) = @_;
5976 return undef
5977 unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'unregisterpush');
5979 my $u = $flags->{u};
5981 return fail($err,200)
5982 unless $req->{platform};
5984 my $error = LJ::PushNotification->unsubscribe($u, $req);
5985 return $error if $error;
5987 return { status => 'OK' };
5990 sub pushsubscriptions {
5991 my ($req, $err, $flags) = @_;
5992 return undef
5993 unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'pushsubscriptions');
5995 my $u = $flags->{u};
5996 my @errors;
5997 foreach my $event (@{$req->{events}}) {
5998 if($event->{action} =~ /^(un)?subscribe$/) {
6000 my $res = eval{
6001 LJ::PushNotification->manage(
6003 app_name => $req->{app_name},
6004 platform => $req->{platform},
6005 deviceid => $req->{deviceid},
6006 optional_data => $req->{optional_data},
6007 %$event,
6011 push @errors, $@
6012 if $@;
6014 } else {
6015 push @errors, "wrong action '$event->{action}'";
6019 return { status => 'Has errors', errors => join "; ", @errors }
6020 if @errors;
6022 return { status => 'OK' };
6026 sub resetpushcounter {
6027 my ($req, $err, $flags) = @_;
6028 return undef
6029 unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'resetpushcounter');
6031 my $u = $flags->{u};
6033 return fail($err,200)
6034 unless $req->{platform} && $req->{deviceid};
6036 return fail($err,200)
6037 if $req->{platform} eq 'android';
6040 if(LJ::PushNotification::Storage->reset_counter($u, $req)) {
6041 return { status => 'OK' }
6044 return { status => 'Error', error => "Can't reset counter"}
6048 sub getpushlist {
6049 my ($req, $err, $flags) = @_;
6050 return undef
6051 unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getpushlist');
6053 my $u = $flags->{u};
6055 return fail($err,200)
6056 unless $req->{platform} && $req->{deviceid};
6058 my @subs = grep { $_->{ntypeid} == LJ::NotificationMethod::Push->ntypeid } ($u->subscriptions);
6060 my @events;
6061 foreach my $s (@subs) {
6063 my ($event) = $s->event_class =~ /LJ::Event::(.*)/;
6065 my $journal = LJ::load_userid($s->{journalid});
6067 my %event = (
6068 event => $event,
6071 $event{journal} = LJ::load_userid($s->{journalid})->user
6072 if $s->{journalid} != $s->{userid};
6074 if($event eq 'JournalNewComment' && $s->arg1) {
6075 $event{ditemid} = $s->arg1;
6078 if($event eq 'JournalNewComment' && $s->arg2) {
6079 my $comment = LJ::Comment->instance($s->{journalid}, jtalkid => $s->arg2);
6080 $event{dtalkid} = $comment->dtalkid;
6083 push @events, \%event;
6086 return {
6087 status => 'OK',
6088 events => \@events,
6093 sub geteventsrating {
6094 my ($req, $err, $flags) = @_;
6096 $flags->{allow_anonymous} = 1;
6097 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'geteventsrating');
6099 # unpacking parameters
6100 my $category = $req->{category };
6101 my $item_show = $req->{itemshow } || 30;
6102 my $region = exists $req->{region } ? $req->{region} : 'cyr';
6103 my $sort = exists $req->{sort } ? $req->{sort} : 'hits';
6104 my $skip = exists $req->{skip } ? $req->{skip} : 0;
6105 my $is_show_promo = $req->{show_promo };
6107 my $user_id = $flags->{u} ? $flags->{u}->id : 0;
6108 #----------------------#
6110 # searching errors
6111 my @er = ($err, 203);
6113 return fail( @er, 'sort') if $sort !~ m{^( hits | visitors )$}x;
6114 return fail( @er, 'region') if $region !~ m{^( cyr | noncyr | ua )$}x;
6116 push @er, 'xmlrpc.des.non_arifmetic';
6118 return fail( @er, { param => 'skip', value => $skip }) if $skip =~ m{\D};
6119 return fail( @er, { param => 'itemshow', value => $item_show }) if $item_show =~ m{\D};
6120 return fail( @er, { param => 'user_id', value => $user_id }) if $user_id =~ m{\D};
6122 return fail( $err, 209, 'xmlrpc.des.bad_value', {param => 'itemshow'} ) if $item_show > 100;
6123 #----------------------#
6125 my $answer;
6127 my $rating = LJ::PersonalStats::Ratings::Posts::Top->new( {
6128 homepage_v2 => 1,
6129 category_id => $category,
6130 country => $region,
6131 sort => $sort,
6132 skip => $skip,
6133 length => $item_show,
6134 filter_commpromo => 1,
6135 filter_selfpromo => 1,
6136 filter_featured => 0,
6137 filter_blacklist => 0,
6138 filter_readlist => 0,
6139 unique_journals => 0,
6140 do_not_load_data => 0,
6141 do_not_include_promo => !$is_show_promo,
6142 } );
6144 $answer->{posts} = $rating->ajax_output();
6146 my $selfpromo;
6147 if (
6148 $rating->selfpromo()
6149 &&( my $sp = $rating->selfpromo()->current_promoted_object() )
6152 my $entry = $sp->object();
6153 my $poster = $entry->poster();
6155 $selfpromo = {
6156 # entry data
6157 ditemid => $entry->ditemid(),
6158 subject => $entry->subject_raw(),
6159 body => $entry->event_raw(),
6160 url => $entry->url(),
6162 # user data
6163 poster_id => $poster->id(),
6164 poster_name => $poster->name(),
6165 poster_journal => $poster->journal_url(),
6168 $answer->{selfpromo} = $selfpromo;
6171 my $commpromo;
6172 if (
6173 $rating->commpromo()
6174 &&( my $cp = $rating->commpromo()->promoted_object() )
6176 my $entry = $cp->object();
6177 my $poster = $entry->poster();
6179 $commpromo = {
6180 # entry data
6181 ditemid => $entry->ditemid(),
6182 subject => $entry->subject_raw(),
6183 body => $entry->event_raw(),
6184 url => $entry->url(),
6186 # user data
6187 poster_id => $poster->id(),
6188 poster_name => $poster->name(),
6189 poster_journal => $poster->journal_url(),
6192 $answer->{commpromo} = $commpromo;
6195 $answer->{status} = 'OK';
6197 return $answer;
6200 sub getusersrating {
6201 my ($req, $err, $flags) = @_;
6203 $flags->{allow_anonymous} = 1;
6204 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getusersrating');
6206 my $user_id = $flags->{u} ? $flags->{u}->id : 0;
6208 return fail($err, 200, 'region') unless $req->{region};
6210 return fail($err, 203, 'region') unless $req->{region} =~ /^cyr|noncyr|ua$/;
6212 return fail($err, 203, 'sort') if $req->{sort} && $req->{sort} !~ /^hits|friends|authority|default$/;
6214 foreach my $p (qw(skip itemshow user_id)){
6215 return fail($err, 203, 'xmlrpc.des.non_arifmetic', {'param'=>$p, 'value'=>$req->{$p}}) if ($req->{$p} && $req->{$p} =~ /\D/);
6218 return fail($err, 209, 'xmlrpc.des.bad_value', {'param'=>'itemshow'}) if $req->{itemshow} > 100;
6220 $req->{getselfpromo} = 1 unless defined $req->{getselfpromo};
6222 my ($res, @err) = LJ::PersonalStats::Ratings::Journals->get_rating_segment( {
6223 rating_country => $req->{region},
6224 ($req->{sort} ne 'default' ? (sort => $req->{sort}) : ()),
6225 is_community => $req->{journaltype} eq 'C' ? 1 : 0,
6226 offset => $req->{skip} || 0,
6227 length => $req->{itemshow} || 30,
6228 show_selfpromo => $req->{getselfpromo},
6229 filter_selfpromo => $req->{getselfpromo},
6232 return fail($err, 500, $err[0]) unless $res && ref $res && $res->{data} && ref $res->{data};
6234 my (@users, $selfpromo);
6236 my $user_opts = {
6237 attrs => [qw(username display_name profile_url journal_base userpic userhead_url name_raw
6238 identity_pretty_type identity_value identity_url )],
6241 foreach my $row (@{$res->{data}}) {
6243 $row->{userid} = delete $row->{journal_id} if $row->{journal_id};
6244 LJ::get_aggregated_user($row, $user_opts);
6246 push @users, {
6247 # rating data
6248 rating_value => $row->{value},
6249 position => $row->{position},
6250 delta => $row->{delta},
6251 isnew => $row->{is_new} || 0,
6252 was_in_promo => $row->{was_in_promo} || 0,
6253 # user data
6254 username => $row->{username},
6255 identity_display => $row->{display_name},
6256 identity_url => $row->{identity_url},
6257 identity_type => $row->{identity_pretty_type},
6258 identity_value => $row->{identity_value},
6259 userpic_url => $row->{userpic} ? $row->{userpic}->url : '',
6260 journal_url => $row->{journal_base},
6261 userhead_url => $row->{userhead_url},
6262 title => $row->{name_raw},
6266 if (my $sp = $res->{selfpromo} && $res->{selfpromo}->get_template_params ) {
6268 $selfpromo = {
6269 # selfpromo data
6270 remaning_time => $sp->{timeleft},
6271 price => $sp->{buyout},
6274 $sp = $sp->{object}->[0];
6276 $sp->{userid} = delete $sp->{journal_id} if $sp->{journal_id};
6277 LJ::get_aggregated_user($sp, $user_opts);
6279 $selfpromo = {
6280 %$selfpromo,
6281 # user data
6282 username => $sp->{username},
6283 identity_display => $sp->{display_name},
6284 identity_url => $sp->{identity_url},
6285 identity_type => $sp->{identity_pretty_type},
6286 identity_value => $sp->{identity_value},
6287 userpic_url => $sp->{userpic} ? $sp->{userpic}->url : '',
6288 journal_url => $sp->{journal_base},
6289 userhead_url => $sp->{userhead_url},
6290 title => $sp->{name_raw},
6294 return {
6295 status => 'OK',
6296 skip => $req->{skip} || 0,
6297 region => $req->{region},
6298 users => \@users,
6299 ($req->{getselfpromo} ? (selfpromo => $selfpromo) : ())
6303 sub getratingcategories {
6304 my ($req, $err, $flags) = @_;
6306 $flags->{allow_anonymous} = 1;
6307 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getratingcategories');
6309 my $categories = LJ::HomePage::Category->get_all_categories();
6310 @$categories = map { $_->get_template_params } @$categories;
6312 my @result = ();
6313 foreach (@$categories) {
6314 if ( $_->{is_visible} ) {
6315 push @result, {
6316 'name' => $_->{name},
6317 'id' => $_->{id },
6322 return \@result;
6325 ###################################################################
6326 ################################# Discovery functions ##
6327 ###################################################################
6329 sub getdiscoveryfeed {
6330 my ($req, $err, $flags) = @_;
6332 # my @discovery_feeds = LJ::Discovery::Feed->get_items(
6333 # category => $category,
6334 # itemshow => $item_show,
6335 # );
6337 # my @posts;
6338 # foreach my $item (@discovery_feeds) {
6340 # my $entry = LJ::Discovery::Item->get(
6341 # url => $item->{url},
6342 # )->entry();
6344 # my $poster = $entry->poster();
6346 # push @{ $answer->{posts} }, {
6347 # # entry data
6348 # ditemid => $entry->ditemid(),
6349 # subject => $entry->subject_raw(),
6350 # body => $entry->event_raw(),
6351 # url => $entry->url(),
6353 # # user data
6354 # poster_id => $poster->id(),
6355 # poster_name => $poster->name(),
6356 # poster_journal => $poster->journal_url(),
6357 # };
6360 # my @announces = LJ::Discovery::Announce->get_items();
6362 # $answer->{announces} = \@announces;
6364 return {};
6367 sub getdiscoverycategories {
6368 my ($req, $err, $flags) = @_;
6370 $flags->{allow_anonymous} = 1;
6371 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getdiscoverycategories');
6373 my @categories = LJ::Discovery::Category->list();
6375 my @result = ();
6376 foreach (@categories) {
6377 push @result, {
6378 'name' => $_->{name},
6379 'id' => $_->{id },
6383 return \@result;
6386 sub getdiscoveryitem {
6387 my ($req, $err, $flags) = @_;
6389 return {};
6392 #### Old interface (flat key/values) -- wrapper aruond LJ::Protocol
6393 package LJ;
6395 sub do_request {
6396 # get the request and response hash refs
6397 my ($req, $res, $flags) = @_;
6399 # initialize some stuff
6400 %$res = (); # clear the given response hash
6401 $flags = {} unless (ref $flags eq "HASH");
6403 # did they send a mode?
6404 unless ($req->{'mode'}) {
6405 $res->{'success'} = "FAIL";
6406 $res->{'errmsg'} = "Client error: No mode specified.";
6407 return;
6410 # this method doesn't require auth
6411 if ($req->{'mode'} eq "getchallenge") {
6412 return getchallenge($req, $res, $flags);
6415 # mode from here on out require a username
6416 my $user = LJ::canonical_username($req->{'user'});
6417 unless ($user) {
6418 $res->{'success'} = "FAIL";
6419 $res->{'errmsg'} = "Client error: No username sent.";
6420 return;
6423 ### see if the server's under maintenance now
6424 if ($LJ::SERVER_DOWN) {
6425 $res->{'success'} = "FAIL";
6426 $res->{'errmsg'} = $LJ::SERVER_DOWN_MESSAGE;
6427 return;
6430 ## dispatch wrappers
6431 if ($req->{'mode'} eq "login") {
6432 return login($req, $res, $flags);
6434 if ($req->{'mode'} eq "getfriendgroups") {
6435 return getfriendgroups($req, $res, $flags);
6437 if ($req->{'mode'} eq "getfriends") {
6438 return getfriends($req, $res, $flags);
6440 if ($req->{'mode'} eq "friendof") {
6441 return friendof($req, $res, $flags);
6443 if ($req->{'mode'} eq "checkfriends") {
6444 return checkfriends($req, $res, $flags);
6446 if ($req->{'mode'} eq "getdaycounts") {
6447 return getdaycounts($req, $res, $flags);
6449 if ($req->{'mode'} eq "postevent") {
6450 return postevent($req, $res, $flags);
6452 if ($req->{'mode'} eq "editevent") {
6453 return editevent($req, $res, $flags);
6455 if ($req->{'mode'} eq "syncitems") {
6456 return syncitems($req, $res, $flags);
6458 if ($req->{'mode'} eq "getevents") {
6459 return getevents($req, $res, $flags);
6461 if ($req->{'mode'} eq "editfriends") {
6462 return editfriends($req, $res, $flags);
6464 if ($req->{'mode'} eq "editfriendgroups") {
6465 return editfriendgroups($req, $res, $flags);
6467 if ($req->{'mode'} eq "consolecommand") {
6468 return consolecommand($req, $res, $flags);
6470 if ($req->{'mode'} eq "sessiongenerate") {
6471 return sessiongenerate($req, $res, $flags);
6473 if ($req->{'mode'} eq "sessionexpire") {
6474 return sessionexpire($req, $res, $flags);
6476 if ($req->{'mode'} eq "getusertags") {
6477 return getusertags($req, $res, $flags);
6479 if ($req->{'mode'} eq "getfriendspage") {
6480 return getfriendspage($req, $res, $flags);
6483 ### unknown mode!
6484 $res->{'success'} = "FAIL";
6485 $res->{'errmsg'} = "Client error: Unknown mode ($req->{'mode'})";
6486 return;
6489 ## flat wrapper
6490 sub getfriendspage
6492 my ($req, $res, $flags) = @_;
6494 my $err = 0;
6495 my $rq = upgrade_request($req);
6497 my $rs = LJ::Protocol::do_request("getfriendspage", $rq, \$err, $flags);
6498 unless ($rs) {
6499 $res->{'success'} = "FAIL";
6500 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6501 return 0;
6504 my $ect = 0;
6505 foreach my $evt (@{$rs->{'entries'}}) {
6506 $ect++;
6507 foreach my $f (qw(subject_raw journalname journaltype postername postertype ditemid security)) {
6508 if (defined $evt->{$f}) {
6509 $res->{"entries_${ect}_$f"} = $evt->{$f};
6512 $res->{"entries_${ect}_event"} = LJ::eurl($evt->{'event_raw'});
6515 $res->{'entries_count'} = $ect;
6516 $res->{'success'} = "OK";
6518 return 1;
6521 ## flat wrapper
6522 sub login
6524 my ($req, $res, $flags) = @_;
6526 my $err = 0;
6527 my $rq = upgrade_request($req);
6529 my $rs = LJ::Protocol::do_request("login", $rq, \$err, $flags);
6530 unless ($rs) {
6531 $res->{'success'} = "FAIL";
6532 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6533 return 0;
6536 $res->{'success'} = "OK";
6537 $res->{'name'} = $rs->{'fullname'};
6538 $res->{'message'} = $rs->{'message'} if $rs->{'message'};
6539 $res->{'fastserver'} = 1 if $rs->{'fastserver'};
6540 $res->{'caps'} = $rs->{'caps'} if $rs->{'caps'};
6542 # shared journals
6543 my $access_count = 0;
6544 foreach my $user (@{$rs->{'usejournals'}}) {
6545 $access_count++;
6546 $res->{"access_${access_count}"} = $user;
6548 if ($access_count) {
6549 $res->{"access_count"} = $access_count;
6552 # friend groups
6553 populate_friend_groups($res, $rs->{'friendgroups'});
6555 my $flatten = sub {
6556 my ($prefix, $listref) = @_;
6557 my $ct = 0;
6558 foreach (@$listref) {
6559 $ct++;
6560 $res->{"${prefix}_$ct"} = $_;
6562 $res->{"${prefix}_count"} = $ct;
6565 ### picture keywords
6566 $flatten->("pickw", $rs->{'pickws'})
6567 if defined $req->{"getpickws"};
6568 $flatten->("pickwurl", $rs->{'pickwurls'})
6569 if defined $req->{"getpickwurls"};
6570 $res->{'defaultpicurl'} = $rs->{'defaultpicurl'} if $rs->{'defaultpicurl'};
6572 ### report new moods that this client hasn't heard of, if they care
6573 if (defined $req->{"getmoods"}) {
6574 my $mood_count = 0;
6575 foreach my $m (@{$rs->{'moods'}}) {
6576 $mood_count++;
6577 $res->{"mood_${mood_count}_id"} = $m->{'id'};
6578 $res->{"mood_${mood_count}_name"} = $m->{'name'};
6579 $res->{"mood_${mood_count}_parent"} = $m->{'parent'};
6581 if ($mood_count) {
6582 $res->{"mood_count"} = $mood_count;
6586 #### send web menus
6587 if ($req->{"getmenus"} == 1) {
6588 my $menu = $rs->{'menus'};
6589 my $menu_num = 0;
6590 populate_web_menu($res, $menu, \$menu_num);
6593 return 1;
6596 ## flat wrapper
6597 sub getfriendgroups
6599 my ($req, $res, $flags) = @_;
6601 my $err = 0;
6602 my $rq = upgrade_request($req);
6604 my $rs = LJ::Protocol::do_request("getfriendgroups", $rq, \$err, $flags);
6605 unless ($rs) {
6606 $res->{'success'} = "FAIL";
6607 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6608 return 0;
6610 $res->{'success'} = "OK";
6611 populate_friend_groups($res, $rs->{'friendgroups'});
6613 return 1;
6616 ## flat wrapper
6617 sub getusertags
6619 my ($req, $res, $flags) = @_;
6621 my $err = 0;
6622 my $rq = upgrade_request($req);
6624 my $rs = LJ::Protocol::do_request("getusertags", $rq, \$err, $flags);
6625 unless ($rs) {
6626 $res->{'success'} = "FAIL";
6627 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6628 return 0;
6631 $res->{'success'} = "OK";
6633 my $ct = 0;
6634 foreach my $tag (@{$rs->{tags}}) {
6635 $ct++;
6636 $res->{"tag_${ct}_security"} = $tag->{security_level};
6637 $res->{"tag_${ct}_uses"} = $tag->{uses} if $tag->{uses};
6638 $res->{"tag_${ct}_display"} = $tag->{display} if $tag->{display};
6639 $res->{"tag_${ct}_name"} = $tag->{name};
6640 foreach my $lev (qw(friends private public)) {
6641 $res->{"tag_${ct}_sb_$_"} = $tag->{security}->{$_}
6642 if $tag->{security}->{$_};
6644 my $gm = 0;
6645 foreach my $grpid (keys %{$tag->{security}->{groups}}) {
6646 next unless $tag->{security}->{groups}->{$grpid};
6647 $gm++;
6648 $res->{"tag_${ct}_sb_group_${gm}_id"} = $grpid;
6649 $res->{"tag_${ct}_sb_group_${gm}_count"} = $tag->{security}->{groups}->{$grpid};
6651 $res->{"tag_${ct}_sb_group_count"} = $gm if $gm;
6653 $res->{'tag_count'} = $ct;
6655 return 1;
6658 ## flat wrapper
6659 sub getfriends
6661 my ($req, $res, $flags) = @_;
6663 my $err = 0;
6664 my $rq = upgrade_request($req);
6666 my $rs = LJ::Protocol::do_request("getfriends", $rq, \$err, $flags);
6667 unless ($rs) {
6668 $res->{'success'} = "FAIL";
6669 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6670 return 0;
6673 $res->{'success'} = "OK";
6674 if ($req->{'includegroups'}) {
6675 populate_friend_groups($res, $rs->{'friendgroups'});
6677 if ($req->{'includefriendof'}) {
6678 populate_friends($res, "friendof", $rs->{'friendofs'});
6680 populate_friends($res, "friend", $rs->{'friends'});
6682 return 1;
6685 ## flat wrapper
6686 sub friendof
6688 my ($req, $res, $flags) = @_;
6690 my $err = 0;
6691 my $rq = upgrade_request($req);
6693 my $rs = LJ::Protocol::do_request("friendof", $rq, \$err, $flags);
6694 unless ($rs) {
6695 $res->{'success'} = "FAIL";
6696 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6697 return 0;
6700 $res->{'success'} = "OK";
6701 populate_friends($res, "friendof", $rs->{'friendofs'});
6702 return 1;
6705 ## flat wrapper
6706 sub checkfriends
6708 my ($req, $res, $flags) = @_;
6710 my $err = 0;
6711 my $rq = upgrade_request($req);
6713 my $rs = LJ::Protocol::do_request("checkfriends", $rq, \$err, $flags);
6714 unless ($rs) {
6715 $res->{'success'} = "FAIL";
6716 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6717 return 0;
6720 $res->{'success'} = "OK";
6721 $res->{'new'} = $rs->{'new'};
6722 $res->{'lastupdate'} = $rs->{'lastupdate'};
6723 $res->{'interval'} = $rs->{'interval'};
6724 return 1;
6727 ## flat wrapper
6728 sub getdaycounts
6730 my ($req, $res, $flags) = @_;
6732 my $err = 0;
6733 my $rq = upgrade_request($req);
6735 my $rs = LJ::Protocol::do_request("getdaycounts", $rq, \$err, $flags);
6736 unless ($rs) {
6737 $res->{'success'} = "FAIL";
6738 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6739 return 0;
6742 $res->{'success'} = "OK";
6743 foreach my $d (@{ $rs->{'daycounts'} }) {
6744 $res->{$d->{'date'}} = $d->{'count'};
6746 return 1;
6749 ## flat wrapper
6750 sub syncitems
6752 my ($req, $res, $flags) = @_;
6754 my $err = 0;
6755 my $rq = upgrade_request($req);
6757 my $rs = LJ::Protocol::do_request("syncitems", $rq, \$err, $flags);
6758 unless ($rs) {
6759 $res->{'success'} = "FAIL";
6760 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6761 return 0;
6764 $res->{'success'} = "OK";
6765 $res->{'sync_total'} = $rs->{'total'};
6766 $res->{'sync_count'} = $rs->{'count'};
6768 my $ct = 0;
6769 foreach my $s (@{ $rs->{'syncitems'} }) {
6770 $ct++;
6771 foreach my $a (qw(item action time)) {
6772 $res->{"sync_${ct}_$a"} = $s->{$a};
6775 return 1;
6778 ## flat wrapper: limited functionality. (1 command only, server-parsed only)
6779 sub consolecommand
6781 my ($req, $res, $flags) = @_;
6783 my $err = 0;
6784 my $rq = upgrade_request($req);
6785 delete $rq->{'command'};
6787 $rq->{'commands'} = [ $req->{'command'} ];
6789 my $rs = LJ::Protocol::do_request("consolecommand", $rq, \$err, $flags);
6790 unless ($rs) {
6791 $res->{'success'} = "FAIL";
6792 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6793 return 0;
6796 $res->{'cmd_success'} = $rs->{'results'}->[0]->{'success'};
6797 $res->{'cmd_line_count'} = 0;
6798 foreach my $l (@{$rs->{'results'}->[0]->{'output'}}) {
6799 $res->{'cmd_line_count'}++;
6800 my $line = $res->{'cmd_line_count'};
6801 $res->{"cmd_line_${line}_type"} = $l->[0]
6802 if $l->[0];
6803 $res->{"cmd_line_${line}"} = $l->[1];
6806 $res->{'success'} = "OK";
6810 ## flat wrapper
6811 sub getchallenge
6813 my ($req, $res, $flags) = @_;
6814 my $err = 0;
6815 my $rs = LJ::Protocol::do_request("getchallenge", $req, \$err, $flags);
6817 # stupid copy (could just return $rs), but it might change in the future
6818 # so this protects us from future accidental harm.
6819 foreach my $k (qw(challenge server_time expire_time auth_scheme)) {
6820 $res->{$k} = $rs->{$k};
6823 $res->{'success'} = "OK";
6824 return $res;
6827 ## flat wrapper
6828 sub editfriends
6830 my ($req, $res, $flags) = @_;
6832 my $err = 0;
6833 my $rq = upgrade_request($req);
6835 $rq->{'add'} = [];
6836 $rq->{'delete'} = [];
6838 foreach (keys %$req) {
6839 if (/^editfriend_add_(\d+)_user$/) {
6840 my $n = $1;
6841 next unless ($req->{"editfriend_add_${n}_user"} =~ /\S/);
6842 my $fa = { 'username' => $req->{"editfriend_add_${n}_user"},
6843 'fgcolor' => $req->{"editfriend_add_${n}_fg"},
6844 'bgcolor' => $req->{"editfriend_add_${n}_bg"},
6845 'groupmask' => $req->{"editfriend_add_${n}_groupmask"},
6847 push @{$rq->{'add'}}, $fa;
6848 } elsif (/^editfriend_delete_(\w+)$/) {
6849 push @{$rq->{'delete'}}, $1;
6853 my $rs = LJ::Protocol::do_request("editfriends", $rq, \$err, $flags);
6854 unless ($rs) {
6855 $res->{'success'} = "FAIL";
6856 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6857 return 0;
6860 $res->{'success'} = "OK";
6862 my $ct = 0;
6863 foreach my $fa (@{ $rs->{'added'} }) {
6864 $ct++;
6865 $res->{"friend_${ct}_user"} = $fa->{'username'};
6866 $res->{"friend_${ct}_name"} = $fa->{'fullname'};
6869 $res->{'friends_added'} = $ct;
6871 return 1;
6874 ## flat wrapper
6875 sub editfriendgroups
6877 my ($req, $res, $flags) = @_;
6879 my $err = 0;
6880 my $rq = upgrade_request($req);
6882 $rq->{'groupmasks'} = {};
6883 $rq->{'set'} = {};
6884 $rq->{'delete'} = [];
6886 foreach (keys %$req) {
6887 if (/^efg_set_(\d+)_name$/) {
6888 next unless ($req->{$_} ne "");
6889 my $n = $1;
6890 my $fs = {
6891 'name' => $req->{"efg_set_${n}_name"},
6892 'sort' => $req->{"efg_set_${n}_sort"},
6894 if (defined $req->{"efg_set_${n}_public"}) {
6895 $fs->{'public'} = $req->{"efg_set_${n}_public"};
6897 $rq->{'set'}->{$n} = $fs;
6899 elsif (/^efg_delete_(\d+)$/) {
6900 if ($req->{$_}) {
6901 # delete group if value is true
6902 push @{$rq->{'delete'}}, $1;
6905 elsif (/^editfriend_groupmask_(\w+)$/) {
6906 $rq->{'groupmasks'}->{$1} = $req->{$_};
6910 my $rs = LJ::Protocol::do_request("editfriendgroups", $rq, \$err, $flags);
6911 unless ($rs) {
6912 $res->{'success'} = "FAIL";
6913 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6914 return 0;
6917 $res->{'success'} = "OK";
6918 return 1;
6921 sub flatten_props
6923 my ($req, $rq) = @_;
6925 ## changes prop_* to props hashref
6926 foreach my $k (keys %$req) {
6927 next unless ($k =~ /^prop_(.+)/);
6928 $rq->{'props'}->{$1} = $req->{$k};
6932 ## flat wrapper
6933 sub postevent
6935 my ($req, $res, $flags) = @_;
6937 my $err = 0;
6938 my $rq = upgrade_request($req);
6939 flatten_props($req, $rq);
6940 $rq->{'props'}->{'interface'} = "flat";
6942 my $rs = LJ::Protocol::do_request("postevent", $rq, \$err, $flags);
6943 unless ($rs) {
6944 $res->{'success'} = "FAIL";
6945 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6946 return 0;
6949 $res->{'message'} = $rs->{'message'} if $rs->{'message'};
6950 $res->{'extra_result_message'} = $rs->{'extra_result_message'} if $rs->{'extra_result_message'};
6951 $res->{'success'} = "OK";
6952 $res->{'itemid'} = $rs->{'itemid'};
6953 $res->{'anum'} = $rs->{'anum'} if defined $rs->{'anum'};
6954 $res->{'url'} = $rs->{'url'} if defined $rs->{'url'};
6955 # we may not translate 'warnings' here, because it may contain \n characters
6956 return 1;
6959 ## flat wrapper
6960 sub editevent
6962 my ($req, $res, $flags) = @_;
6964 my $err = 0;
6965 my $rq = upgrade_request($req);
6966 flatten_props($req, $rq);
6967 $rq->{'props'}->{'interface'} = "flat";
6969 my $rs = LJ::Protocol::do_request("editevent", $rq, \$err, $flags);
6970 unless ($rs) {
6971 $res->{'success'} = "FAIL";
6972 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6973 return 0;
6976 $res->{'success'} = "OK";
6977 $res->{'itemid'} = $rs->{'itemid'};
6978 $res->{'anum'} = $rs->{'anum'} if defined $rs->{'anum'};
6979 $res->{'url'} = $rs->{'url'} if defined $rs->{'url'};
6980 return 1;
6983 ## flat wrapper
6984 sub sessiongenerate {
6985 my ($req, $res, $flags) = @_;
6987 my $err = 0;
6988 my $rq = upgrade_request($req);
6990 my $rs = LJ::Protocol::do_request('sessiongenerate', $rq, \$err, $flags);
6991 unless ($rs) {
6992 $res->{success} = 'FAIL';
6993 $res->{errmsg} = LJ::Protocol::error_message($err);
6996 $res->{success} = 'OK';
6997 $res->{ljsession} = $rs->{ljsession};
6998 return 1;
7001 ## flat wrappre
7002 sub sessionexpire {
7003 my ($req, $res, $flags) = @_;
7005 my $err = 0;
7006 my $rq = upgrade_request($req);
7008 $rq->{expire} = [];
7009 foreach my $k (keys %$rq) {
7010 push @{$rq->{expire}}, $1
7011 if $k =~ /^expire_id_(\d+)$/;
7014 my $rs = LJ::Protocol::do_request('sessionexpire', $rq, \$err, $flags);
7015 unless ($rs) {
7016 $res->{success} = 'FAIL';
7017 $res->{errmsg} = LJ::Protocol::error_message($err);
7020 $res->{success} = 'OK';
7021 return 1;
7024 ## flat wrapper
7025 sub getevents {
7026 my ($req, $res, $flags) = @_;
7028 my $err = 0;
7029 my $rq = upgrade_request($req);
7031 my $rs = LJ::Protocol::do_request("getevents", $rq, \$err, $flags);
7032 unless ($rs) {
7033 $res->{'success'} = "FAIL";
7034 $res->{'errmsg'} = LJ::Protocol::error_message($err);
7035 return 0;
7038 my $pct = 0;
7039 my $ect = 0;
7041 foreach my $evt (@{$rs->{'events'}}) {
7042 $ect++;
7043 foreach my $f (qw(itemid eventtime security allowmask subject anum url poster)) {
7044 if (defined $evt->{$f}) {
7045 $res->{"events_${ect}_$f"} = $evt->{$f};
7048 $res->{"events_${ect}_event"} = LJ::eurl($evt->{'event'});
7050 if ($evt->{'props'}) {
7051 foreach my $k (sort keys %{$evt->{'props'}}) {
7052 $pct++;
7053 $res->{"prop_${pct}_itemid"} = $evt->{'itemid'};
7054 $res->{"prop_${pct}_name"} = $k;
7055 $res->{"prop_${pct}_value"} = $evt->{'props'}->{$k};
7060 unless ($req->{'noprops'}) {
7061 $res->{'prop_count'} = $pct;
7064 $res->{'events_count'} = $ect;
7065 $res->{'success'} = "OK";
7067 return 1;
7071 sub populate_friends
7073 my ($res, $pfx, $list) = @_;
7074 my $count = 0;
7075 foreach my $f (@$list)
7077 $count++;
7078 $res->{"${pfx}_${count}_name"} = $f->{'fullname'};
7079 $res->{"${pfx}_${count}_user"} = $f->{'username'};
7080 $res->{"${pfx}_${count}_birthday"} = $f->{'birthday'} if $f->{'birthday'};
7081 $res->{"${pfx}_${count}_bg"} = $f->{'bgcolor'};
7082 $res->{"${pfx}_${count}_fg"} = $f->{'fgcolor'};
7083 if (defined $f->{'groupmask'}) {
7084 $res->{"${pfx}_${count}_groupmask"} = $f->{'groupmask'};
7086 if (defined $f->{'type'}) {
7087 $res->{"${pfx}_${count}_type"} = $f->{'type'};
7088 if ($f->{'type'} eq 'identity') {
7089 $res->{"${pfx}_${count}_identity_type"} = $f->{'identity_type'};
7090 $res->{"${pfx}_${count}_identity_value"} = $f->{'identity_value'};
7091 $res->{"${pfx}_${count}_identity_display"} = $f->{'identity_display'};
7094 if (defined $f->{'status'}) {
7095 $res->{"${pfx}_${count}_status"} = $f->{'status'};
7098 $res->{"${pfx}_count"} = $count;
7102 sub upgrade_request
7104 my $r = shift;
7105 my $new = { %{ $r } };
7106 $new->{'username'} = $r->{'user'};
7108 # but don't delete $r->{'user'}, as it might be, say, %FORM,
7109 # that'll get reused in a later request in, say, update.bml after
7110 # the login before postevent. whoops.
7112 return $new;
7115 ## given a $res hashref and friend group subtree (arrayref), flattens it
7116 sub populate_friend_groups
7118 my ($res, $fr) = @_;
7120 my $maxnum = 0;
7121 foreach my $fg (@$fr)
7123 my $num = $fg->{'id'};
7124 $res->{"frgrp_${num}_name"} = $fg->{'name'};
7125 $res->{"frgrp_${num}_sortorder"} = $fg->{'sortorder'};
7126 if ($fg->{'public'}) {
7127 $res->{"frgrp_${num}_public"} = 1;
7129 if ($num > $maxnum) { $maxnum = $num; }
7131 $res->{'frgrp_maxnum'} = $maxnum;
7134 ## given a menu tree, flattens it into $res hashref
7135 sub populate_web_menu
7137 my ($res, $menu, $numref) = @_;
7138 my $mn = $$numref; # menu number
7139 my $mi = 0; # menu item
7140 foreach my $it (@$menu) {
7141 $mi++;
7142 $res->{"menu_${mn}_${mi}_text"} = $it->{'text'};
7143 if ($it->{'text'} eq "-") { next; }
7144 if ($it->{'sub'}) {
7145 $$numref++;
7146 $res->{"menu_${mn}_${mi}_sub"} = $$numref;
7147 &populate_web_menu($res, $it->{'sub'}, $numref);
7148 next;
7151 $res->{"menu_${mn}_${mi}_url"} = $it->{'url'};
7153 $res->{"menu_${mn}_count"} = $mi;