LJSUP-16152: Implement targeting logic for paid reposts
[livejournal.git] / cgi-bin / ljprotocol.pl
blob7cc7faf4b95d8d08ba613f90b1581d58fb0448f9
1 #!/usr/bin/perl
4 use strict;
5 no warnings 'uninitialized';
7 use LJ::Constants;
8 use Class::Autouse qw(
9 LJ::Event::JournalNewEntry
10 LJ::Event::UserNewEntry
11 LJ::Entry
12 LJ::Poll
13 LJ::EventLogRecord::NewEntry
14 LJ::EventLogRecord::EditEntry
15 LJ::Config
16 LJ::Comment
17 LJ::RateLimit
18 LJ::EmbedModule
19 LJ::DelayedEntry
20 LJ::PushNotification
21 LJ::Tidy
22 LJ::PersistentQueue
23 LJ::PersonalStats::Ratings::Posts
24 LJ::PersonalStats::Ratings::Journals
25 LJ::API::RateLimiter
26 LJ::Pay::Repost::Offer
27 LJ::MemCacheProxy
28 LJ::Entry::Repost
29 LJ::Tags
32 use LJ::TimeUtil;
33 use POSIX;
35 LJ::Config->load;
37 use lib "$ENV{LJHOME}/cgi-bin";
39 # have to do this else mailgate will croak with email posting, but only want
40 # to do it if the site has enabled the hack
41 require "talklib.pl" if $LJ::NEW_ENTRY_CLEANUP_HACK;
43 # when posting or editing ping hubbub
44 require "ljfeed.pl";
46 #### New interface (meta handler) ... other handlers should call into this.
47 package LJ::Protocol;
49 # global declaration of this text since we use it in two places
50 our $CannotBeShown = '(cannot be shown)';
52 # error classes
53 use constant E_TEMP => 0;
54 use constant E_PERM => 1;
55 # maximum items for get_friends_page function
56 use constant FRIEND_ITEMS_LIMIT => 50;
58 my %e = (
59 # User Errors
60 "100" => E_PERM,
61 "101" => E_PERM,
62 "102" => E_PERM,
63 "103" => E_PERM,
64 "104" => E_TEMP,
65 "105" => E_PERM,
66 "150" => E_PERM,
67 "151" => E_TEMP,
68 "152" => E_PERM,
69 "153" => E_PERM,
70 "154" => E_PERM,
71 "155" => E_TEMP,
72 "156" => E_TEMP,
73 "157" => E_TEMP,
74 "158" => E_TEMP,
75 "159" => E_PERM,
76 "160" => E_TEMP,
77 "161" => E_PERM,
79 # Client Errors
80 "200" => E_PERM,
81 "201" => E_PERM,
82 "202" => E_PERM,
83 "203" => E_PERM,
84 "204" => E_PERM,
85 "205" => E_PERM,
86 "206" => E_PERM,
87 "207" => E_PERM,
88 "208" => E_PERM,
89 "209" => E_PERM,
90 "210" => E_PERM,
91 "211" => E_PERM,
92 "212" => E_PERM,
93 "213" => E_PERM,
94 "214" => E_PERM,
95 "215" => E_PERM,
96 "216" => E_PERM,
97 "217" => E_PERM,
98 "218" => E_PERM,
99 "219" => E_PERM,
100 "220" => E_PERM,
101 "221" => E_PERM,
102 "222" => E_PERM,
103 "223" => E_TEMP,
104 "224" => E_TEMP,
105 "225" => E_TEMP,
106 "226" => E_TEMP,
107 "227" => E_TEMP,
108 "228" => E_TEMP,
109 "229" => E_TEMP,
111 # Access Errors
112 "300" => E_TEMP,
113 "301" => E_TEMP,
114 "302" => E_TEMP,
115 "303" => E_TEMP,
116 "304" => E_TEMP,
117 "305" => E_TEMP,
118 "306" => E_TEMP,
119 "307" => E_PERM,
120 "308" => E_TEMP,
121 "309" => E_PERM,
122 "310" => E_TEMP,
123 "311" => E_TEMP,
124 "312" => E_TEMP,
125 "313" => E_TEMP,
126 "314" => E_PERM,
127 "315" => E_PERM,
128 "316" => E_TEMP,
129 "317" => E_TEMP,
130 "318" => E_TEMP,
131 "319" => E_TEMP,
132 "320" => E_TEMP,
133 "321" => E_TEMP,
134 "322" => E_PERM,
135 "323" => E_PERM,
136 "324" => E_PERM,
137 "325" => E_PERM,
138 "326" => E_PERM,
139 "327" => E_PERM,
140 "328" => E_PERM,
141 "329" => E_PERM,
142 "330" => E_PERM,
143 "331" => E_TEMP,
144 "332" => E_TEMP,
145 "333" => E_PERM,
146 "334" => E_PERM,
147 "335" => E_PERM,
148 "336" => E_TEMP,
149 "337" => E_TEMP,
151 # Limit errors
152 "402" => E_TEMP,
153 "404" => E_TEMP,
154 "405" => E_TEMP,
155 "406" => E_TEMP,
156 "407" => E_TEMP,
157 "408" => E_TEMP,
158 "409" => E_PERM,
159 "410" => E_PERM,
160 "411" => E_TEMP,
161 "412" => E_TEMP,
162 "413" => E_TEMP,
164 # Server Errors
165 "500" => E_TEMP,
166 "501" => E_TEMP,
167 "502" => E_TEMP,
168 "503" => E_TEMP,
169 "504" => E_PERM,
170 "505" => E_TEMP,
171 "506" => E_TEMP,
172 "507" => E_PERM,
173 "508" => E_PERM,
176 my %HANDLERS = (
177 login => \&login,
178 getfriendgroups => \&getfriendgroups,
179 getfriends => \&getfriends,
180 friendof => \&friendof,
181 checkfriends => \&checkfriends,
182 getdaycounts => \&getdaycounts,
183 postevent => \&postevent,
184 editevent => \&editevent,
185 syncitems => \&syncitems,
186 getevents => \&getevents,
187 createrepost => \&createrepost,
188 deleterepost => \&deleterepost,
189 getrepoststatus => \&getrepoststatus,
190 editfriends => \&editfriends,
191 editfriendgroups => \&editfriendgroups,
192 consolecommand => \&consolecommand,
193 getchallenge => \&getchallenge,
194 sessiongenerate => \&sessiongenerate,
195 sessionexpire => \&sessionexpire,
196 getusertags => \&getusertags,
197 getfriendspage => \&getfriendspage,
198 getinbox => \&getinbox,
199 sendmessage => \&sendmessage,
200 setmessageread => \&setmessageread,
201 addcomment => \&addcomment,
202 checksession => \&checksession,
204 getrecentcomments => \&getrecentcomments,
205 getcomments => \&getcomments,
206 deletecomments => \&deletecomments,
207 updatecomments => \&updatecomments,
208 editcomment => \&editcomment,
210 getuserpics => \&getuserpics,
211 createpoll => \&createpoll,
212 getpoll => \&getpoll,
213 editpoll => \&editpoll,
214 votepoll => \&votepoll,
215 registerpush => \&registerpush,
216 unregisterpush => \&unregisterpush,
217 pushsubscriptions => \&pushsubscriptions,
218 resetpushcounter => \&resetpushcounter,
219 getpushlist => \&getpushlist,
221 !$LJ::DISABLED{'xmlrpc_ratings'} ? (geteventsrating => \&geteventsrating) : (),
222 !$LJ::DISABLED{'xmlrpc_ratings'} ? (getusersrating => \&getusersrating) : (),
225 sub translate
227 my ($u, $msg, $vars) = @_;
229 LJ::load_user_props($u, "browselang") unless $u->{'browselang'};
230 return LJ::Lang::get_text($u->{'browselang'}, "protocol.$msg", undef, $vars);
233 sub error_class
235 my $code = shift;
236 $code = $1 if $code =~ /^(\d\d\d):(.+)/;
237 return $e{$code};
240 sub error_is_transient
242 my $class = error_class($_[0]);
243 return defined $class ? ! $class+0 : undef;
246 sub error_is_permanent
248 return error_class($_[0]);
251 sub error_message
253 my $code = shift;
254 my $des;
255 ($code, $des) = ($1, $2) if $code =~ /^(\d\d\d):(.+)/;
257 my $prefix = "";
258 my $error = LJ::Lang::ml("xmlrpc.error.$code") || LJ::Lang::get_text(undef, "xmlrpc.error.$code") || "BUG: Unknown error code ($code)!";
259 $prefix = LJ::Lang::ml('xmlrpc.client_error') if $code >= 200;
260 $prefix = LJ::Lang::ml('xmlrpc.server_error') if $code >= 500;
261 my $totalerror = "$prefix$error";
262 $totalerror .= ": $des" if $des;
263 return $totalerror;
266 sub do_request
268 # get the request and response hash refs
269 my ($method, $req, $err, $flags) = @_;
271 if (ref $req eq "HASH") {
273 # if version isn't specified explicitly, it's version 0
274 $req->{'ver'} ||= $req->{'version'};
275 $req->{'ver'} = 0 unless defined $req->{'ver'};
277 # check specified language
278 if ($req->{'lang'} && not grep /^$req->{'lang'}$/, (@LJ::LANGS, 'en')) {
279 return fail($err, 221, $req->{'lang'} );
283 # set specified or default language
284 my $current_lang = LJ::Lang::current_language();
285 my $lang = $req->{'lang'} || $current_lang || $LJ::DEFAULT_LANG;
286 $lang = 'en_LJ' if $lang eq 'en';
287 LJ::Lang::current_language($lang) unless $lang eq $current_lang;
289 $flags ||= {};
290 my @args = ($req, $err, $flags);
292 LJ::Request->notes("codepath" => "protocol.$method")
293 if LJ::Request->is_inited && ! LJ::Request->notes("codepath");
295 my $method_ref = $HANDLERS{$method};
297 if ($method_ref)
299 my $result = $method_ref->(@args);
301 if ($result && exists $result->{xc3})
303 my $xc3 = delete $result->{xc3};
305 if ($req->{props}->{interface} eq 'xml-rpc')
307 my $ua = eval { LJ::Request->header_in("User-Agent") };
308 Encode::from_to($ua, 'utf8', 'utf8') if $ua;
310 my ($ip_class, $country) = LJ::GeoLocation->ip_class();
312 my $args = {
313 function => $method || ''
316 if ($xc3->{u})
318 my $u = $xc3->{u};
319 $args->{userid} = $u->userid;
320 $args->{usercaps} = $u->caps;
323 $args->{useragent} = $ua if $ua;
324 $args->{country} = $country if $country;
325 $args->{post} = $xc3->{post} if $xc3->{post};
326 $args->{comment} = $xc3->{comment} if $xc3->{comment};
328 LJ::run_hooks("remote_procedure_call", $args);
332 return $result;
335 LJ::Request->notes("codepath" => "") if LJ::Request->is_inited;
337 return fail($err, 201);
340 sub getpoll
342 my ($req, $err, $flags) = @_;
343 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getpoll');
344 my $u = $flags->{'u'};
346 # check arguments
347 my $mode = $req->{mode} || 'all';
348 return fail($err, 203, 'mode') unless($mode =~ /enter|results|answers|all/);
350 my $pollid = $req->{pollid} + 0;
351 return fail($err, 200, 'pollid') unless($pollid);
353 # load poll object
354 my $poll = LJ::Poll->new($pollid);
355 return fail($err, 203, 'pollid') unless($poll && $poll->valid);
357 # question id
358 my $pollqid = $req->{'pollqid'} + 0;
360 my $res = {
361 xc3 => {
362 u => $u,
364 pollid => $pollid,
365 ditemid => $poll->ditemid,
366 name => $poll->name,
367 whovote => $poll->whovote,
368 whoview => $poll->whoview,
369 posterid => $poll->posterid,
370 journalid => $poll->journalid,
371 journal => $poll->journal->username,
372 poster => $poll->poster->username,
373 status => ($poll->is_closed ? 'close' : 'open'),
374 can_vote => $poll->can_vote($u),
375 can_view => $poll->can_view($u),
376 is_owner => $poll->is_owner($u),
377 mode => $mode,
380 my $time = $poll->get_time_user_submitted($u);
381 $res->{submitted_time} = $time if ($time);
382 $res->{pollqid} = $pollqid if($pollqid);
384 # Get all questions
385 my @questions = $poll->questions;
387 @questions = grep { $_->pollqid eq $pollqid } @questions if ($pollqid);
388 return fail($err, 203, 'pollqid') unless(@questions);
390 if ($req->{'asxml'}) {
391 my $tidy = LJ::Tidy->new();
393 foreach my $question (@questions) {
394 if ($question->{text}) {
395 $question->{text} = $tidy->clean( $question->{text} );
398 $res->{'name'} = $tidy->clean( $res->{'name'} );
402 # mode to show poll questions
403 if($mode =~ /enter|all/) {
404 # render_enter
405 @{$res->{questions}} = map { $_->get_hash } @questions;
408 if($mode =~ /results|all/) {
409 $poll->load_aggregated_results();
410 $res->{results} = $poll->{results};
413 if($mode =~ /answers|all/ && $poll->can_view($u)) {
414 foreach my $question (@questions) {
415 my @answers = map { my $user = LJ::load_userid($_->{userid});
416 if ($user) {
417 $_->{'username'} = $user->username;
418 if ($user->identity) {
419 my $i = $user->identity;
420 $_->{'identity_type'} = $i->pretty_type;
421 $_->{'identity_value'} = $i->value;
422 $_->{'identity_url'} = $i->url($user);
423 $_->{'identity_display'} = $user->display_name;
427 } map { delete $_->{pollqid}; $_ } $question->answers;
429 @{$res->{answers}{$question->pollqid}} = @answers;
434 return $res;
437 sub editpoll
439 my ($req, $err, $flags) = @_;
440 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'editpoll');
441 my $u = $flags->{'u'};
443 # check arguments
444 my $pollid = $req->{pollid} + 0;
445 return fail($err, 200, 'pollid') unless($pollid);
447 # load poll object
448 my $poll = LJ::Poll->new($pollid);
449 return fail($err, 203, 'pollid') unless($poll && $poll->valid);
451 my $is_super = $poll->prop('supermaintainer');
453 return fail($err, 103, 'xmlrpc.des.maintainer_poll') if($is_super);
455 my $status = $req->{status};
456 return fail($err, 200, 'status') unless($status);
457 return fail($err, 203, 'status') unless($status =~ /open|close/);
459 return fail($err, 103, 'xmlrpc.des.not_poll_owner') unless($poll->is_owner($u));
461 if($status eq 'open') {
462 $poll->open_poll();
463 } elsif ($status eq 'close') {
464 $poll->close_poll();
467 return {
468 pollid => $pollid,
469 status => ($poll->{status} eq 'X' ? 'close' : 'open') ,
470 xc3 => {
471 u => $u,
476 sub votepoll
478 my ($req, $err, $flags) = @_;
479 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'votepoll');
480 my $u = $flags->{'u'}; # remote_id
482 # check pollid
483 my $pollid = $req->{pollid} + 0;
484 return fail($err, 200, 'pollid') unless($pollid);
486 # load poll object
487 my $poll = LJ::Poll->new($pollid);
488 return fail($err, 203, 'pollid') unless($poll && $poll->valid);
490 # check answers parameter
491 my $answers = $req->{answers};
492 return fail($err, 200, 'answers') unless($answers);
493 return fail($err, 203, 'answers') unless(ref $answers eq 'HASH');
495 my @warnings;
496 my $errors;
498 unless (LJ::Poll->process_vote($u, $pollid, $answers, \$errors, \@warnings, wrong_value_as_error => 1)) {
499 return fail($err, 103, $errors);
502 return {
503 pollid => $pollid,
504 journalid => $poll->journalid,
505 posterid => $poll->posterid,
506 journal => $poll->journal->username,
507 poster => $poll->poster->username,
508 xc3 => {
509 u => $u,
514 sub checksession {
515 my ($req, $err, $flags) = @_;
516 return undef
517 unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'checksession');
519 my $u = $flags->{'u'};
521 my $session = $u->session;
523 return {
524 username => $u->username,
525 session => $u->id.":".$session->id.":".$session->auth,
526 caps => $u->caps,
527 usejournals => list_usejournals($u),
528 xc3 => {
529 u => $u
534 sub addcomment
536 my ($req, $err, $flags) = @_;
538 $flags->{allow_anonymous} = 1;
539 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'addcomment');
540 my $u = $flags->{'u'};
542 return fail($err,200,"body") unless($req->{body});
543 return fail($err,200,"ditemid") unless($req->{ditemid});
544 return fail($err,200,"journal/journalid") unless($u || $req->{journal} || $req->{journalid});
546 my $journal;
547 if ($req->{journal}) {
548 return fail($err,100) unless LJ::canonical_username($req->{journal});
549 $journal = LJ::load_user($req->{journal}) or return fail($err, 100);
550 return fail($err,226)
551 if LJ::Talk::Post::require_captcha_test($u, $journal, $req->{body}, $req->{ditemid});
552 } elsif ( $req->{journalid} ) {
553 $journal = LJ::load_userid($req->{journalid}) or return fail($err, 100);
554 return fail($err,226)
555 if LJ::Talk::Post::require_captcha_test($u, $journal, $req->{body}, $req->{ditemid});
556 } else {
557 $journal = $u;
560 # some additional checks
561 return fail($err,214) if LJ::Comment->is_text_spam( \ $req->{body} );
563 my $pk = $req->{prop_picture_keyword} || $req->{picture_keyword};
565 # create
566 my $comment;
567 eval {
568 $comment = LJ::Comment->create(
569 journal => $journal,
570 ditemid => $req->{ditemid},
571 parenttalkid => ($req->{parenttalkid} || int($req->{parent} / 256)),
573 poster => $u, # TODO: to allow poster to be undef
575 body => $req->{body},
576 subject => $req->{subject},
578 props => { picture_keyword => $pk }
582 return fail($err,337) unless $comment;
584 ## Counter "new_comment" for monitoring
585 LJ::run_hook ("update_counter", {
586 counter => "new_comment",
589 # OK
590 return {
591 status => "OK",
592 commentlink => $comment->url,
593 dtalkid => $comment->dtalkid,
594 xc3 => {
595 u => $u,
596 comment => {
597 toplevel => ($comment->parenttalkid == 0 ? 1 : 0),
603 sub getcomments {
604 my ($req, $err, $flags) = @_;
606 $flags->{allow_anonymous} = 1;
608 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getcomments');
609 my $u = $flags->{'u'};
611 my $journal;
612 if($req->{journal}) {
613 return fail($err,100) unless LJ::canonical_username($req->{journal});
614 $journal = LJ::load_user($req->{journal}) or return fail($err, 100);
615 } elsif ( $req->{journalid} ) {
616 $journal = LJ::load_userid($req->{journalid}) or return fail($err, 100);
617 } else {
618 $journal = $u;
621 return fail($err,200,"journal") unless($journal);
622 return fail($err,200,'xmlrpc.des.or', {'first'=>'ditemid', 'second'=>'itemid'}) unless($req->{ditemid} || $req->{itemid});
624 my $itemid = int($req->{ditemid} / 256);
625 $itemid ||= $req->{itemid} + 0;
627 # load root post
628 my $jitem = LJ::Talk::get_journal_item($journal, $itemid);
629 return fail($err,203,'xmlrpc.des.no_post_by_param', {'param'=>'ditemid'}) unless($jitem);
630 my $up = LJ::load_userid( $jitem->{'posterid'} );
632 # check permission to access root post
633 return fail($err,300) unless( LJ::can_view($u, $jitem));
635 my $talkid = int(($req->{dtalkid} + 0)/256); # talkid to load thread
637 my $page_size = $req->{page_size} + 0;
638 $page_size = 500 if($page_size <= 0 || $page_size > 500);
640 my $page = $req->{page} + 0; # page to show - defaut
641 my $view = $req->{view_ditemid} + 0; # ditemid - external comment id to show that page with it
643 my $skip = $req->{skip} + 0;
644 my $itemshow = $req->{itemshow} + 0;
646 my $expand = $req->{expand_strategy} ? $req->{expand_strategy} : 'default' ;
647 return fail($err, 203, 'expand_strategy') unless ($expand =~ /^mobile|mobile_thread|expand_all|by_level|detailed|default$/);
649 my $format = $req->{format} || 'thread'; # default value thread
650 return fail($err, 203, 'format') unless($format =~ /^thread|list$/ );
652 my $expand_all = 0;
653 if( $expand eq 'mobile_thread' || $expand eq 'expand_all'){
654 undef $expand;
655 $expand_all = 1;
658 my $expand_child;
659 my $expand_level;
660 if ($expand eq 'mobile') {
661 $expand_child = $req->{expand_child} + 0;
662 $expand_child = 3 if $expand_child > 500 || $expand_child <= 0
663 } elsif ($expand eq 'by_level') {
664 $expand_level = ($req->{expand_level} ? $req->{expand_level} + 0 : 1);
665 $expand_level = 1 if $expand_level > 128 || $expand_level < 0;
668 my $opts = {
669 page => $page, # page to get
670 view => $view,
671 expand_level => $expand_level+1,
672 expand_child => $expand_child,
673 expand_all => $expand_all,
674 init_comobj => 0, # do not init LJ::Comment objects in the function
675 up => $up, # author of root post
676 page_size => $page_size, # max comments returned per call!
677 strict_page_size => 1, # fix page size, do not extent it in case of less comments
680 # optional parameters
681 $opts->{thread} = $talkid if $talkid;
682 $opts->{expand_strategy} = $expand unless($expand eq 'default');
684 my @com = LJ::Talk::load_comments($journal, $u, "L", $itemid, $opts);
686 my %extra;
687 $extra{topitems} = $opts->{out_items};
688 $extra{topitem_first} = $opts->{out_itemfirst};
689 $extra{topitem_last} = $opts->{out_itemlast};
690 $extra{page_size} = $opts->{out_pagesize};
691 $extra{pages} = $opts->{out_pages};
692 $extra{page} = $opts->{out_page};
693 $extra{ditemid} = $jitem->{ditemid};
695 my @comments;
696 my @parent = ( \{ level => -1, children => \@comments } );
698 while (my $item = shift @com){
699 $item->{indent} ||= 0;
700 shift( @parent ) while $item->{indent} <= ${$parent[0]}->{level};
702 my $item_data = {
703 parentdtalkid => $item->{parenttalkid}?($item->{parenttalkid} * 256 + $jitem->{anum}):0,
704 postername => $item->{userpost},
705 level => $item->{indent},
706 posterid => $item->{posterid},
707 datepostunix => $item->{datepost_unix},
708 datepost => $item->{datepost},
709 dtalkid => $item->{talkid} * 256 + $jitem->{anum},
710 state => $item->{state},
711 is_show => $item->{_show},
712 is_loaded => ($item->{_loaded} ? 1 : 0),
715 unless($u || $item->{_show}) {
716 delete $item_data->{postername};
717 delete $item_data->{posterid};
718 delete $item_data->{datepost_unix};
719 delete $item_data->{datepost};
722 $item_data->{body} = $item->{body} if($item->{body} && $item->{_loaded});
723 if ($req->{'asxml'}) {
724 my $tidy = LJ::Tidy->new();
725 $item_data->{body} = $tidy->clean( $item_data->{body} );
728 # add parameters to lj-embed
729 #LJ::EmbedModule->expand_entry($item->{upost}, \$item_data->{body}, get_video_id => 1) if($item->{upost} && $req->{get_video_ids});
731 $item_data->{subject} = $item->{subject} if($item->{subject} && $item->{_loaded});
733 if($item->{upost} && $item->{upost}->identity ){
734 my $i = $item->{upost}->identity;
735 $item_data->{'identity_type'} = $i->pretty_type;
736 $item_data->{'identity_value'} = $i->value;
737 $item_data->{'identity_url'} = $i->url($item->{upost});
738 $item_data->{'identity_display'} = $item->{upost}->display_name;
741 if ($item->{'_loaded'} && $req->{extra}) {
742 my $comment = LJ::Comment->new($journal, dtalkid => $item_data->{dtalkid});
744 my $userpic = $comment->userpic;
745 $item_data->{userpic} = $userpic && $userpic->url; # left here forawhile
746 $item_data->{poster_userpic_url} = $item_data->{userpic};
748 $item_data->{props} = { map {$item->{props}->{$_} ? ($_ => $item->{props}->{$_}) : ()}
749 qw(edit_time deleted_poster picture_keyword opt_preformatted) };
751 $item_data->{props}->{'poster_ip'} = $item->{'props'}->{'poster_ip'}
752 if $item->{'props'}->{'poster_ip'} && $u && ( $u->{'user'} eq $up->{'user'} || $u->can_manage($journal) );
754 $item_data->{privileges} = {};
755 $item_data->{privileges}->{'delete'} = LJ::Talk::can_delete($u, $journal, $up, $item->{userpost});
756 $item_data->{privileges}->{'edit'} = $comment->user_can_edit($u);
757 $item_data->{privileges}->{'freeze'} = (!$comment->is_frozen && LJ::Talk::can_freeze($u, $journal, $up, $item->{userpost}));
758 $item_data->{privileges}->{'unfreeze'} = ($comment->is_frozen && LJ::Talk::can_unfreeze($u, $journal, $up, $item->{userpost}));
759 my $pu = $comment->poster;
760 unless ($pu && $pu->is_suspended){
761 $item_data->{privileges}->{'screen'} = (!$comment->is_screened && LJ::Talk::can_screen($u, $journal, $up, $item->{userpost}));
762 $item_data->{privileges}->{'unscreen'} = ($comment->is_screened && LJ::Talk::can_unscreen($u, $journal, $up, $item->{userpost}));
764 $item_data->{privileges}->{'spam'} = (!$comment->is_spam && LJ::Talk::can_marked_as_spam($u, $journal, $up, $item->{userpost}));
765 $item_data->{privileges}->{'unspam'} = ($comment->is_spam && LJ::Talk::can_unmark_spam($u, $journal, $up, $item->{userpost}));
766 $item_data->{privileges}->{'reply'} = (LJ::Talk::Post::require_captcha_test($u, $journal, '', $req->{ditemid}) ? 0 : 1);
769 if ( $req->{calculate_count} ){
770 $item_data->{thread_count} = 0;
771 $$_->{thread_count}++ for @parent;
774 if( $req->{only_loaded} && !$item->{_loaded} ){
775 my $hc = \${$parent[0]}->{has_children};
776 $$hc = $$hc?$$hc+1:1;
777 next unless $req->{calculate_count};
778 }elsif( $format eq 'list' ){ # list or thread
779 push @comments, $item_data;
780 }else{
781 ${$parent[0]}->{children} = [] unless ${$parent[0]}->{children};
782 push @{${$parent[0]}->{children}}, $item_data;
785 my $children = $item->{children};
786 if($children && @$children){
787 $_->{indent} = $item->{indent} + 1 for @$children;
788 unshift @com, @$children;
789 unshift @parent, \$item_data;
790 undef $item->{children};
794 if($format eq 'list') {
795 $extra{items} = scalar(@comments);
796 $itemshow = $extra{items} unless ($itemshow && $itemshow <= $extra{items});
797 @comments = splice(@comments, $skip, $itemshow);
798 $extra{skip} = $skip;
799 $extra{itemshow} = $itemshow;
802 return {
803 comments => \@comments,
804 %extra,
805 xc3 => {
806 u => $flags->{'u'}
811 =head deletecomments
812 Delete specified comment, comments or thread(s) of comments in specified journal that current use
813 Parameters:
814 journal/journalid or current user's journal
815 dtalkid/dtalkids - ids of current
816 thread - bool
818 =cut
819 sub deletecomments {
820 my ($req, $err, $flags) = @_;
821 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'deletecomments');
823 my $u = $flags->{'u'};
824 my $journal;
825 if($req->{journal}) {
826 return fail($err,100) unless LJ::canonical_username($req->{journal});
827 $journal = LJ::load_user($req->{journal}) or return fail($err, 100);
828 } elsif($req->{journalid}) {
829 $journal = LJ::load_userid($req->{journalid}) or return fail($err, 100);
830 } else {
831 $journal = $u;
834 return fail($err, 200, 'xmlrpc.des.or', {'first'=>'dtalkid','second'=>'dtalkids'}) unless($req->{dtalkid} || $req->{dtalkids});
835 my @ids;
836 if ($req->{dtalkids}) {
837 foreach my $num (split(/\s*,\s*/, $req->{'dtalkids'})) {
838 return fail($err, 203, 'xmlrpc.des.non_arifmetic', {'param'=>'dtalkid','value'=>$num}) unless $num =~ /^\d+$/;
839 push @ids, $num;
841 } else {
842 my $num = $req->{dtalkid};
843 return fail($err, 203, 'xmlrpc.des.non_arifmetic', {'param'=>'dtalkid','value'=>$num}) unless $num =~ /^\d+$/;
844 push @ids, $num;
847 my $can_manage = $u->can_manage($journal);
849 my (%to_delauthor, %to_ban, %to_mark_spam);
851 my @comments;
852 foreach my $id (@ids) {
853 my $comm = LJ::Comment->new($journal, dtalkid => $id);
854 return fail($err, 203, 'xmlrpc.des.no_comment_by_param',{'param'=>'dtalkid'}) unless $comm && ($comm->dtalkid == $id);
855 return fail($err, 327, 'dtalkid:'.$comm->dtalkid) if $comm->is_deleted;
856 return fail($err, 326, 'dtalkid:'.$comm->dtalkid) unless $comm->user_can_delete($u);
858 if($req->{'delauthor'}) {
860 # they can delete all comments posted by the same author
861 # if they are the entry author, and the comment being deleted
862 # has not been posted anonymously
863 my $can_delauthor = $comm->poster && ( $can_manage || ( $u->userid == $comm->entry->poster->userid ) );
864 return fail($err, 328, 'dtalkid:'.$comm->dtalkid) unless $can_delauthor;
865 $to_delauthor{$comm->entry->jitemid}->{$comm->poster->userid} = 1;
868 if($req->{'ban'}) {
870 # they can ban the comment author if they are the journal owner
871 # and there is an author; also, they will not be able to ban
872 # themselves
873 my $can_sweep = ( $u && $comm->poster && $u->can_sweep($journal) );
874 my $can_ban = ( $can_manage || $can_sweep ) && $comm->poster && ( $u->userid != $comm->poster->userid );
875 return fail($err, 329, 'dtalkid:'.$comm->dtalkid) unless $can_ban;
876 $to_ban{$comm->poster->userid} = $comm->poster;
879 if($req->{'spam'}) {
881 # they can mark as spam unless the comment is their own;
882 # they don't need to be the community maintainer to do that
883 my $can_mark_spam = LJ::Talk::can_mark_spam($u, $journal, $comm->poster, $comm)
884 && $comm->poster && ( $u->userid != $comm->poster->userid );
885 return fail($err, 330, 'dtalkid:'.$comm->dtalkid) unless $can_mark_spam;
886 $to_mark_spam{$comm->jtalkid} = $comm;
889 push @comments, $comm;
892 my @to_delete;
893 if(!$req->{thread}){
894 push @to_delete, @comments;
895 } else {
896 my %map_delete;
897 foreach my $comment (@comments) {
898 my @comment_tree = $comment->entry->comment_list;
899 my @children = ($comment);
900 while(my $item = shift @children){
901 return fail($err, 326, 'xmlrpc.des.foreign_comment', {'dtalkid'=>$item->dtalkid}) unless $item->user_can_delete($u);
902 $map_delete{$item->dtalkid} = $item unless $item->is_deleted;
903 push @children, grep { $_->{parenttalkid} == $item->{jtalkid} } @comment_tree;
906 push @to_delete, values %map_delete;
909 # delete all comments
910 $_->delete for @to_delete;
912 # delete author comments (only for authors of root comment in thread)
913 foreach my $jitemid (keys %to_delauthor) {
914 foreach my $userid (keys %{$to_delauthor{$jitemid}}) {
915 LJ::Talk::delete_author( $journal, $jitemid, $userid );
919 # ban authors (only for authors of root comment in thread)
920 $journal->ban_user($_) for values %to_ban;
922 # mark comments as spam (only for root comment in thread)
923 foreach my $comment (values %to_mark_spam) {
924 my $poster = $comment->poster;
925 LJ::Talk::mark_comment_as_spam( $journal, $comment->jtalkid );
926 LJ::set_rel($journal, $poster, 'D');
928 LJ::User::UserlogRecord::SpamSet->create( $journal,
929 'spammerid' => $poster->userid, 'remote' => $u );
931 LJ::run_hook('auto_suspender_for_spam', $poster->{userid});
934 return {
935 status => 'OK',
936 result => @to_delete + 0,
937 dtalkids => [ map {$_->dtalkid} @to_delete ],
938 xc3 => {
939 u => $u,
944 =head updatecomments
945 Use that function to update comments statuses:
946 single or multiple
947 complete thread or root ony
948 =cut
950 sub updatecomments {
951 my ($req, $err, $flags) = @_;
952 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'updatecomments');
954 my $u = $flags->{'u'};
955 my $journal;
956 if($req->{journal}) {
957 return fail($err,100) unless LJ::canonical_username($req->{journal});
958 $journal = LJ::load_user($req->{journal}) or return fail($err, 100);
959 } elsif($req->{journalid}) {
960 $journal = LJ::load_userid($req->{journalid}) or return fail($err, 100);
961 } else {
962 $journal = $u;
965 return fail($err, 200, 'xmlrpc.des.or',{'first'=>'dtalkid','second'=>'dtalkids'}) unless($req->{dtalkid} || $req->{dtalkids});
967 my @ids;
968 if ($req->{dtalkids}) {
969 foreach my $num (split(/\s*,\s*/, $req->{'dtalkids'})) {
970 return fail($err, 203, 'xmlrpc.des.non_arifmetic', {'param'=>'dtalkid', 'value'=>$num}) unless $num =~ /^\d+$/;
971 push @ids, $num;
973 } else {
974 my $num = $req->{dtalkid};
975 return fail($err, 203, 'xmlrpc.des.non_arifmetic', {'param'=>'dtalkid', 'value'=>$num}) unless $num =~ /^\d+$/;
976 push @ids, $num;
979 my $action = $req->{action};
980 return fail($err, 200, "action") unless($action);
981 return fail($err, 203, "action") unless($action =~ /^screen|unscreen|freeze|unfreeze|unspam$/);
983 my $can_method = ($action =~ /unspam/ ? "LJ::Talk::can_unmark_spam" : "LJ::Talk::can_$action");
984 $can_method = \&{$can_method};
986 my @comments;
987 foreach my $id (@ids) {
988 my $comm = LJ::Comment->new($journal, dtalkid => $id);
989 return fail($err, 203, 'xmlrpc.des.no_comment_by_param',{'param'=>'dtalkid'}) unless $comm && ($comm->dtalkid == $id);
990 return fail($err, 327, 'dtalkid:'.$comm->dtalkid) if $comm->is_deleted;
991 return fail($err, 326, 'dtalkid:'.$comm->dtalkid) unless $can_method->($u, $journal, $comm->entry->poster, $comm->poster);
992 push @comments, $comm;
995 # get first entry
996 my $jitemid = $comments[0]->entry->jitemid;
998 # get list of comments to process
999 my @to_update;
1000 if(!$req->{thread} || $action =~ /freeze|unfreeze/) {
1001 push @to_update, @comments;
1002 } else { # get all elements from threads
1003 my %map_update;
1004 foreach my $comment (@comments) {
1005 my @comment_tree = $comment->entry->comment_list;
1006 my @children = ($comment);
1007 while(my $item = shift @children){
1008 next if $item->is_deleted;
1009 return fail($err, 326, 'dtalkid:'.$item->dtalkid) unless $can_method->($u, $journal, $item->entry->poster, $item->poster);
1010 $map_update{$item->dtalkid} = $item;
1011 push @children, grep { $_->{parenttalkid} == $item->{jtalkid} } @comment_tree;
1014 push @to_update, values %map_update;
1017 # process comments
1018 my $method;
1019 if ($action =~ /screen|unscreen|unspam/) {
1020 $method = \&{"LJ::Talk::$action".'_comment'};
1021 $method->($journal, $jitemid, map { $_->{jtalkid} } @to_update);
1022 } elsif ($action =~ /freeze|unfreeze/) {
1023 $method = \&{"LJ::Talk::$action".'_thread'};
1024 $method->($journal, $jitemid, map { $_->{jtalkid} } @to_update);
1027 return {
1028 status => 'OK',
1029 result => @to_update + 0,
1030 dtalkids => [ map {$_->dtalkid} @to_update ],
1031 xc3 => {
1032 u => $u,
1037 sub screencomments {
1038 my ($req, $err, $flags) = @_;
1039 return undef unless authenticate($req, $err, $flags);
1041 my $journal = $req->{journalid}?LJ::load_userid($req->{journalid}):$flags->{'u'};
1042 my $comment = LJ::Comment->new( $journal , dtalkid => $req->{dtalkid} );
1043 my $up = $comment->entry->poster;
1044 return fail($err, 300) unless LJ::Talk::can_screen($flags->{'u'}, $journal, $up, $comment->poster);
1046 my @to_screen;
1048 if( !$req->{recursive}){
1049 push @to_screen, $comment;
1050 }else{
1051 my @comment_tree = $comment->entry->comment_list;
1052 my @children = ($comment);
1053 while(my $item = shift @children){
1054 return fail($err, 300) unless LJ::Talk::can_screen($flags->{'u'}, $journal, $up, $item->poster);
1055 push @to_screen, $item;
1056 push @children, grep { $_->{parenttalkid} == $item->{jtalkid} } @comment_tree;
1059 LJ::Talk::screen_comment($journal, $comment->entry->jitemid, $_->{jtalkid}) for @to_screen;
1061 return {
1062 status => 'OK',
1063 result => @to_screen + 0,
1064 xc3 => {
1065 u => $flags->{'u'}
1070 sub unscreencomments {
1071 my ($req, $err, $flags) = @_;
1072 return undef unless authenticate($req, $err, $flags);
1074 my $journal = $req->{journalid}?LJ::load_userid($req->{journalid}):$flags->{'u'};
1075 my $comment = LJ::Comment->new( $journal , dtalkid => $req->{dtalkid} );
1076 my $up = $comment->entry->poster;
1077 return fail($err, 300) unless LJ::Talk::can_unscreen($flags->{'u'}, $journal, $up, $comment->poster);
1079 my @to_screen;
1081 if( !$req->{recursive}){
1082 push @to_screen, $comment;
1083 }else{
1084 my @comment_tree = $comment->entry->comment_list;
1085 my @children = ($comment);
1086 while(my $item = shift @children){
1087 return fail($err, 300) unless LJ::Talk::can_unscreen($flags->{'u'}, $journal, $up, $item->poster);
1088 push @to_screen, $item;
1089 push @children, grep { $_->{parenttalkid} == $item->{jtalkid} } @comment_tree;
1092 LJ::Talk::unscreen_comment($journal, $comment->entry->jitemid, $_->{jtalkid}) for @to_screen;
1094 return {
1095 status => 'OK',
1096 result => @to_screen + 0,
1097 xc3 => {
1098 u => $flags->{'u'}
1103 sub freezecomments {
1104 my ($req, $err, $flags) = @_;
1105 return undef unless authenticate($req, $err, $flags);
1107 my $journal = $req->{journalid}?LJ::load_userid($req->{journalid}):$flags->{'u'};
1108 my $comment = LJ::Comment->new( $journal , dtalkid => $req->{dtalkid} );
1109 my $up = $comment->entry->poster;
1110 return fail($err, 300) unless LJ::Talk::can_freeze($flags->{'u'}, $journal, $up, $comment->poster);
1112 LJ::Talk::freeze_thread($journal, $comment->entry->jitemid, $comment->{jtalkid});
1114 return {
1115 status => 'OK',
1116 result => 1,
1117 xc3 => {
1118 u => $flags->{'u'}
1123 sub unfreezecomments {
1124 my ($req, $err, $flags) = @_;
1125 return undef unless authenticate($req, $err, $flags);
1127 my $journal = $req->{journalid}?LJ::load_userid($req->{journalid}):$flags->{'u'};
1128 my $comment = LJ::Comment->new( $journal , dtalkid => $req->{dtalkid} );
1129 my $up = $comment->entry->poster;
1130 return fail($err, 300) unless LJ::Talk::can_unfreeze($flags->{'u'}, $journal, $up, $comment->poster);
1132 LJ::Talk::unfreeze_thread($journal, $comment->entry->jitemid, $comment->{jtalkid});
1134 return {
1135 status => 'OK',
1136 result => 1,
1137 xc3 => {
1138 u => $flags->{'u'}
1143 =head editcomment
1144 Edit one single comment, just content.
1145 To change statuses use other API functions.
1146 =cut
1147 sub editcomment {
1148 my ($req, $err, $flags) = @_;
1149 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'editcomment');
1151 my $remote = $flags->{'u'};
1152 return fail($err, 318) if $remote && $remote->is_readonly;
1154 my $journal;
1155 if($req->{journal}) {
1156 return fail($err, 100) unless LJ::canonical_username($req->{journal});
1157 $journal = LJ::load_user($req->{journal}) or return fail($err, 100);
1158 } elsif($req->{journalid}) {
1159 $journal = LJ::load_userid($req->{journalid}) or return fail($err, 100);
1160 } else {
1161 $journal = $remote;
1163 return fail($err, 319) if $journal && $journal->is_readonly;
1165 return fail($err, 200, "dtalkid") unless($req->{dtalkid});
1167 my $comment = LJ::Comment->new($journal, dtalkid => $req->{dtalkid});
1168 return fail($err, 203, 'xmlrpc.des.and', {first=>'dtalkid',second=>'journal(id)'}) unless $comment;
1170 my $entry = $comment->entry;
1171 return fail($err, 203, 'xmlrpc.des.and', {first=>'dtalkid',second=>'journal(id)'}) unless $entry;;
1172 return fail($err, 323) if $entry && $entry->is_suspended;
1174 my $up = $entry->poster;
1175 my $parent = $comment->parent;
1176 return fail($err, 324) if $parent && $parent->{state} eq 'F';
1178 my $new_comment = { map {$_ => $req->{$_}} grep { defined $req->{$_} } qw( picture_keyword preformat subject body) };
1179 $new_comment->{editid} = $req->{dtalkid};
1180 $new_comment->{body} = $comment->body_orig() unless($new_comment->{body}); # body can't be empty!
1181 $new_comment->{subject} = $comment->subject_orig() unless(defined $new_comment->{subject});
1182 $new_comment->{preformat} = $comment->prop('opt_preformatted') unless(defined $new_comment->{preformat});
1184 my $errref;
1185 return fail($err, 325, $errref)
1186 unless LJ::Talk::Post::edit_comment(
1187 $up, $journal, $new_comment, $parent, {itemid => $entry->jitemid}, \$errref, $remote);
1189 return {
1190 status => 'OK',
1191 commentlink => $comment->url,
1192 dtalkid => $comment->dtalkid,
1193 xc3 => {
1194 u => $flags->{'u'}
1199 sub getrecentcomments {
1200 my ($req, $err, $flags) = @_;
1201 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getrecentcomments');
1202 my $u = $flags->{'u'};
1204 my $journal;
1205 if($req->{journal}) {
1206 return fail($err,100) unless LJ::canonical_username($req->{journal});
1207 $journal = LJ::load_user($req->{journal}) or return fail($err, 100);
1208 } elsif ( $req->{journalid} ) {
1209 $journal = LJ::load_userid($req->{journalid}) or return fail($err, 100);
1210 } else {
1211 $journal = $u;
1213 return fail($err,200,"journal") unless($journal);
1215 my $count = $req->{itemshow};
1216 $count = 10 if !$count || ($count > 100) || ($count < 0);
1218 my @recv = $journal->get_recent_talkitems($count, remote => $u);
1219 my @recv_talkids = map { $_->{'jtalkid'} } @recv;
1220 my %recv_userids = map { $_->{'posterid'} => 1} @recv;
1221 my $comment_text = LJ::get_talktext2($journal, @recv_talkids);
1222 my $users = LJ::load_userids(keys(%recv_userids));
1223 foreach my $comment ( @recv ) {
1224 $comment->{subject} = $comment_text->{$comment->{jtalkid}}[0];
1225 $comment->{text} = $comment_text->{$comment->{jtalkid}}[1];
1227 $comment->{text} = LJ::trim_widgets(
1228 length => $req->{trim_widgets},
1229 img_length => $req->{widgets_img_length},
1230 text => $comment->{text},
1231 read_more => '<a href="' . $comment->url . '"> ...</a>',
1232 ) if $req->{trim_widgets};
1234 if ($req->{'asxml'} && $comment->{text}) {
1235 my $tidy = LJ::Tidy->new();
1236 $comment->{text} = $tidy->clean( $comment->{text} );
1239 # add parameters to lj-tags
1240 #LJ::EmbedModule->expand_entry($users->{$comment->{posterid}}, \$comment->{text}, get_video_id => 1) if($req->{get_video_ids});
1242 if ($req->{view}) {
1243 LJ::EmbedModule->expand_entry($users->{$comment->{posterid}}, \$comment->{text}, edit => 1) if $req->{view} eq 'stored';
1244 } elsif ($req->{parseljtags}) {
1245 $comment->{text} = LJ::convert_lj_tags_to_links(
1246 event => $comment->{text},
1247 embed_url => $comment->url );
1250 my $poster = $users->{$comment->{posterid}};
1251 $comment->{postername} = $poster->username if $poster;
1253 if ($poster && $poster->identity ) {
1254 my $i = $poster->identity;
1255 $comment->{'identity_type'} = $i->pretty_type;
1256 $comment->{'identity_value'} = $i->value;
1257 $comment->{'identity_url'} = $i->url($poster);
1258 $comment->{'identity_display'} = $poster->display_name;
1261 my $comm_obj = LJ::Comment->new($journal, jtalkid => $comment->{jtalkid});
1262 my $userpic = $comm_obj->userpic;
1263 $comment->{poster_userpic_url} = $userpic && $userpic->url;
1267 return {
1268 status => 'OK',
1269 comments => [ @recv ],
1270 xc3 => {
1271 u => $u
1276 sub getfriendspage
1278 my ($req, $err, $flags) = @_;
1279 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getfriendspage');
1280 my $u = $flags->{'u'};
1282 my $itemshow = (defined $req->{itemshow}) ? $req->{itemshow} : 100;
1283 return fail($err, 209, 'xmlrpc.des.bad_value', {'param'=>'itemshow'}) if $itemshow ne int($itemshow ) or $itemshow <= 0 or $itemshow > 100;
1284 my $skip = (defined $req->{skip}) ? $req->{skip} : 0;
1285 return fail($err, 209, 'xmlrpc.des.bad_value', {'param'=>'skip'}) if $skip ne int($skip ) or $skip < 0 or $skip > 100;
1287 my $lastsync = int $req->{lastsync};
1288 my $before = int $req->{before};
1289 my $before_count = 0;
1290 my $before_skip = 0;
1291 if ($before){
1292 $before_skip = $skip + 0;
1293 $skip = 0;
1296 my %get_params = (
1297 u => $u,
1298 userid => $u->{'userid'},
1299 remote => $u,
1300 dateformat => 'S2',
1301 itemshow => $itemshow,
1302 filter => $req->{groupmask},
1303 showtypes => $req->{journaltype},
1306 my @entries = LJ::get_friend_items({
1307 %get_params,
1308 'skip' => $skip,
1309 'filter_by_tags' => 1,
1312 my @attrs = qw/subject_raw event_raw journalid posterid ditemid security reply_count userpic props security/;
1314 my @uids;
1316 my @res = ();
1317 while (my $ei = shift @entries) {
1319 next unless $ei;
1321 # exit cycle if maximum friend items limit reached
1322 last
1323 if scalar @res >= FRIEND_ITEMS_LIMIT;
1325 # if passed lastsync argument - skip items with logtime less than lastsync
1326 if($lastsync) {
1327 next
1328 if $LJ::EndOfTime - $ei->{rlogtime} <= $lastsync;
1331 if($before) {
1332 last if @res >= $itemshow;
1333 push @entries, LJ::get_friend_items({
1334 %get_params,
1335 'skip' => $skip + ($before_count += $itemshow),
1336 'filter_by_tags' => 1,
1337 }) unless @entries;
1338 next if $LJ::EndOfTime - $ei->{rlogtime} > $before;
1339 next if $before_skip-- > 0;
1342 my $entry = LJ::Entry->new_from_item_hash($ei);
1343 next unless $entry;
1344 next unless $entry->visible_to($u);
1346 # event result data structure
1347 my %h = ();
1349 my $repost_props = { use_repost_signature => 0 };
1350 my ($original_entry, $repost_entry, $event_raw);
1351 my $opts = {original_post_obj => \$original_entry, repost_obj => \$repost_entry, event => \$event_raw};
1352 if (LJ::Entry::Repost->substitute_content( $entry, $opts, $repost_props )) {
1353 $h{repost} = 1;
1354 $entry = $original_entry;
1357 $entry->normalize_props() unless $flags->{'noauth'};
1359 # Add more data for public posts
1360 foreach my $method (@attrs) {
1361 $h{$method} = $entry->$method;
1364 if($h{repost}){
1365 $h{event_raw} = $event_raw;
1366 $h{original_entry_url} = $original_entry->url;
1367 $h{repostername} = $repost_entry->journal->username;
1368 $h{postername} = $original_entry->poster->username;
1369 $h{journalname} = $entry->journal->username;
1370 my $userpic = $original_entry->userpic;
1371 $h{poster_userpic_url} = $userpic && $userpic->url;
1375 $h{event_raw} = LJ::trim_widgets(
1376 length => $req->{trim_widgets},
1377 img_length => $req->{widgets_img_length},
1378 text => $h{event_raw},
1379 read_more => '<a href="' . $entry->url . '"> ...</a>',
1380 ) if $req->{trim_widgets};
1382 LJ::EmbedModule->expand_entry($entry->poster, \$h{event_raw}, get_video_id => 1) if $req->{get_video_ids};
1383 LJ::Poll->expand_entry(\$h{event_raw}, getpolls => 1, viewer => $u ) if $req->{get_polls};
1385 if ($req->{view}) {
1386 LJ::EmbedModule->expand_entry($entry->poster, \$h{event_raw}, edit => 1) if $req->{view} eq 'stored';
1387 } elsif ($req->{parseljtags}) {
1388 $h{event_raw} = LJ::convert_lj_tags_to_links(
1389 event => $h{event_raw},
1390 embed_url => $entry->url)
1393 if ($req->{'asxml'}) {
1394 my $tidy = LJ::Tidy->new();
1395 $h{event_raw} = $tidy->clean( $h{event_raw} );
1398 #userpic
1399 $h{poster_userpic_url} = $h{userpic} && $h{userpic}->url;
1401 # log time value
1402 $h{logtime} = $LJ::EndOfTime - $ei->{rlogtime};
1403 $h{do_captcha} = LJ::Talk::Post::require_captcha_test($u, $entry->poster, '', $h{ditemid}, 1)?1:0;
1405 push @res, \%h;
1407 push @uids, $h{posterid}, $h{journalid};
1410 my $users = LJ::load_userids(@uids);
1412 foreach (@res) {
1413 $_->{journalname} = $users->{ $_->{journalid} }->{'user'};
1414 $_->{journaltype} = $users->{ $_->{journalid} }->{'journaltype'};
1415 $_->{journalurl} = $users->{ $_->{journalid} }->journal_base;
1416 delete $_->{journalid};
1417 $_->{postername} = $users->{ $_->{posterid} }->{'user'};
1418 $_->{postertype} = $users->{ $_->{posterid} }->{'journaltype'};
1419 $_->{posterurl} = $users->{ $_->{posterid} }->journal_base;
1420 if ($users->{ $_->{posterid} }->identity) {
1421 my $i = $users->{ $_->{posterid} }->identity;
1422 $_->{'identity_type'} = $i->pretty_type;
1423 $_->{'identity_value'} = $i->value;
1424 $_->{'identity_url'} = $i->url($users->{ $_->{posterid} });
1425 $_->{'identity_display'} = $users->{ $_->{posterid} }->display_name;
1427 delete $_->{posterid};
1428 delete $_->{props}->{repost_offer};
1431 LJ::run_hooks("getfriendspage", {userid => $u->userid, });
1433 return {
1434 entries => [ @res ],
1435 skip => $skip,
1436 xc3 => {
1437 u => $u
1443 sub getinbox
1445 my ($req, $err, $flags) = @_;
1446 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getinbox');
1447 my $u = $flags->{'u'};
1449 my $itemshow = (defined $req->{itemshow}) ? $req->{itemshow} : 100;
1450 return fail($err, 209, 'xmlrpc.des.bad_value', {'param'=>'itemshow'}) if $itemshow ne int($itemshow ) or $itemshow <= 0 or $itemshow > 100;
1451 my $skip = (defined $req->{skip}) ? $req->{skip} : 0;
1452 return fail($err, 209, 'xmlrpc.des.bad_value', {'param'=>'skip'}) if $skip ne int($skip ) or $skip < 0 or $skip > 100;
1454 # get the user's inbox
1455 my $inbox = $u->notification_inbox or return fail($err, 500, 'xmlrpc.des.inbox_fail');
1457 my %type_number = (
1458 Befriended => 1,
1459 Birthday => 2,
1460 CommunityInvite => 3,
1461 CommunityJoinApprove => 4,
1462 CommunityJoinReject => 5,
1463 CommunityJoinRequest => 6,
1464 Defriended => 7,
1465 InvitedFriendJoins => 8,
1466 JournalNewComment => 9,
1467 JournalNewEntry => 10,
1468 NewUserpic => 11,
1469 NewVGift => 12,
1470 OfficialPost => 13,
1471 PermSale => 14,
1472 PollVote => 15,
1473 SupOfficialPost => 16,
1474 UserExpunged => 17,
1475 UserMessageRecvd => 18,
1476 UserMessageSent => 19,
1477 UserNewComment => 20,
1478 UserNewEntry => 21,
1480 my %number_type = reverse %type_number;
1482 my @notifications;
1484 my $sync_date;
1485 # check lastsync for valid date
1486 if ($req->{'lastsync'}) {
1487 $sync_date = int $req->{'lastsync'};
1488 return fail($err,203,'xmlrpc.des.date_unixtime',{'param'=>'syncitems'}) if $sync_date <= 0;
1491 my $before;
1492 # check before for valid date
1493 if ($req->{'before'}) {
1494 $before = int $req->{'before'};
1495 return fail($err,203,'xmlrpc.des.date_unixtime',{'param'=>'syncitems'}) if $before <= 0;
1498 if ($req->{gettype}) {
1499 $req->{gettype} = [$req->{gettype}] unless ref($req->{gettype});
1501 my %filter;
1502 $filter{"LJ::Event::" . $number_type{$_}} = 1 for @{$req->{gettype}};
1503 @notifications = grep { exists $filter{$_->event->class} } $inbox->items;
1505 } else {
1506 @notifications = $inbox->all_items;
1509 # By default, notifications are sorted as "oldest are the first"
1510 # Reverse it by "newest are the first"
1511 @notifications = reverse @notifications;
1513 @notifications = grep {
1514 (!$before || $_->when_unixtime <= $before) &&
1515 (!$sync_date || $_->when_unixtime >= $sync_date)
1516 } @notifications;
1518 $itemshow = scalar @notifications - $skip if scalar @notifications < $skip + $itemshow;
1520 my @res;
1521 foreach my $item (@notifications[$skip .. $itemshow + $skip - 1]) {
1522 my $raw = $item->event->raw_info($u, {extended => $req->{extended}});
1524 my $type_index = $type_number{$raw->{type}};
1525 if (defined $type_index) {
1526 $raw->{type} = $type_index;
1527 } else {
1528 $raw->{typename} = $raw->{type};
1529 $raw->{type} = 0;
1532 $raw->{state} = $item->{state};
1534 push @res, { %$raw,
1535 when => $item->when_unixtime,
1536 qid => $item->qid,
1540 return {
1541 'skip' => $skip,
1542 'items' => \@res,
1543 'login' => $u->user,
1544 'journaltype' => $u->journaltype,
1545 xc3 => {
1546 u => $u
1551 sub setmessageread {
1552 my ($req, $err, $flags) = @_;
1554 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'setmessageread');
1556 my $u = $flags->{'u'};
1558 # get the user's inbox
1559 my $inbox = $u->notification_inbox or return fail($err, 500, 'xmlrpc.des.inbox_fail');
1560 my @result;
1562 # passing requested ids for loading
1563 my @notifications = $inbox->all_items;
1565 # Try to select messages by qid if specified
1566 my @qids = @{$req->{qid}};
1567 if (scalar @qids) {
1568 foreach my $qid (@qids) {
1569 my $item = eval {LJ::NotificationItem->new($u, $qid)};
1570 $item->mark_read if $item;
1571 push @result, { qid => $qid, result => 'set read' };
1573 } else { # Else select it by msgid for back compatibility
1574 # make hash of requested message ids
1575 my %requested_items = map { $_ => 1 } @{$req->{messageid}};
1577 # proccessing only requested ids
1578 foreach my $item (@notifications) {
1579 my $msgid = $item->event->raw_info($u)->{msgid};
1580 next unless $requested_items{$msgid};
1581 # if message already read -
1582 if ($item->{state} eq 'R') {
1583 push @result, { msgid => $msgid, result => 'already red' };
1584 next;
1586 # in state no 'R' - marking as red
1587 $item->mark_read;
1588 push @result, { msgid => $msgid, result => 'set read' };
1592 return {
1593 result => \@result,
1594 xc3 => {
1595 u => $u
1600 sub sendmessage
1602 my ($req, $err, $flags) = @_;
1604 return fail($err, 315) if $LJ::DISABLED{user_messaging};
1606 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'sendmessage');
1607 my $u = $flags->{'u'};
1609 return fail($err, 305) if $u->statusvis eq 'S'; # suspended cannot send private messages
1611 my $msg_limit = LJ::get_cap($u, "usermessage_length");
1613 my @errors;
1615 my $subject_text = LJ::strip_html($req->{'subject'});
1616 return fail($err, 208, 'subject')
1617 unless LJ::text_in($subject_text);
1619 # strip HTML from body and test encoding and length
1620 my $body_text = LJ::strip_html($req->{'body'});
1621 return fail($err, 208, 'body')
1622 unless LJ::text_in($body_text);
1624 my ($msg_len_b, $msg_len_c) = LJ::text_length($body_text);
1625 return fail($err, 212, 'xmlrpc.des.message_long', {'len'=>LJ::commafy($msg_len_c), 'limit'=>LJ::commafy($msg_limit)})
1626 unless ($msg_len_c <= $msg_limit);
1629 return fail($err, 213, 'xmlrpc.des.message_empty', {'len'=>LJ::commafy($msg_len_c)})
1630 if ($msg_len_c <= 0);
1632 my @to = (ref $req->{'to'}) ? @{$req->{'to'}} : ($req->{'to'});
1633 return fail($err, 200) unless scalar @to;
1635 # remove duplicates
1636 my %to = map { lc($_), 1 } @to;
1637 @to = keys %to;
1639 my @msg;
1640 BML::set_language('en') unless BML::get_language();
1642 foreach my $to (@to) {
1643 my $tou = LJ::load_user($to);
1644 return fail($err, 100, $to)
1645 unless $tou;
1647 my $msg = LJ::Message->new({
1648 journalid => $u->userid,
1649 otherid => $tou->userid,
1650 subject => $subject_text,
1651 body => $body_text,
1652 parent_msgid => defined $req->{'parent'} ? $req->{'parent'} + 0 : undef,
1653 userpic => $req->{'userpic'} || undef,
1656 push @msg, $msg
1657 if $msg->can_send(\@errors);
1659 return fail($err, 203, join('; ', @errors))
1660 if scalar @errors;
1662 foreach my $msg (@msg) {
1663 $msg->send(\@errors);
1666 return {
1667 'sent_count' => scalar @msg,
1668 'msgid' => [ grep { $_ } map { $_->msgid } @msg ],
1669 (@errors ? ('last_errors' => \@errors) : () ),
1670 xc3 => {
1671 u => $u
1676 sub login
1678 my ($req, $err, $flags) = @_;
1679 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'login');
1680 return undef unless check_altusage($req, $err, $flags);
1682 my $u = $flags->{'u'};
1683 my $uowner = $flags->{'u_owner'} || $u;
1685 my $res = {
1686 xc3 => {
1687 u => $u
1690 my $ver = $req->{'ver'};
1692 ## check for version mismatches
1693 ## non-Unicode installations can't handle versions >=1
1695 return fail($err,207, 'xmlrpc.des.not_unicode')
1696 if $ver>=1 and not $LJ::UNICODE;
1698 # do not let locked people log in
1699 return fail($err, 308) if $u->{statusvis} eq 'L';
1701 ## return a message to the client to be displayed (optional)
1702 login_message($req, $res, $flags);
1703 LJ::text_out(\$res->{'message'}) if $ver>=1 and defined $res->{'message'};
1705 ## report what shared journals this user may post in
1706 $res->{'usejournals'} = list_usejournals($u) if (LJ::u_equals($u, $uowner));
1708 # identity users can only post to communities
1709 # return fail( $err, 150 )
1710 # if $u->is_identity and LJ::u_equals( $u, $uowner );
1713 ## return their friend groups
1714 if (LJ::u_equals($u, $uowner)) {
1715 $res->{'friendgroups'} = list_friendgroups($u);
1716 return fail($err, 502, 'xmlrpc.des.friend_groups_fail') unless $res->{'friendgroups'};
1717 if ($ver >= 1) {
1718 foreach (@{$res->{'friendgroups'}}) {
1719 LJ::text_out(\$_->{'name'});
1724 ## if they gave us a number of moods to get higher than, then return them
1725 if (defined $req->{'getmoods'}) {
1726 $res->{'moods'} = list_moods($req->{'getmoods'});
1727 if ($ver >= 1) {
1728 # currently all moods are in English, but this might change
1729 foreach (@{$res->{'moods'}}) { LJ::text_out(\$_->{'name'}) }
1733 ### picture keywords, if they asked for them.
1734 if ($req->{'getpickws'} || $req->{'getpickwurls'}) {
1735 my $pickws = list_pickws($uowner);
1736 @$pickws = sort { lc($a->[0]) cmp lc($b->[0]) } @$pickws;
1737 $res->{'pickws'} = [ map { $_->[0] } @$pickws ] if $req->{'getpickws'};
1738 if ($req->{'getpickwurls'}) {
1739 if ($uowner->{'defaultpicid'}) {
1740 $res->{'defaultpicurl'} = "$LJ::USERPIC_ROOT/$uowner->{'defaultpicid'}/$uowner->{'userid'}";
1742 $res->{'pickwurls'} = [ map {
1743 "$LJ::USERPIC_ROOT/$_->[1]/$uowner->{'userid'}"
1744 } @$pickws ];
1746 if ($ver >= 1) {
1747 # validate all text
1748 foreach(@{$res->{'pickws'}}) { LJ::text_out(\$_); }
1749 foreach(@{$res->{'pickwurls'}}) { LJ::text_out(\$_); }
1750 LJ::text_out(\$res->{'defaultpicurl'});
1753 ## return caps, if they asked for them
1754 if ($req->{'getcaps'} && $u->can_manage($uowner)) {
1755 $res->{'caps'} = $uowner->caps;
1758 ## return client menu tree, if requested
1759 if ($req->{'getmenus'} ) {
1760 $res->{'menus'} = hash_menus($uowner);
1761 if ($ver >= 1) {
1762 # validate all text, just in case, even though currently
1763 # it's all English
1764 foreach (@{$res->{'menus'}}) {
1765 LJ::text_out(\$_->{'text'});
1766 LJ::text_out(\$_->{'url'}); # should be redundant
1771 ## tell some users they can hit the fast servers later.
1772 $res->{'fastserver'} = 1 if LJ::get_cap($uowner, "fastserver");
1774 ## user info
1775 $res->{'username'} = $uowner->{'user'};
1776 $res->{'userid'} = $uowner->{'userid'};
1777 $res->{'fullname'} = $uowner->{'name'};
1778 LJ::text_out(\$res->{'fullname'}) if $ver >= 1;
1780 # Identity info
1781 if ($uowner->is_identity){
1782 my $i = $uowner->identity;
1783 $res->{'identity_type'} = $i->pretty_type;
1784 $res->{'identity_value'} = $i->value;
1785 $res->{'identity_url'} = $i->url($uowner);
1786 $res->{'identity_display'} = $uowner->display_name;
1787 } else {
1788 foreach (qw(identity_display identity_url identity_value identity_type)) {
1789 $res->{$_} = '';
1793 if ($req->{'clientversion'} =~ /^\S+\/\S+$/) {
1794 eval {
1795 LJ::Request->notes("clientver", $req->{'clientversion'});
1799 ## update or add to clientusage table
1800 if ($req->{'clientversion'} =~ /^\S+\/\S+$/ &&
1801 ! $LJ::DISABLED{'clientversionlog'})
1803 my $client = $req->{'clientversion'};
1805 return fail($err, 208, 'xmlrpc.des.bad_value', {'param'=>'clientversion'})
1806 if $ver >= 1 and not LJ::text_in($client);
1808 my $dbh = LJ::get_db_writer();
1809 my $qclient = $dbh->quote($client);
1810 my $cu_sql = "REPLACE INTO clientusage (userid, clientid, lastlogin) " .
1811 "SELECT $u->{'userid'}, clientid, NOW() FROM clients WHERE client=$qclient";
1812 my $sth = $dbh->prepare($cu_sql);
1813 $sth->execute;
1814 unless ($sth->rows) {
1815 # only way this can be 0 is if client doesn't exist in clients table, so
1816 # we need to add a new row there, to get a new clientid for this new client:
1817 $dbh->do("INSERT INTO clients (client) VALUES ($qclient)");
1818 # and now we can do the query from before and it should work:
1819 $sth = $dbh->prepare($cu_sql);
1820 $sth->execute;
1824 return $res;
1827 sub getfriendgroups
1829 my ($req, $err, $flags) = @_;
1830 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getfriendgroups');
1831 my $u = $flags->{'u'};
1832 my $res = {
1833 xc3 => {
1834 u => $u
1838 $res->{'friendgroups'} = list_friendgroups($u);
1839 return fail($err, 502, 'xmlrpc.des.friend_groups_fail') unless $res->{'friendgroups'};
1840 if ($req->{'ver'} >= 1) {
1841 foreach (@{$res->{'friendgroups'} || []}) {
1842 LJ::text_out(\$_->{'name'});
1846 return $res;
1849 sub getusertags
1851 my ($req, $err, $flags) = @_;
1852 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getusertags');
1853 return undef unless check_altusage($req, $err, $flags);
1855 my $u = $flags->{'u'};
1856 my $uowner = $flags->{'u_owner'} || $u;
1857 return fail($req, 502) unless $u && $uowner;
1859 my $tags = LJ::Tags::get_usertags($uowner, { remote => $u });
1860 return {
1861 tags => [ values %$tags ],
1862 xc3 => {
1863 u => $u
1868 sub getuserpics
1870 my ($req, $err, $flags) = @_;
1872 $flags->{'allow_anonymous'} = 1;
1873 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getuserpics');
1874 $flags->{'ignorecanuse'} = 1; # function return public info
1875 return undef unless check_altusage($req, $err, $flags);
1877 my $u = $flags->{'u'};
1878 my $uowner = $flags->{'u_owner'} || $u;
1879 return fail($err, 502) unless $uowner;
1881 my $res = {
1882 xc3 => {
1883 u => $u
1887 my $pickws = list_pickws($uowner);
1888 @$pickws = sort { lc($a->[0]) cmp lc($b->[0]) } @$pickws;
1889 $res->{'pickws'} = [ map { $_->[0] } @$pickws ];
1891 if ($uowner->{'defaultpicid'}) {
1892 $res->{'defaultpicurl'} = "$LJ::USERPIC_ROOT/$uowner->{'defaultpicid'}/$uowner->{'userid'}";
1894 $res->{'pickwurls'} = [ map {
1895 "$LJ::USERPIC_ROOT/$_->[1]/$uowner->{'userid'}"
1896 } @$pickws ];
1897 # validate all text
1898 foreach(@{$res->{'pickws'}}) { LJ::text_out(\$_); }
1899 foreach(@{$res->{'pickwurls'}}) { LJ::text_out(\$_); }
1900 LJ::text_out(\$res->{'defaultpicurl'});
1902 return $res;
1906 sub getfriends
1908 my ($req, $err, $flags) = @_;
1909 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getfriends');
1911 return fail($req,502) unless LJ::get_db_reader();
1912 my $u = $flags->{'u'};
1913 my $res = {
1914 xc3 => {
1915 u => $u
1919 if ($req->{'includegroups'}) {
1920 $res->{'friendgroups'} = list_friendgroups($u);
1921 return fail($err, 502, 'xmlrpc.des.friend_groups_fail') unless $res->{'friendgroups'};
1922 if ($req->{'ver'} >= 1) {
1923 foreach (@{$res->{'friendgroups'} || []}) {
1924 LJ::text_out(\$_->{'name'});
1928 # TAG:FR:protocol:getfriends_of
1929 if ($req->{'includefriendof'}) {
1930 $res->{'friendofs'} = list_friends($u, {
1931 'limit' => $req->{'friendoflimit'},
1932 'friendof' => 1,
1934 if ($req->{'ver'} >= 1) {
1935 foreach(@{$res->{'friendofs'}}) { LJ::text_out(\$_->{'fullname'}) };
1938 # TAG:FR:protocol:getfriends
1939 $res->{'friends'} = list_friends($u, {
1940 'limit' => $req->{'friendlimit'},
1941 'includebdays' => $req->{'includebdays'},
1943 if ($req->{'ver'} >= 1) {
1944 foreach(@{$res->{'friends'}}) { LJ::text_out(\$_->{'fullname'}) };
1947 return $res;
1950 sub friendof
1952 my ($req, $err, $flags) = @_;
1953 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'friendof');
1954 return fail($req,502) unless LJ::get_db_reader();
1955 my $u = $flags->{'u'};
1956 my $res = {
1957 xc3 => {
1958 u => $u
1962 # TAG:FR:protocol:getfriends_of2 (same as TAG:FR:protocol:getfriends_of)
1963 $res->{'friendofs'} = list_friends($u, {
1964 'friendof' => 1,
1965 'limit' => $req->{'friendoflimit'},
1967 if ($req->{'ver'} >= 1) {
1968 foreach(@{$res->{'friendofs'}}) { LJ::text_out(\$_->{'fullname'}) };
1971 return $res;
1974 sub checkfriends
1976 my ($req, $err, $flags) = @_;
1977 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'checkfriends');
1978 my $u = $flags->{'u'};
1979 my $res = {
1980 xc3 => {
1981 u => $u
1985 # return immediately if they can't use this mode
1986 unless (LJ::get_cap($u, "checkfriends")) {
1987 $res->{'new'} = 0;
1988 $res->{'interval'} = 36000; # tell client to bugger off
1989 return $res;
1992 ## have a valid date?
1993 my $lastupdate = $req->{'lastupdate'};
1994 if ($lastupdate) {
1995 return fail($err,203) unless
1996 ($lastupdate =~ /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/);
1997 } else {
1998 $lastupdate = "0000-00-00 00:00:00";
2001 my $interval = LJ::get_cap_min($u, "checkfriends_interval");
2002 $res->{'interval'} = $interval;
2004 my $mask;
2005 if ($req->{'mask'} and $req->{'mask'} !~ /\D/) {
2006 $mask = $req->{'mask'};
2009 my $memkey = [$u->{'userid'},"checkfriends:$u->{userid}:$mask"];
2010 my $update = LJ::MemCache::get($memkey);
2011 unless ($update) {
2012 # TAG:FR:protocol:checkfriends (wants reading list of mask, not "friends")
2013 my $fr = LJ::get_friends($u, $mask);
2014 unless ($fr && %$fr) {
2015 $res->{'new'} = 0;
2016 $res->{'lastupdate'} = $lastupdate;
2017 return $res;
2019 if (@LJ::MEMCACHE_SERVERS) {
2020 my $tu = LJ::get_timeupdate_multi({ memcache_only => 1 }, keys %$fr);
2021 my $max = 0;
2022 while ($_ = each %$tu) {
2023 $max = $tu->{$_} if $tu->{$_} > $max;
2025 $update = LJ::TimeUtil->mysql_time($max) if $max;
2026 } else {
2027 my $dbr = LJ::get_db_reader();
2028 unless ($dbr) {
2029 # rather than return a 502 no-db error, just say no updates,
2030 # because problem'll be fixed soon enough by db admins
2031 $res->{'new'} = 0;
2032 $res->{'lastupdate'} = $lastupdate;
2033 return $res;
2035 my $list = join(", ", map { int($_) } keys %$fr);
2036 if ($list) {
2037 my $sql = "SELECT MAX(timeupdate) FROM userusage ".
2038 "WHERE userid IN ($list)";
2039 $update = $dbr->selectrow_array($sql);
2042 LJ::MemCache::set($memkey,$update,time()+$interval) if $update;
2044 $update ||= "0000-00-00 00:00:00";
2046 if ($req->{'lastupdate'} && $update gt $lastupdate) {
2047 $res->{'new'} = 1;
2048 } else {
2049 $res->{'new'} = 0;
2052 $res->{'lastupdate'} = $update;
2054 return $res;
2057 sub getdaycounts
2059 my ($req, $err, $flags) = @_;
2060 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getdaycounts');
2061 return undef unless check_altusage($req, $err, $flags);
2063 my $u = $flags->{'u'};
2064 my $uowner = $flags->{'u_owner'} || $u;
2065 my $ownerid = $flags->{'ownerid'};
2067 my $res = {
2068 xc3 => {
2069 u => $u
2073 my $daycts = LJ::get_daycounts($uowner, $u);
2074 return fail($err,502) unless $daycts;
2076 foreach my $day (@$daycts) {
2077 my $date = sprintf("%04d-%02d-%02d", $day->[0], $day->[1], $day->[2]);
2078 push @{$res->{'daycounts'}}, { 'date' => $date, 'count' => $day->[3] };
2081 return $res;
2084 sub common_event_validation
2086 my ($req, $err, $flags) = @_;
2088 # clean up event whitespace
2089 # remove surrounding whitespace
2090 $req->{event} =~ s/^\s+//;
2091 $req->{event} =~ s/\s+$//;
2093 # convert line endings to unix format
2094 if ($req->{'lineendings'} eq "mac") {
2095 $req->{event} =~ s/\r/\n/g;
2096 } else {
2097 $req->{event} =~ s/\r//g;
2100 # date validation
2101 if ($req->{'year'} !~ /^\d\d\d\d$/ ||
2102 $req->{'year'} < 1970 || # before unix time started = bad
2103 $req->{'year'} > 2037) # after unix time ends = worse! :)
2105 return fail($err,203,'xmlrpc.des.bad_value',{'param'=>'year'});
2107 if ($req->{'mon'} !~ /^\d{1,2}$/ ||
2108 $req->{'mon'} < 1 ||
2109 $req->{'mon'} > 12)
2111 return fail($err,203,'xmlrpc.des.bad_value',{'param'=>'month'});
2113 if ($req->{'day'} !~ /^\d{1,2}$/ || $req->{'day'} < 1 ||
2114 $req->{'day'} > LJ::TimeUtil->days_in_month($req->{'mon'},
2115 $req->{'year'}))
2117 return fail($err,203,'xmlrpc.des.bad_value',{'param'=>'day of month'});
2119 if ($req->{'hour'} !~ /^\d{1,2}$/ ||
2120 $req->{'hour'} < 0 || $req->{'hour'} > 23)
2122 return fail($err,203,'xmlrpc.des.bad_value',{'param'=>'hour'});
2124 if ($req->{'min'} !~ /^\d{1,2}$/ ||
2125 $req->{'min'} < 0 || $req->{'min'} > 59)
2127 return fail($err,203,'xmlrpc.des.bad_value',{'param'=>'minute'});
2130 # column width
2131 # we only trim Unicode data
2133 if ($req->{'ver'} >=1 ) {
2134 $req->{'subject'} = LJ::text_trim($req->{'subject'}, LJ::BMAX_SUBJECT, LJ::CMAX_SUBJECT);
2135 $req->{'event'} = LJ::text_trim($req->{'event'}, LJ::BMAX_EVENT, LJ::CMAX_EVENT);
2136 foreach (keys %{$req->{'props'}}) {
2137 # do not trim this property, as it's magical and handled later
2138 next if $_ eq 'taglist';
2140 # Allow syn_links and syn_ids the full width of the prop, to avoid truncating long URLS
2141 if ($_ eq 'syn_link' || $_ eq 'syn_id') {
2142 $req->{'props'}->{$_} = LJ::text_trim($req->{'props'}->{$_}, LJ::BMAX_PROP);
2143 } elsif ( $_ eq 'current_music' || $_ eq 'current_location' ) {
2144 $req->{'props'}->{$_} = LJ::text_trim($req->{'props'}->{$_}, LJ::CMMAX_PROP);
2145 } else {
2146 $req->{'props'}->{$_} = LJ::text_trim($req->{'props'}->{$_}, LJ::BMAX_PROP, LJ::CMAX_PROP);
2152 # setup non-user meta-data. it's important we define this here to
2153 # 0. if it's not defined at all, then an editevent where a user
2154 # removes random 8bit data won't remove the metadata. not that
2155 # that matters much. but having this here won't hurt. false
2156 # meta-data isn't saved anyway. so the only point of this next
2157 # line is making the metadata be deleted on edit.
2158 $req->{'props'}->{'unknown8bit'} = 0;
2160 # we don't want attackers sending something that looks like gzipped data
2161 # in protocol version 0 (unknown8bit allowed), otherwise they might
2162 # inject a 100MB string of single letters in a few bytes.
2163 return fail($err,208,'xmlrpc.des.send_gzip_fail')
2164 if substr($req->{'event'},0,2) eq "\037\213";
2166 # non-ASCII?
2167 unless ( $flags->{'use_old_content'} || (
2168 LJ::is_ascii($req->{'event'}) &&
2169 LJ::is_ascii($req->{'subject'}) &&
2170 LJ::is_ascii(join(' ', values %{$req->{'props'}})) ))
2173 if ($req->{'ver'} < 1) { # client doesn't support Unicode
2174 ## Hack: some old clients do send valid UTF-8 data,
2175 ## but don't tell us about that.
2176 ## Check, if the event/subject are valid UTF-8 strings.
2177 my $tmp_event = $req->{'event'};
2178 my $tmp_subject = $req->{'subject'};
2179 Encode::from_to($tmp_event, "utf-8", "utf-8");
2180 Encode::from_to($tmp_subject, "utf-8", "utf-8");
2181 if ($tmp_event eq $req->{'event'} && $tmp_subject eq $req->{'subject'}) {
2182 ## ok, this looks like valid UTF-8
2183 } else {
2184 ## encoding is unknown - it's neither ASCII nor UTF-8
2185 # only people should have unknown8bit entries.
2186 my $uowner = $flags->{u_owner} || $flags->{u};
2187 return fail($err,207,'xmlrpc.des.need_unicode_client')
2188 if $uowner->{journaltype} ne 'P';
2190 # so rest of site can change chars to ? marks until
2191 # default user's encoding is set. (legacy support)
2192 $req->{'props'}->{'unknown8bit'} = 1;
2194 } else {
2195 return fail($err,207, 'xmlrpc.des.not_unicode') unless $LJ::UNICODE;
2196 # validate that the text is valid UTF-8
2197 if (!LJ::text_in($req->{'subject'}) ||
2198 !LJ::text_in($req->{'event'}) ||
2199 grep { !LJ::text_in($_) } values %{$req->{'props'}}) {
2200 return fail($err, 208, 'xmlrpc.des.not_valid_unicode');
2205 ## handle meta-data (properties)
2206 LJ::load_props("log");
2207 foreach my $pname (keys %{$req->{'props'}})
2209 my $p = LJ::get_prop("log", $pname);
2211 # does the property even exist?
2212 unless ($p) {
2213 $pname =~ s/[^\w]//g;
2214 return fail($err,205,$pname);
2217 # don't validate its type if it's 0 or undef (deleting)
2218 next unless ($req->{'props'}->{$pname});
2220 my $ptype = $p->{'datatype'};
2221 my $val = $req->{'props'}->{$pname};
2223 if ($ptype eq "bool" && $val !~ /^[01]$/) {
2224 return fail($err,204,'xmlrpc.des.non_boolean',{'param'=>$pname});
2226 if ($ptype eq "num" && $val =~ /[^\d]/) {
2227 return fail($err,204,'xmlrpc.des.non_arifmetic',{'param'=>$pname,'value'=>$val});
2229 if ($pname eq "current_coords" && ! eval { LJ::Location->new(coords => $val) }) {
2230 return fail($err,204,'xmlrpc.des.bad_value', {'param'=>'current_coords'});
2234 # check props for inactive userpic
2235 if (my $pickwd = $req->{'props'}->{'picture_keyword'}) {
2236 my $pic = LJ::get_pic_from_keyword($flags->{'u'}, $pickwd);
2238 # need to make sure they aren't trying to post with an inactive keyword, but also
2239 # we don't want to allow them to post with a keyword that has no pic at all to prevent
2240 # them from deleting the keyword, posting, then adding it back with editpics.bml
2241 delete $req->{'props'}->{'picture_keyword'} if ! $pic || $pic->{'state'} eq 'I';
2244 # validate incoming list of tags
2245 return fail($err, 211)
2246 if $req->{props}->{taglist} &&
2247 ! LJ::Tags::is_valid_tagstring($req->{props}->{taglist});
2249 return 1;
2252 sub postevent {
2253 my ($req, $err, $flags) = @_;
2254 un_utf8_request($req);
2256 my $post_noauth = LJ::run_hook('post_noauth', $req);
2258 return undef unless $post_noauth || authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'postevent');
2259 my $spam = 0;
2260 LJ::run_hook('spam_detector', $req, \$spam);
2261 return fail($err,320) if $spam;
2263 # if going through mod queue, then we know they're permitted to post at least this entry
2264 $flags->{'usejournal_okay'} = 1 if $post_noauth;
2265 return undef unless check_altusage($req, $err, $flags) || $flags->{nomod};
2267 my $u = $flags->{'u'};
2268 my $ownerid = $flags->{'ownerid'}+0;
2269 my $uowner = $flags->{'u_owner'} || $u;
2270 # Make sure we have a real user object here
2271 $uowner = LJ::want_user($uowner) unless LJ::isu($uowner);
2272 r($uowner) unless LJ::isu($uowner);
2273 my $clusterid = $uowner->{'clusterid'};
2275 my $dbh = LJ::get_db_writer();
2276 my $dbcm = LJ::get_cluster_master($uowner);
2278 return fail($err,306) unless $dbh && $dbcm && $uowner->writer;
2279 return fail($err,200) unless $req->{'event'} =~ /\S/;
2281 ### make sure community, shared, or news journals don't post
2282 ### note: shared and news journals are deprecated. every shared journal
2283 ## should one day be a community journal, of some form.
2284 return fail($err,150) if ($u->{'journaltype'} eq "C" ||
2285 $u->{'journaltype'} eq "S" ||
2286 $u->{'journaltype'} eq "N");
2288 # identity users can only post to communities
2289 return fail( $err, 150 )
2290 if $u->is_identity and LJ::u_equals( $u, $uowner );
2292 # underage users can't do this
2293 return fail($err,310) if $u->underage;
2295 # suspended users can't post
2296 return fail($err,305) if ($u->{'statusvis'} eq "S");
2298 # memorials can't post
2299 return fail($err,309) if $u->{statusvis} eq 'M';
2301 # locked accounts can't post
2302 return fail($err,308) if $u->{statusvis} eq 'L';
2304 # check the journal's read-only bit
2305 return fail($err,306) if LJ::get_cap($uowner, "readonly");
2307 # is the user allowed to post?
2308 return fail($err,404,$LJ::MSG_NO_POST) unless LJ::get_cap($u, "can_post");
2310 # is the user allowed to post?
2311 return fail($err,410) if LJ::get_cap($u, "disable_can_post");
2313 # read-only accounts can't post
2314 return fail($err,316) if $u->is_readonly;
2316 # read-only accounts can't be posted to
2317 return fail($err,317) if $uowner->is_readonly;
2319 # can't post to deleted/suspended community
2320 return fail($err,307) unless $uowner->{'statusvis'} eq "V";
2322 # user must have a validated email address to post to any journal - including its own,
2323 # except syndicated (rss, 'Y') journals
2324 # unless this is approved from the mod queue (we'll error out initially, but in case they change later)
2325 return fail($err, 155, LJ::Lang::ml('event.post.error.not_validated_email'))
2326 unless $flags->{'first_post'} || $u->{'status'} eq 'A' || $u->{'journaltype'} eq 'Y';
2328 $req->{'event'} =~ s/\r\n/\n/g; # compact new-line endings to more comfort chars count near 65535 limit
2330 # post content too large
2331 # NOTE: requires $req->{event} be binary data, but we've already
2332 # removed the utf-8 flag in the XML-RPC path, and it never gets
2333 # set in the "flat" protocol path.
2334 return fail($err,409) if length($req->{'event'}) >= LJ::BMAX_EVENT;
2336 my $time_was_faked = 0;
2337 my $offset = 0; # assume gmt at first.
2339 if (defined $req->{'tz'}) {
2340 if ($req->{tz} eq 'guess') {
2341 LJ::get_timezone($u, \$offset, \$time_was_faked);
2342 } elsif ($req->{'tz'} =~ /^[+\-]\d\d\d\d$/) {
2343 # FIXME we ought to store this timezone and make use of it somehow.
2344 $offset = $req->{'tz'} / 100.0;
2345 } else {
2346 return fail($err, 203, 'xmlrpc.des.bad_value', {'param'=>'tz'});
2350 if (defined $req->{'tz'} and not grep { defined $req->{$_} } qw(year mon day hour min)) {
2351 my @ltime = gmtime(time() + ($offset*3600));
2352 $req->{'year'} = $ltime[5]+1900;
2353 $req->{'mon'} = $ltime[4]+1;
2354 $req->{'day'} = $ltime[3];
2355 $req->{'hour'} = $ltime[2];
2356 $req->{'min'} = $ltime[1];
2357 $time_was_faked = 1;
2360 return undef
2361 unless common_event_validation($req, $err, $flags);
2363 # confirm we can add tags, at least
2364 return fail($err, 312)
2365 if $req->{props} && $req->{props}->{taglist} &&
2366 ! LJ::Tags::can_add_tags($uowner, $u);
2368 my $event = $req->{'event'};
2370 if ($uowner->is_community) {
2371 delete $req->{'props'}->{'opt_backdated'};
2374 ### allow for posting to journals that aren't yours (if you have permission)
2375 my $posterid = $u->{'userid'}+0;
2377 # make the proper date format
2378 my $eventtime = sprintf("%04d-%02d-%02d %02d:%02d",
2379 $req->{'year'}, $req->{'mon'},
2380 $req->{'day'}, $req->{'hour'},
2381 $req->{'min'});
2382 my $qeventtime = $dbh->quote($eventtime);
2384 # load userprops all at once
2385 my @poster_props = qw(newesteventtime);
2386 my @owner_props = qw(newpost_minsecurity moderated);
2387 push @owner_props, 'opt_weblogscom' unless $req->{'props'}->{'opt_backdated'};
2389 LJ::load_user_props($u, @poster_props, @owner_props);
2390 if ($uowner->{'userid'} == $u->{'userid'}) {
2391 $uowner->{$_} = $u->{$_} foreach (@owner_props);
2392 } else {
2393 LJ::load_user_props($uowner, @owner_props);
2396 # are they trying to post back in time?
2397 if ($posterid == $ownerid && $u->{'journaltype'} ne 'Y' &&
2398 !LJ::is_enabled("delayed_entries") &&
2399 !$time_was_faked && $u->{'newesteventtime'} &&
2400 $eventtime lt $u->{'newesteventtime'} &&
2401 !$req->{'props'}->{'opt_backdated'}) {
2402 return fail($err, 153, 'xmlrpc.des.entry_time_conflict', {'newesteventtime'=>$u->{'newesteventtime'}});
2405 if ( $req->{sticky} &&
2406 $uowner->is_community() &&
2407 !$u->can_manage($uowner) )
2409 return fail($err, 158);
2412 my $qallowmask = $req->{'allowmask'}+0;
2413 my $security = "public";
2414 my $uselogsec = 0;
2415 if ($req->{'security'} eq "usemask" || $req->{'security'} eq "private") {
2416 $security = $req->{'security'};
2418 if ($req->{'security'} eq "usemask") {
2419 $uselogsec = 1;
2422 # can't specify both a custom security and 'friends-only'
2423 return fail($err, 203, 'xmlrpc.des.friends_security')
2424 if $qallowmask > 1 && $qallowmask % 2;
2426 ## if newpost_minsecurity is set, new entries have to be
2427 ## a minimum security level
2428 unless ($flags->{'entryrepost'}) {
2429 $security = "private"
2430 if $uowner->newpost_minsecurity eq "private";
2431 ($security, $qallowmask) = ("usemask", 1)
2432 if $uowner->newpost_minsecurity eq "friends"
2433 and $security eq "public";
2436 my $qsecurity = $dbh->quote($security);
2438 ### make sure user can't post with "custom/private security" on shared journals
2439 return fail($err,102)
2440 if ($ownerid != $posterid && # community post
2441 !($u && $u->can_manage($uowner)) && # poster is not admin
2442 ($req->{'security'} eq "private" ||
2443 ($req->{'security'} eq "usemask" && $qallowmask != 1 )));
2445 # make sure this user isn't banned from posting here (if
2446 # this is a community journal)
2447 return fail($err,151) if
2448 LJ::is_banned($posterid, $ownerid);
2450 if (!LJ::is_enabled("delayed_entries")) {
2451 # don't allow backdated posts in communities
2452 return fail($err,152) if
2453 ($req->{'props'}->{"opt_backdated"} &&
2454 $uowner->{'journaltype'} ne "P");
2457 # do processing of embedded polls (doesn't add to database, just
2458 # does validity checking)
2459 my @polls = ();
2460 if (LJ::Poll->contains_new_poll(\$event))
2462 return fail($err,301,'xmlrpc.des.poll_not_permitted')
2463 unless (LJ::get_cap($u, "makepoll")
2464 || ($uowner->{'journaltype'} eq "C"
2465 && LJ::get_cap($uowner, "makepoll")
2466 && LJ::can_manage_other($u, $uowner)));
2468 my $error = "";
2469 @polls = LJ::Poll->new_from_html(\$event, \$error, {
2470 'journalid' => $ownerid,
2471 'posterid' => $posterid,
2473 return fail($err,103,$error) if $error;
2476 my $repost_offer;
2477 if (LJ::is_enabled("paid_repost")) {
2478 my $error;
2480 $repost_offer = LJ::Pay::Repost::Offer->from_create_entry(
2481 \$event,
2482 {repost_budget => $req->{'repost_budget'},
2483 limit_sc => $req->{'repost_limit_sc'},
2484 journalid => $ownerid,
2485 userid => $posterid,
2486 targeting_gender => $req->{'repost_targeting_gender'},
2487 targeting_age => $req->{'repost_targeting_age'},
2488 targeting_country => $req->{'repost_targeting_country'},
2489 targeting_state => $req->{'repost_targeting_state'}},
2490 \$error
2493 return fail($err,222) if $repost_offer && ! $flags->{noauth};
2495 return fail($err,160,$error) if $error;
2498 # convert RTE lj-embeds to normal lj-embeds
2499 $event = LJ::EmbedModule->transform_rte_post($event);
2501 # process module embedding
2502 LJ::EmbedModule->parse_module_embed($uowner, \$event);
2504 my $now = $dbcm->selectrow_array("SELECT UNIX_TIMESTAMP()");
2505 my $anum = int(rand(256));
2507 # by default we record the true reverse time that the item was entered.
2508 # however, if backdate is on, we put the reverse time at the end of time
2509 # (which makes it equivalent to 1969, but get_recent_items will never load
2510 # it... where clause there is: < $LJ::EndOfTime). but this way we can
2511 # have entries that don't show up on friends view, now that we don't have
2512 # the hints table to not insert into.
2513 my $rlogtime = $LJ::EndOfTime;
2514 unless ($req->{'props'}->{'opt_backdated'}) {
2515 $rlogtime -= $now;
2518 my $dupsig = Digest::MD5::md5_hex(join('', map { $req->{$_} }
2519 qw(subject event usejournal security allowmask)));
2520 my $lock_key = "post-$ownerid";
2522 # release our duplicate lock
2523 my $release = sub { $dbcm->do("SELECT RELEASE_LOCK(?)", undef, $lock_key); };
2525 # our own local version of fail that releases our lock first
2526 my $fail = sub { $release->(); return fail(@_); };
2528 my $res = {};
2529 my $res_done = 0; # set true by getlock when post was duplicate, or error getting lock
2531 my $getlock = sub {
2532 my ($delayed) = @_;
2533 my $r = $dbcm->selectrow_array("SELECT GET_LOCK(?, 2)", undef, $lock_key);
2534 unless ($r) {
2535 $res = undef; # a failure case has an undef result
2536 fail($err,503); # set error flag to "can't get lock";
2537 $res_done = 1; # tell caller to bail out
2538 return;
2541 if ($delayed) {
2542 my $entry = LJ::DelayedEntry->dupsig_check($uowner, $posterid, $req);
2543 if ($entry) {
2544 $res->{'delayedid'} = $entry->delayedid;
2545 $res->{'type'} = 'delayed';
2546 $res->{'url'} = $entry->url;
2548 $res_done = 1;
2549 $release->();
2551 return;
2554 LJ::load_user_props($u, { use_master => 1, reload => 1 }, 'dupsig_post');
2556 my @parts = split(/:/, $u->{'dupsig_post'});
2557 if ($parts[0] eq $dupsig) {
2558 # duplicate! let's make the client think this was just the
2559 # normal firsit response.
2561 $res->{'itemid'} = $parts[1];
2562 $res->{'anum'} = $parts[2];
2564 my $dup_entry = LJ::Entry->new($uowner, jitemid => $res->{'itemid'}, anum => $res->{'anum'});
2565 $res->{'url'} = $dup_entry->url;
2567 $res_done = 1;
2568 $release->();
2572 # LJSUP-9616
2573 if ($req->{'props'}->{'opt_backdated'}) {
2574 my $state_date = POSIX::strftime("%Y-%m-%d", gmtime);
2575 my $key = "stat:opt_backdated:$state_date";
2577 LJ::MemCache::incr($key, 1) ||
2578 (LJ::MemCache::add($key, 0), LJ::MemCache::incr($key, 1));
2580 my $poster_offset = $u->timezone;
2581 my @ltime = gmtime(time() + $poster_offset * 3600);
2582 my $current = sprintf("%04d-%02d-%02d %02d:%02d",
2583 $ltime[5]+1900,
2584 $ltime[4] + 1,
2585 $ltime[3],
2586 $ltime[2],
2587 $ltime[1]);
2588 if ($eventtime gt $current) {
2589 my $key_future = "stat:opt_backdated:future:$state_date";
2590 LJ::MemCache::incr($key_future, 1) ||
2591 (LJ::MemCache::add($key_future, 0), LJ::MemCache::incr($key_future, 1));
2595 if ( $req->{ver} > 3 && LJ::is_enabled("delayed_entries") ) {
2596 if ( $req->{'custom_time'} && LJ::DelayedEntry::is_future_date($req) ) {
2597 return fail($err, 215) unless $req->{tz};
2599 return fail($err, 159) if $repost_offer;
2601 # if posting to a moderated community, store and bail out here
2602 if ( !LJ::DelayedEntry::can_post_to($uowner, $u, $req)) {
2603 return fail($err, 322);
2606 $req->{ext}->{flags} = $flags;
2607 $req->{usejournal} = $req->{usejournal} || '';
2608 delete $req->{'custom_time'};
2610 $getlock->('delayed');
2611 return $res if $res_done;
2613 my $entry = LJ::DelayedEntry->create( $req, { journal => $uowner,
2614 poster => $u,} );
2615 if (!$entry) {
2616 return $fail->($err, 507);
2619 $res->{'delayedid'} = $entry->delayedid;
2620 $res->{'type'} = 'delayed';
2621 $res->{'url'} = $entry->url;
2623 $release->();
2624 return $res;
2626 else {
2627 $res->{type} = 'posted';
2631 my $need_moderated = ( $uowner->{'moderated'} =~ /^[1A]$/ ) ? 1 : 0;
2632 if ( $uowner->{'moderated'} eq 'F' ) {
2633 ## Scan post for spam
2634 LJ::run_hook('spam_community_detector', $uowner, $req, \$need_moderated);
2637 # if posting to a moderated community, store and bail out here
2638 if ($uowner->{'journaltype'} eq 'C' && $need_moderated && !$flags->{'nomod'}) {
2639 # don't moderate admins, moderators & pre-approved users
2640 my $dbh = LJ::get_db_writer();
2641 my $relcount = $dbh->selectrow_array("SELECT COUNT(*) FROM reluser ".
2642 "WHERE userid=$ownerid AND targetid=$posterid ".
2643 "AND type IN ('A','M','N')");
2644 unless ($relcount) {
2645 # moderation queue full?
2646 my $modcount = $dbcm->selectrow_array("SELECT COUNT(*) FROM modlog WHERE journalid=$ownerid");
2647 return fail($err, 407) if $modcount >= LJ::get_cap($uowner, "mod_queue");
2649 $modcount = $dbcm->selectrow_array("SELECT COUNT(*) FROM modlog ".
2650 "WHERE journalid=$ownerid AND posterid=$posterid");
2651 return fail($err, 408) if $modcount >= LJ::get_cap($uowner, "mod_queue_per_poster");
2653 $req->{'_moderate'}->{'authcode'} = LJ::make_auth_code(15);
2655 # create tag <lj-embed> from HTML-tag <embed>
2656 LJ::EmbedModule->parse_module_embed($uowner, \$req->{event});
2658 my $fr = $dbcm->quote(Storable::nfreeze($req));
2659 return fail($err, 409) if length($fr) > 200_000;
2661 # store
2662 my $modid = LJ::alloc_user_counter($uowner, "M");
2663 return fail($err, 501) unless $modid;
2665 $uowner->do("INSERT INTO modlog (journalid, modid, posterid, subject, logtime) ".
2666 "VALUES ($ownerid, $modid, $posterid, ?, NOW())", undef,
2667 LJ::text_trim($req->{'subject'}, 30, 0));
2668 return fail($err, 501) if $uowner->err;
2670 $uowner->do("INSERT INTO modblob (journalid, modid, request_stor) ".
2671 "VALUES ($ownerid, $modid, $fr)");
2672 if ($uowner->err) {
2673 $uowner->do("DELETE FROM modlog WHERE journalid=$ownerid AND modid=$modid");
2674 return fail($err, 501);
2677 # alert moderator(s), maintainers, owner
2678 my $mods = LJ::load_rel_user($dbh, $ownerid, 'M') || [];
2679 my $mains = LJ::load_rel_user($dbh, $ownerid, 'A') || [];
2680 my $super = LJ::load_rel_user($dbh, $ownerid, 'S') || [];
2681 my %mail_list = (map { $_ => 1 } (@$super, @$mods, @$mains));
2683 if (%mail_list) {
2684 # load up all these mods and figure out if they want email or not
2685 my $modlist = LJ::load_userids(keys %mail_list);
2687 my @emails;
2688 my $ct;
2689 foreach my $mod (values %$modlist) {
2690 last if $ct > 20; # don't send more than 20 emails.
2692 next unless $mod->is_visible;
2694 LJ::load_user_props($mod, 'opt_nomodemail');
2695 next if $mod->{opt_nomodemail};
2696 next if $mod->{status} ne "A";
2698 push @emails,
2700 to => $mod->email_raw,
2701 browselang => $mod->prop('browselang'),
2702 charset => $mod->mailencoding || 'utf-8',
2705 ++$ct;
2708 foreach my $to (@emails) {
2709 # TODO: html/plain text.
2710 my $body = LJ::Lang::get_text(
2711 $to->{'browselang'},
2712 'esn.moderated_submission.body', undef,
2714 user => $u->{'user'},
2715 subject => $req->{'subject'},
2716 community => $uowner->{'user'},
2717 modid => $modid,
2718 siteroot => $LJ::SITEROOT,
2719 sitename => $LJ::SITENAME,
2720 moderateurl => "$LJ::SITEROOT/community/moderate.bml?authas=$uowner->{'user'}&modid=$modid",
2721 viewurl => "$LJ::SITEROOT/community/moderate.bml?authas=$uowner->{'user'}",
2724 my $subject = LJ::Lang::get_text($to->{'browselang'},'esn.moderated_submission.subject');
2726 LJ::send_mail({
2727 'to' => $to->{to},
2728 'from' => $LJ::DONOTREPLY_EMAIL,
2729 'charset' => $to->{charset},
2730 'subject' => $subject,
2731 'body' => $body,
2736 my $msg = translate($u, "modpost", undef);
2737 return {
2738 'message' => $msg,
2739 xc3 => {
2740 u => $u,
2741 post => {
2742 coords => $req->{props}->{current_coords},
2743 has_images => ($req->{event} =~ /pics\.livejournal\.com/ ? 1 : 0),
2744 from_mobile => ($req->{event} =~ /m\.livejournal\.com/ ? 1 : 0)
2749 } # /moderated comms
2751 # posting:
2753 $getlock->();
2755 return $res if $res_done;
2757 # do rate-checking
2758 if ($u->{'journaltype'} ne "Y" && ! LJ::rate_log($u, "post", 1)) {
2759 return $fail->($err,405);
2762 my $jitemid = LJ::alloc_user_counter($uowner, "L");
2763 return $fail->($err,501,'xmlrpc.des.cannnot_generate_items') unless $jitemid;
2765 # bring in LJ::Entry with Class::Autouse
2766 LJ::Entry->can("dostuff");
2767 LJ::replycount_do($uowner, $jitemid, "init");
2769 # remove comments and logprops on new entry ... see comment by this sub for clarification
2770 LJ::Protocol::new_entry_cleanup_hack($u, $jitemid) if $LJ::NEW_ENTRY_CLEANUP_HACK;
2771 my $verb = $LJ::NEW_ENTRY_CLEANUP_HACK ? 'REPLACE' : 'INSERT';
2773 my $dberr;
2774 $uowner->log2_do(\$dberr, "INSERT INTO log2 (journalid, jitemid, posterid, eventtime, logtime, security, ".
2775 "allowmask, replycount, year, month, day, revttime, rlogtime, anum) ".
2776 "VALUES ($ownerid, $jitemid, $posterid, $qeventtime, FROM_UNIXTIME($now), $qsecurity, $qallowmask, ".
2777 "0, $req->{'year'}, $req->{'mon'}, $req->{'day'}, $LJ::EndOfTime-".
2778 "UNIX_TIMESTAMP($qeventtime), $rlogtime, $anum)");
2779 return $fail->($err,501,$dberr) if $dberr;
2781 # post become 'sticky post'
2782 if ( $req->{sticky} ) {
2783 $uowner->set_sticky_id($jitemid);
2784 my $state_date = POSIX::strftime("%Y-%m-%d", gmtime);
2786 my $postfix = '';
2787 if ($uowner->is_community) {
2788 $postfix = '_community';
2791 my $sticky_entry = "stat:sticky$postfix:$state_date";
2792 LJ::MemCache::incr($sticky_entry, 1) ||
2793 (LJ::MemCache::add($sticky_entry, 0), LJ::MemCache::incr($sticky_entry, 1));
2796 LJ::MemCache::incr([$ownerid, "log2ct:$ownerid"]);
2798 # set userprops.
2800 my %set_userprop;
2802 # keep track of itemid/anum for later potential duplicates
2803 $set_userprop{"dupsig_post"} = "$dupsig:$jitemid:$anum";
2805 # record the eventtime of the last update (for own journals only)
2806 $set_userprop{"newesteventtime"} = $eventtime
2807 if $posterid == $ownerid and not $req->{'props'}->{'opt_backdated'} and not $time_was_faked;
2809 $u->set_prop(\%set_userprop);
2812 # end duplicate locking section
2813 $release->();
2815 my $ditemid = $jitemid * 256 + $anum;
2817 ### finish embedding stuff now that we have the itemid
2819 ### this should NOT return an error, and we're mildly fucked by now
2820 ### if it does (would have to delete the log row up there), so we're
2821 ### not going to check it for now.
2823 my $error = "";
2824 foreach my $poll (@polls) {
2825 $poll->save_to_db(
2826 journalid => $ownerid,
2827 posterid => $posterid,
2828 ditemid => $ditemid,
2829 error => \$error,
2832 my $pollid = $poll->pollid;
2834 $event =~ s/<lj-poll-placeholder>/<lj-poll-$pollid>/;
2837 #### /embedding
2839 # record journal's disk usage
2840 my $bytes = length($event) + length($req->{'subject'});
2841 $uowner->dudata_set('L', $jitemid, $bytes);
2843 $uowner->do("$verb INTO logtext2 (journalid, jitemid, subject, event) ".
2844 "VALUES ($ownerid, $jitemid, ?, ?)", undef, $req->{'subject'},
2845 LJ::text_compress($event));
2846 if ($uowner->err) {
2847 my $msg = $uowner->errstr;
2848 LJ::delete_entry($uowner, $jitemid, undef, $anum); # roll-back
2849 return fail($err,501,"logtext:$msg");
2851 LJ::MemCache::set([$ownerid,"logtext:$clusterid:$ownerid:$jitemid"],
2852 [ $req->{'subject'}, $event ]);
2854 # keep track of custom security stuff in other table.
2855 if ($uselogsec) {
2856 $uowner->do("INSERT INTO logsec2 (journalid, jitemid, allowmask) ".
2857 "VALUES ($ownerid, $jitemid, $qallowmask)");
2858 if ($uowner->err) {
2859 my $msg = $uowner->errstr;
2860 LJ::delete_entry($uowner, $jitemid, undef, $anum); # roll-back
2861 return fail($err,501,"logsec2:$msg");
2865 # Entry tags
2866 if ($req->{props} && defined $req->{props}->{taglist}) {
2867 # slightly misnamed, the taglist is/was normally a string, but now can also be an arrayref.
2868 my $taginput = $req->{props}->{taglist};
2870 my $logtag_opts = {
2871 remote => $u,
2872 skipped_tags => [], # do all possible and report impossible
2875 if (ref $taginput eq 'ARRAY') {
2876 $logtag_opts->{set} = [@$taginput];
2877 $req->{props}->{taglist} = join(", ", @$taginput);
2878 } else {
2879 $logtag_opts->{set_string} = $taginput;
2882 my $rv = LJ::Tags::update_logtags($uowner, $jitemid, $logtag_opts);
2883 push @{$res->{warnings} ||= []}, LJ::Lang::ml('/update.bml.tags.skipped', { 'tags' => join(', ', @{$logtag_opts->{skipped_tags}}),
2884 'limit' => $uowner->get_cap('tags_max') } )
2885 if @{$logtag_opts->{skipped_tags}};
2888 ## copyright
2889 if (LJ::is_enabled('default_copyright', $u)) {
2890 $req->{'props'}->{'copyright'} = $u->prop('default_copyright')
2891 unless defined $req->{'props'}->{'copyright'};
2892 $req->{'props'}->{'copyright'} = 'P' # second try
2893 unless defined $req->{'props'}->{'copyright'};
2894 } else {
2895 delete $req->{'props'}->{'copyright'};
2898 ## give features
2899 if (LJ::is_enabled('give_features')) {
2900 $req->{'props'}->{'give_features'} = ($req->{'props'}->{'give_features'} eq 'enable') ? 1 :
2901 ($req->{'props'}->{'give_features'} eq 'disable') ? 0 :
2902 1; # LJSUP-9142: All users should be able to use give button
2905 my $entry = LJ::Entry->new($uowner, jitemid => $jitemid, anum => $anum);
2907 # meta-data
2908 if (%{$req->{'props'}}) {
2909 my $propset = {};
2911 foreach my $pname (keys %{$req->{'props'}}) {
2912 next unless $req->{'props'}->{$pname};
2913 next if $pname eq "revnum" || $pname eq "revtime";
2914 my $p = LJ::get_prop("log", $pname);
2915 next unless $p;
2916 next unless $req->{'props'}->{$pname};
2917 $propset->{$pname} = $req->{'props'}->{$pname};
2920 my %logprops;
2921 $entry->set_prop_multi( $propset, \%logprops );
2923 for my $key ( keys %logprops ) {
2924 next if $key =~ /^\d+$/;
2926 unless ( $LJ::CACHE_PROP{'log'}->{$key}->{'propid'} ) {
2927 delete $logprops{$key};
2929 else {
2930 $logprops{ $LJ::CACHE_PROP{'log'}->{$key}->{'propid'} } = delete $logprops{$key};
2934 # if set_prop_multi modified props above, we can set the memcache key
2935 # to be the hashref of modified props, since this is a new post
2936 LJ::MemCache::set([$uowner->{'userid'}, "logprop2:$uowner->{'userid'}:$jitemid"],
2937 \%logprops) if %logprops;
2940 # Paid Repost Offer
2941 if ($repost_offer) {
2942 my $error = '';
2944 $repost_offer->{jitemid} = $jitemid;
2946 my $offer_id = LJ::Pay::Repost::Offer->create(
2947 \$error,
2948 %$repost_offer,
2951 unless ( $offer_id ) {
2952 LJ::delete_entry($uowner, $jitemid, undef, $anum); # roll-back
2953 return fail($err,160,$error);
2957 $dbh->do("UPDATE userusage SET timeupdate=NOW(), lastitemid=$jitemid ".
2958 "WHERE userid=$ownerid") unless $flags->{'notimeupdate'};
2959 LJ::MemCache::set([$ownerid, "tu:$ownerid"], pack("N", time()), 30*60);
2961 # argh, this is all too ugly. need to unify more postpost stuff into async
2962 $u->invalidate_directory_record;
2964 # note this post in recentactions table
2965 LJ::note_recent_action($uowner, 'post');
2967 # if the post was public, and the user has not opted out, try to insert into the random table;
2968 # note we do INSERT INGORE since there will be lots of people posting every second, and that's
2969 # the granularity we use
2970 if ($security eq 'public' && LJ::u_equals($u, $uowner) && ! $u->prop('latest_optout')) {
2971 $u->do("INSERT IGNORE INTO random_user_set (posttime, userid) VALUES (UNIX_TIMESTAMP(), ?)",
2972 undef, $u->{userid});
2975 my @jobs; # jobs to add into TheSchwartz
2977 # notify weblogs.com of post if necessary
2978 if (!$LJ::DISABLED{'weblogs_com'} &&
2979 $u->{'opt_weblogscom'} &&
2980 LJ::get_cap($u, "weblogscom") &&
2981 $security eq "public" ) {
2982 push @jobs, TheSchwartz::Job->new_from_array("LJ::Worker::Ping::WeblogsCom", {
2983 'user' => $u->{'user'},
2984 'title' => $u->{'journaltitle'} || $u->{'name'},
2985 'url' => LJ::journal_base($u) . "/",
2989 my $ip = LJ::get_remote_ip();
2990 my $uniq = LJ::UniqCookie->current_uniq();
2992 $u->do('INSERT INTO logleft(userid, posttime, journalid, ditemid, ip, uniq, publicitem)
2993 VALUES (?, NOW(), ?, ?, ?, ?, ?)', undef,
2994 $posterid,
2995 $ownerid,
2996 $ditemid,
2997 $ip,
2998 $uniq,
2999 $security eq 'public'
3000 ) if $uowner->{'journaltype'} eq 'C';
3002 ## Counter "new_post" for monitoring
3003 LJ::run_hook ("update_counter", {
3004 counter => "new_post",
3007 # run local site-specific actions
3008 LJ::run_hooks("postpost", {
3009 'itemid' => $jitemid,
3010 'anum' => $anum,
3011 'journal' => $uowner,
3012 'poster' => $u,
3013 'event' => $event,
3014 'eventtime' => $eventtime,
3015 'subject' => $req->{'subject'},
3016 'security' => $security,
3017 'allowmask' => $qallowmask,
3018 'props' => $req->{'props'},
3019 'entry' => $entry,
3020 'jobs' => \@jobs, # for hooks to push jobs onto
3021 'req' => $req,
3022 'res' => $res,
3023 'entryrepost' => $flags->{'entryrepost'},
3024 'logtime' => $now,
3027 # cluster tracking
3028 LJ::mark_user_active($u, 'post');
3029 LJ::mark_user_active($uowner, 'post') unless LJ::u_equals($u, $uowner);
3031 $res->{'itemid'} = $jitemid; # by request of mart
3032 $res->{'anum'} = $anum;
3033 $res->{'ditemid'} = $ditemid;
3034 $res->{'url'} = $entry->url;
3036 if ($flags->{'entryrepost'}) {
3037 push @jobs, LJ::Event::JournalNewRepost->new($entry)->fire_job;
3038 } else {
3039 push @jobs, LJ::Event::JournalNewEntry->new($entry)->fire_job;
3041 if (!$LJ::DISABLED{'esn-userevents'} || $LJ::_T_FIRE_USERNEWENTRY) {
3042 push @jobs, LJ::Event::UserNewEntry->new($entry)->fire_job
3046 push @jobs, LJ::EventLogRecord::NewEntry->new($entry)->fire_job;
3048 # PubSubHubbub Support
3049 LJ::Feed::generate_hubbub_jobs($uowner, \@jobs) unless $uowner->is_syndicated;
3050 if (LJ::is_enabled('new_homepage_oftenread')) {
3051 push @jobs, TheSchwartz::Job->new(
3052 'funcname' => 'TheSchwartz::Worker::OftenRead',
3053 'arg' => {
3054 'journalid' => $uowner->userid,
3055 'jitemid' => $jitemid,
3060 my $sclient = LJ::theschwartz();
3061 if ($sclient && @jobs) {
3062 my @handles = $sclient->insert_jobs(@jobs);
3063 # TODO: error on failure? depends on the job I suppose? property of the job?
3066 $res->{xc3} = {
3067 u => $u,
3068 post => {
3069 url => $res->{url},
3070 coords => $req->{props}->{current_coords},
3071 has_images => ($req->{event} =~ /pics\.livejournal\.com/ ? 1 : 0),
3072 from_mobile => ($req->{event} =~ /m\.livejournal\.com/ ? 1 : 0)
3076 return $res;
3079 sub editevent {
3080 my ($req, $err, $flags) = @_;
3081 un_utf8_request($req);
3083 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'editevent');
3085 my $spam = 0;
3086 return undef unless LJ::run_hook('spam_detector', $req, \$spam);
3087 return fail($err,320) if $spam;
3089 # new rule from 14 march 2011: user is allowed to edit only if he is allowed to do new post
3090 # but is allowed to delete its own post
3091 return undef unless check_altusage($req, $err, $flags) or $req->{'event'} !~ /\S/;
3093 my $u = $flags->{'u'};
3095 # Ownerid - community/blog id
3096 my $ownerid = $flags->{'ownerid'};
3097 my $uowner = $flags->{'u_owner'} || $u;
3099 # Make sure we have a user object here
3100 $uowner = LJ::want_user($uowner) unless LJ::isu($uowner);
3101 my $clusterid = $uowner->{'clusterid'};
3103 # Posterid - the id of the author of the entry
3104 my $posterid = $u->{'userid'};
3105 my $qallowmask = $req->{'allowmask'}+0;
3106 my $sth;
3108 if ($uowner->is_community) {
3109 delete $req->{'props'}->{'opt_backdated'};
3112 my $itemid = $req->{'itemid'}+0;
3114 $itemid ||= int( ($req->{'ditemid'} + 0) / 256);
3116 # underage users can't do this
3117 return fail($err,310) if $u->underage;
3119 # check the journal's read-only bit
3120 return fail($err,306) if LJ::get_cap($uowner, "readonly");
3122 # can't edit in deleted/suspended community
3123 return fail($err,307) unless $uowner->{'statusvis'} eq "V" || $uowner->is_readonly;
3125 my $dbh = LJ::get_db_writer();
3126 my $dbcm = LJ::get_cluster_master($uowner);
3127 return fail($err,306) unless $dbcm && $dbh;
3129 # can't specify both a custom security and 'friends-only'
3130 return fail($err, 203, 'xmlrpc.des.friends_security')
3131 if $qallowmask > 1 && $qallowmask % 2;
3133 ### make sure user can't change a post to "custom/private security" on shared journals
3134 return fail($err,102)
3135 if ($ownerid != $posterid && # community post
3136 !($u && $u->can_manage($uowner)) && # poster is not admin
3137 ($req->{'security'} eq "private" ||
3138 ($req->{'security'} eq "usemask" && $qallowmask != 1 )));
3140 # make sure user can't change post in a certain community without being its member
3141 return fail($err,102)
3142 if ($LJ::JOURNALS_WITH_PROTECTED_CONTENT{ $uowner->{user} } &&
3143 !LJ::is_friend($uowner, $u));
3145 # make sure the new entry's under the char limit
3146 # NOTE: as in postevent, this requires $req->{event} to be binary data
3147 # but we've already removed the utf-8 flag in the XML-RPC path, and it
3148 # never gets set in the "flat" protocol path
3149 return fail($err, 409) if length($req->{event}) >= LJ::BMAX_EVENT;
3151 if ( $req->{ver} > 3 && LJ::is_enabled("delayed_entries") && $req->{delayedid} ) {
3152 my $delayedid = delete $req->{delayedid};
3153 my $res = {};
3155 if ( $delayedid ) {
3156 return fail( $err, 217 ) if $req->{itemid} || $req->{anum};
3157 return fail( $err, 215 ) unless $req->{tz};
3159 if ( $req->{repost_budget} || LJ::CleanHtml::Like->extract_repost_params(\$req->{event}) ) {
3160 return fail($err, 159);
3163 $req->{ext}->{flags} = $flags;
3164 $req->{usejournal} = $req->{usejournal} || '';
3166 my $entry = LJ::DelayedEntry->get_entry_by_id(
3167 $uowner,
3168 $delayedid,
3169 { userid => $posterid },
3172 return fail($err, 508) unless $entry;
3173 if ($req->{'event'} !~ /\S/ ) {
3174 $entry->delete();
3175 $res->{delayedid} = $delayedid;
3177 unless ( $flags->{'noauth'} ) {
3178 LJ::User::UserlogRecord::DeleteDelayedEntry->create(
3179 $uowner,
3180 'remote' => $u,
3181 'delayedid' => $delayedid,
3182 'method' => 'protocol',
3186 return $res;
3189 # updating an entry:
3190 return undef
3191 unless common_event_validation($req, $err, $flags);
3193 $entry->update($req);
3194 $res->{type} = 'delayed';
3195 $res->{delayedid} = $delayedid;
3198 return $res if $res->{type};
3201 if ( $req->{sticky} &&
3202 $uowner->is_community() &&
3203 !$u->can_manage($uowner) )
3205 return fail($err, 158);
3208 # don't moderate admins, moderators, pre-approved users & unsuspicious users
3209 my $is_unsuspicious_user = 0;
3210 LJ::run_hook('is_unsuspicious_user_in_comm', $posterid, \$is_unsuspicious_user);
3211 my $is_approved_user = LJ::RelationService->is_relation_type_to( $ownerid, $posterid, [ 'A','M','N' ] );
3212 unless ( $is_unsuspicious_user || $is_approved_user || !$uowner->check_non_whitelist_enabled() ) {
3214 my $entry = LJ::Entry->new($ownerid, jitemid => $itemid);
3215 my $modid_old = $entry->prop("mod_queue_id");
3217 my $need_moderated_old = 0;
3219 my $suspicious_list_old = {};
3220 LJ::run_hook('spam_community_detector', $uowner, { event => $entry->event_html }, \$need_moderated_old, $suspicious_list_old);
3222 my $need_moderated = 0;
3224 my $suspicious_list = {};
3225 LJ::run_hook('spam_community_detector', $uowner, $req, \$need_moderated, $suspicious_list);
3227 foreach ( keys %$suspicious_list_old ) {
3228 delete $suspicious_list->{$_};
3230 $need_moderated = scalar keys %$suspicious_list;
3232 if ($uowner->{'journaltype'} eq 'C' && !$flags->{'nomod'}) {
3235 if ($need_moderated) {
3237 $req->{'_moderate'}->{'authcode'} = LJ::make_auth_code(15);
3239 # create tag <lj-embed> from HTML-tag <embed>
3240 LJ::EmbedModule->parse_module_embed($uowner, \$req->{event});
3242 my $fr = $dbcm->quote(Storable::nfreeze($req));
3243 return fail($err, 409) if length($fr) > 200_000;
3245 # store
3246 my $modid = LJ::alloc_user_counter($uowner, "M");
3247 return fail($err, 501) unless $modid;
3249 $uowner->do("INSERT INTO modlog (journalid, modid, posterid, subject, logtime) ".
3250 "VALUES ($ownerid, $modid, $posterid, ?, NOW())", undef,
3251 LJ::text_trim($req->{'subject'}, 30, 0));
3252 return fail($err, 501) if $uowner->err;
3254 $uowner->do("INSERT INTO modblob (journalid, modid, request_stor) ".
3255 "VALUES ($ownerid, $modid, $fr)");
3256 if ($uowner->err) {
3257 $uowner->do("DELETE FROM modlog WHERE journalid=$ownerid AND modid=$modid");
3258 return fail($err, 501);
3261 if ($modid_old) {
3262 $uowner->do("DELETE FROM modlog WHERE journalid=$ownerid AND modid=$modid_old");
3263 return fail($err, 501) if $uowner->err;
3264 $uowner->do("DELETE FROM modblob WHERE journalid=$ownerid AND modid=$modid_old");
3265 return fail($err, 501) if $uowner->err;
3268 $entry->set_prop("mod_queue_id", $modid);
3270 my $suspicious_text = "";
3271 foreach ( sort keys %$suspicious_list ) {
3272 $suspicious_text .= " - $suspicious_list->{$_}->{type} - $suspicious_list->{$_}->{url}\n";
3275 # alert moderator(s), maintainers, owner
3276 my $mods = LJ::load_rel_user($dbh, $ownerid, 'M') || [];
3277 my $mains = LJ::load_rel_user($dbh, $ownerid, 'A') || [];
3278 my $super = LJ::load_rel_user($dbh, $ownerid, 'S') || [];
3279 my %mail_list = (map { $_ => 1 } (@$super, @$mods, @$mains));
3281 if (%mail_list) {
3282 # load up all these mods and figure out if they want email or not
3283 my $modlist = LJ::load_userids(keys %mail_list);
3285 my @emails;
3286 my $ct;
3287 foreach my $mod (values %$modlist) {
3288 last if $ct > 20; # don't send more than 20 emails.
3290 next unless $mod->is_visible;
3292 LJ::load_user_props($mod, 'opt_nomodemail');
3293 next if $mod->{opt_nomodemail};
3294 next if $mod->{status} ne "A";
3296 push @emails,
3298 to => $mod->email_raw,
3299 browselang => $mod->prop('browselang'),
3300 charset => $mod->mailencoding || 'utf-8',
3303 ++$ct;
3306 foreach my $to (@emails) {
3307 # TODO: html/plain text.
3308 my $body = LJ::Lang::get_text(
3309 $to->{'browselang'},
3310 'esn.moderated_edited_submission.body', undef,
3312 user => $u->{'user'},
3313 subject => $req->{'subject'},
3314 community => $uowner->{'user'},
3315 modid => $modid,
3316 siteroot => $LJ::SITEROOT,
3317 sitename => $LJ::SITENAME,
3318 moderateurl => "$LJ::SITEROOT/community/moderate.bml?authas=$uowner->{'user'}&modid=$modid",
3319 viewurl => "$LJ::SITEROOT/community/moderate.bml?authas=$uowner->{'user'}",
3320 susp_list => $suspicious_text,
3323 my $subject = LJ::Lang::get_text($to->{'browselang'},'esn.moderated_edited_submission.subject');
3325 LJ::send_mail({
3326 'to' => $to->{to},
3327 'from' => $LJ::DONOTREPLY_EMAIL,
3328 'charset' => $to->{charset},
3329 'subject' => $subject,
3330 'body' => $body,
3335 my $msg = translate($u, "modpost", undef);
3336 return {
3337 'message' => $msg,
3338 xc3 => {
3339 u => $u,
3340 post => {
3341 coords => $req->{props}->{current_coords},
3342 has_images => ($req->{event} =~ /pics\.livejournal\.com/ ? 1 : 0),
3343 from_mobile => ($req->{event} =~ /m\.livejournal\.com/ ? 1 : 0)
3347 } elsif ($modid_old) {
3348 $uowner->do("DELETE FROM modlog WHERE journalid=? and modid=?", undef, $ownerid, $modid_old);
3349 $uowner->do("DELETE FROM modblob WHERE journalid=? and modid=?", undef, $ownerid, $modid_old);
3350 $entry->set_prop("mod_queue_id", undef);
3355 # fetch the old entry from master database so we know what we
3356 # really have to update later. usually people just edit one part,
3357 # not every field in every table. reads are quicker than writes,
3358 # so this is worth it.
3359 my $oldevent = $dbcm->selectrow_hashref
3360 ("SELECT journalid AS 'ownerid', posterid, eventtime, logtime, ".
3361 "compressed, security, allowmask, year, month, day, ".
3362 "rlogtime, anum FROM log2 WHERE journalid=$ownerid AND jitemid=$itemid");
3364 ($oldevent->{subject}, $oldevent->{event}) = $dbcm->selectrow_array
3365 ("SELECT subject, event FROM logtext2 ".
3366 "WHERE journalid=$ownerid AND jitemid=$itemid");
3368 LJ::text_uncompress(\$oldevent->{'event'});
3370 # use_old_content indicates the subject and entry are not changing
3371 if ($flags->{'use_old_content'}) {
3372 $req->{'event'} = $oldevent->{event};
3373 $req->{'subject'} = $oldevent->{subject};
3376 # kill seconds in eventtime, since we don't use it, then we can use 'eq' and such
3377 $oldevent->{'eventtime'} =~ s/:00$//;
3379 ### make sure this user is allowed to edit this entry
3380 return fail($err,302)
3381 unless ($ownerid == $oldevent->{'ownerid'});
3383 ### what can they do to somebody elses entry? (in shared journal)
3384 ### can edit it if they own or maintain the journal, but not if the journal is read-only
3385 if ($posterid != $oldevent->{'posterid'} || $u->is_readonly || $uowner->is_readonly) {
3386 ## deleting.
3387 return fail($err,304)
3388 if ($req->{'event'} !~ /\S/ && !
3389 ($ownerid == $u->{'userid'} ||
3390 # community account can delete it (ick)
3392 LJ::can_manage_other($posterid, $ownerid)
3393 # if user is a community maintainer they can delete
3394 # it too (good)
3397 ## editing:
3398 if ($req->{'event'} =~ /\S/) {
3399 return fail($err,303) if $posterid != $oldevent->{'posterid'};
3400 return fail($err,318) if $u->is_readonly;
3401 return fail($err,319) if $uowner->is_readonly;
3405 # simple logic for deleting an entry
3406 if (!$flags->{'use_old_content'} && $req->{'event'} !~ /\S/) {
3407 ## 23.11.2009. Next code added due to some hackers activities
3408 ## that use trojans to delete user's entries in theirs journals.
3409 if ($LJ::DELETING_ENTRIES_IS_DISABLED
3410 && $u->is_person and $u->userid eq $oldevent->{ownerid}
3412 my $qsecurity = $uowner->quote('private');
3413 my $dberr;
3414 LJ::run_hooks('report_entry_update', $ownerid, $itemid);
3415 $uowner->log2_do(\$dberr, "UPDATE log2 SET security=$qsecurity " .
3416 "WHERE journalid=$ownerid AND jitemid=$itemid");
3417 return fail($err,501,$dberr) if $dberr;
3418 return fail($err, 321);
3421 # if their newesteventtime prop equals the time of the one they're deleting
3422 # then delete their newesteventtime.
3423 if ($u->{'userid'} == $uowner->{'userid'}) {
3424 LJ::load_user_props($u, { use_master => 1 }, "newesteventtime");
3425 if ($u->{'newesteventtime'} eq $oldevent->{'eventtime'}) {
3426 $u->clear_prop('newesteventtime');
3430 # log this event, unless noauth is on, which means it is being done internally and we should
3431 # rely on them to log why they're deleting the entry if they need to. that way we don't have
3432 # double entries, and we have as much information available as possible at the location the
3433 # delete is initiated.
3434 unless ( $flags->{'noauth'} ) {
3435 LJ::User::UserlogRecord::DeleteEntry->create( $uowner,
3436 'remote' => $u,
3437 'ditemid' => $itemid * 256 + $oldevent->{'anum'},
3438 'method' => 'protocol',
3442 # We must use property 'dupsig_post' in author of entry to be deleted, not in
3443 # remote user or journal owner!
3444 my $item = LJ::get_log2_row($uowner, $itemid);
3445 my $poster = $item ? LJ::want_user($item->{'posterid'}) : '';
3447 if ($req->{delspam}) {
3448 if ($uowner) {
3449 LJ::mark_entry_as_spam($uowner, $itemid);
3451 if ($poster) {
3452 if (my $remote = LJ::get_remote()) {
3453 LJ::User::UserlogRecord::SpamSet->create(
3454 $uowner,
3455 remote => $remote,
3456 spammerid => $poster->userid,
3460 $uowner->ban_user($poster);
3461 LJ::set_rel($uowner, $poster, 'D');
3466 LJ::delete_entry($uowner, $itemid, 'quick', $oldevent->{'anum'});
3468 # clear their duplicate protection, so they can later repost
3469 # what they just deleted. (or something... probably rare.)
3470 $poster->clear_prop('dupsig_post') if $poster && LJ::get_cluster_reader($poster);
3472 my $res = {
3473 'itemid' => $itemid,
3474 'anum' => $oldevent->{'anum'},
3475 xc3 => {
3476 u => $u
3480 if ( $itemid == $uowner->get_sticky_entry_id() ) {
3481 $uowner->remove_sticky_entry_id();
3484 $dbh->do("UPDATE userusage SET timeupdate=NOW() ".
3485 "WHERE userid=$ownerid");
3486 LJ::MemCache::set([$ownerid, "tu:$ownerid"], pack("N", time()), 30*60);
3488 return $res;
3491 # now make sure the new entry text isn't $CannotBeShown
3492 return fail($err, 210)
3493 if $req->{event} eq $CannotBeShown;
3495 if (!LJ::is_enabled("delayed_entries")) {
3496 # don't allow backdated posts in communities
3497 return fail($err,152) if
3498 ($req->{'props'}->{"opt_backdated"} &&
3499 $uowner->{'journaltype'} ne "P");
3502 # make year/mon/day/hour/min optional in an edit event,
3503 # and just inherit their old values
3505 $oldevent->{'eventtime'} =~ /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d)/;
3506 $req->{'year'} = $1 unless defined $req->{'year'};
3507 $req->{'mon'} = $2+0 unless defined $req->{'mon'};
3508 $req->{'day'} = $3+0 unless defined $req->{'day'};
3509 $req->{'hour'} = $4+0 unless defined $req->{'hour'};
3510 $req->{'min'} = $5+0 unless defined $req->{'min'};
3513 # updating an entry:
3514 return undef
3515 unless common_event_validation($req, $err, $flags);
3517 ### load existing meta-data
3518 my %curprops;
3519 LJ::load_log_props2($dbcm, $ownerid, [ $itemid ], \%curprops);
3522 # create, edit, revoke repost offer
3523 my ($repost_offer, $repost_offer_action);
3525 if (LJ::is_enabled("paid_repost") && $req->{'event'} =~ /\S/) {
3526 my $error;
3528 ($repost_offer, $repost_offer_action) = LJ::Pay::Repost::Offer->from_edit_entry(
3529 \$req->{event},
3531 current => $curprops{$itemid}->{repost_offer},
3532 userid => $posterid,
3533 journalid => $ownerid,
3534 jitemid => $itemid,
3535 budget => $req->{repost_budget},
3536 limit_sc => $req->{repost_limit_sc},
3537 revoke => !$req->{paid_repost_on},
3538 targeting_gender => $req->{repost_targeting_gender},
3539 targeting_age => $req->{repost_targeting_age},
3540 targeting_country => $req->{repost_targeting_country},
3541 targeting_state => $req->{repost_targeting_state}
3543 \$error,
3546 unless ($flags->{noauth}) {
3547 # cannot create or edit repost offer via api
3548 return fail($err,222) if $repost_offer && $repost_offer_action =~ /create|edit/;
3550 # do not revoke repost offer via api
3551 undef $repost_offer if $repost_offer && $repost_offer_action =~ /revoke/;
3554 return fail($err,160,$error) if $error;
3557 # make post sticky
3558 if ( $req->{sticky} ) {
3559 if( $uowner->get_sticky_entry_id() != $itemid ) {
3560 $uowner->set_sticky_id($itemid);
3562 my $state_date = POSIX::strftime("%Y-%m-%d", gmtime);
3563 my $postfix = '';
3564 if ($uowner->is_community) {
3565 $postfix = '_community';
3568 my $sticky_entry = "stat:sticky$postfix:$state_date";
3569 LJ::MemCache::incr($sticky_entry, 1) ||
3570 (LJ::MemCache::add($sticky_entry, 0), LJ::MemCache::incr($sticky_entry, 1));
3573 elsif ( $itemid == $uowner->get_sticky_entry_id() ) {
3574 $uowner->remove_sticky_entry_id();
3577 ## give features
3578 my $give_features = $req->{'props'}->{'give_features'};
3579 if ($give_features) {
3580 $req->{'props'}->{'give_features'} = ($give_features eq 'enable') ? 1 : 0;
3583 my $event = $req->{'event'};
3584 my $owneru = LJ::load_userid($ownerid);
3585 $event = LJ::EmbedModule->transform_rte_post($event);
3586 LJ::EmbedModule->parse_module_embed($owneru, \$event);
3588 my $bytes = length($event) + length($req->{'subject'});
3590 my $eventtime = sprintf("%04d-%02d-%02d %02d:%02d",
3591 map { $req->{$_} } qw(year mon day hour min));
3592 my $qeventtime = $dbcm->quote($eventtime);
3594 # preserve old security by default, use user supplied if it's understood
3595 my $security = $oldevent->{security};
3596 $security = $req->{security}
3597 if $req->{security} &&
3598 $req->{security} =~ /^(?:public|private|usemask)$/;
3600 $qallowmask = $oldevent->{allowmask} unless defined $req->{'allowmask'};
3602 my $do_tags = $req->{props} && defined $req->{props}->{taglist};
3603 if ($oldevent->{security} ne $security || $qallowmask != $oldevent->{allowmask}) {
3604 # FIXME: this is a hopefully temporary hack which deletes tags from the entry
3605 # when the security has changed. the real fix is to make update_logtags aware
3606 # of security changes so it can update logkwsum appropriately.
3608 unless ($do_tags) {
3609 # we need to fix security on this entry's tags, but the user didn't give us a tag list
3610 # to work with, so we have to go get the tags on the entry, and construct a tag list,
3611 # in order to pass to update_logtags down at the bottom of this whole update
3612 my $tags = LJ::Tags::get_logtags($uowner, $itemid);
3613 $tags = $tags->{$itemid};
3614 $req->{props}->{taglist} = join(',', sort values %{$tags || {}});
3615 $do_tags = 1; # bleh, force the update later
3618 LJ::Tags::delete_logtags($uowner, $itemid);
3621 my $qyear = $req->{'year'}+0;
3622 my $qmonth = $req->{'mon'}+0;
3623 my $qday = $req->{'day'}+0;
3625 if ($eventtime ne $oldevent->{'eventtime'} ||
3626 $security ne $oldevent->{'security'} ||
3627 (!$curprops{$itemid}->{opt_backdated} && $req->{props}{opt_backdated}) ||
3628 $qallowmask != $oldevent->{'allowmask'})
3630 # are they changing their most recent post?
3631 LJ::load_user_props($u, "newesteventtime");
3632 if ($u->{userid} == $uowner->{userid} &&
3633 $u->{newesteventtime} eq $oldevent->{eventtime}) {
3634 if (!$curprops{$itemid}->{opt_backdated} && $req->{props}{opt_backdated}) {
3635 # if they set the backdated flag, then we no longer know
3636 # the newesteventtime.
3637 $u->clear_prop('newesteventtime');
3638 } elsif ($eventtime ne $oldevent->{eventtime}) {
3639 # otherwise, if they changed time on this event,
3640 # the newesteventtime is this event's new time.
3641 $u->set_prop( 'newesteventtime' => $eventtime );
3645 my $qsecurity = $uowner->quote($security);
3646 my $dberr;
3647 LJ::run_hooks('report_entry_update', $ownerid, $itemid);
3648 $uowner->log2_do(\$dberr, "UPDATE log2 SET eventtime=$qeventtime, revttime=$LJ::EndOfTime-".
3649 "UNIX_TIMESTAMP($qeventtime), year=$qyear, month=$qmonth, day=$qday, ".
3650 "security=$qsecurity, allowmask=$qallowmask WHERE journalid=$ownerid ".
3651 "AND jitemid=$itemid");
3652 return fail($err,501,$dberr) if $dberr;
3654 # update memcached
3655 my $sec = $qallowmask;
3656 $sec = 0 if $security eq 'private';
3657 $sec = 2**31 if $security eq 'public';
3659 my $row = pack("NNNNN", $oldevent->{'posterid'},
3660 LJ::TimeUtil->mysqldate_to_time($eventtime, 1),
3661 LJ::TimeUtil->mysqldate_to_time($oldevent->{'logtime'}, 1),
3662 $sec,
3663 $itemid*256 + $oldevent->{'anum'});
3665 LJ::MemCache::set([$ownerid, "log2:$ownerid:$itemid"], $row);
3666 LJ::Entry->reset_singletons; ## flush cached LJ::Entry objects
3669 if ($security ne $oldevent->{'security'} ||
3670 $qallowmask != $oldevent->{'allowmask'})
3672 if ($security eq "public" || $security eq "private") {
3673 $uowner->do("DELETE FROM logsec2 WHERE journalid=$ownerid AND jitemid=$itemid");
3675 else {
3676 $uowner->do("REPLACE INTO logsec2 (journalid, jitemid, allowmask) ".
3677 "VALUES ($ownerid, $itemid, $qallowmask)");
3679 return fail($err,501,$dbcm->errstr) if $uowner->err;
3682 LJ::MemCache::set([$ownerid,"logtext:$clusterid:$ownerid:$itemid"],
3683 [ $req->{'subject'}, $event ]);
3685 if (!$flags->{'use_old_content'} && (
3686 $event ne $oldevent->{'event'} ||
3687 $req->{'subject'} ne $oldevent->{'subject'}))
3689 LJ::run_hooks('report_entry_text_update', $ownerid, $itemid);
3690 $uowner->do("UPDATE logtext2 SET subject=?, event=? ".
3691 "WHERE journalid=$ownerid AND jitemid=$itemid", undef,
3692 $req->{'subject'}, LJ::text_compress($event));
3693 return fail($err,501,$uowner->errstr) if $uowner->err;
3695 # update disk usage
3696 $uowner->dudata_set('L', $itemid, $bytes);
3699 # up the revision number
3700 $req->{'props'}->{'revnum'} = ($curprops{$itemid}->{'revnum'} || 0) + 1;
3701 $req->{'props'}->{'revtime'} = time();
3703 my $res = { 'itemid' => $itemid };
3705 # update or create repost offer
3706 if ($repost_offer) {
3707 my ($error, $warning);
3709 if($repost_offer_action eq 'create') {
3711 my $offer_id = LJ::Pay::Repost::Offer->create(\$error, %$repost_offer) or
3712 fail(\$warning,160,$error);
3714 } elsif($repost_offer_action eq 'edit') {
3715 $repost_offer->edit(\$error,
3716 add_budget => $repost_offer->{add_budget},
3717 limit_sc => $repost_offer->{limit_sc},
3718 targeting_opt => $repost_offer->{targeting_opt},
3719 ) or fail(\$warning,160,$error);
3721 } elsif($repost_offer_action eq 'revoke') {
3723 $repost_offer->revoke(\$error) or
3724 fail(\$warning,161,$error);
3727 push @{$res->{warnings} ||= []}, error_message($warning) if $warning;
3731 # handle tags if they're defined
3732 if ($do_tags) {
3733 my $tagerr = "";
3734 my $skipped_tags = [];
3735 my $rv = LJ::Tags::update_logtags($uowner, $itemid, {
3736 set_string => $req->{props}->{taglist},
3737 remote => $u,
3738 err_ref => \$tagerr,
3739 skipped_tags => $skipped_tags, # do all possible and report impossible
3741 push @{$res->{warnings} ||= []}, LJ::Lang::ml('/update.bml.tags.skipped', { 'tags' => join(', ', @$skipped_tags),
3742 'limit' => $uowner->get_cap('tags_max') } )
3743 if @$skipped_tags;
3746 if (LJ::is_enabled('default_copyright', $u)) {
3747 unless (defined $req->{'props'}->{'copyright'}) { # try 1: previous value
3748 $req->{'props'}->{'copyright'} = $curprops{$itemid}->{'copyright'};
3751 unless (defined $req->{'props'}->{'copyright'}) { # try 2: global setting
3752 $req->{'props'}->{'copyright'} = $uowner->prop('default_copyright');
3755 unless (defined $req->{'props'}->{'copyright'}) { # try 3: allow
3756 $req->{'props'}->{'copyright'} = 'P';
3759 else { # disabled feature
3760 delete $req->{'props'}->{'copyright'};
3763 my $entry = LJ::Entry->new($ownerid, jitemid => $itemid);
3765 # handle the props
3767 my $propset = {};
3768 foreach my $pname (keys %{$req->{'props'}}) {
3769 my $p = LJ::get_prop("log", $pname);
3770 next unless $p;
3771 $propset->{$pname} = $req->{'props'}->{$pname};
3773 $entry->set_prop_multi($propset);
3775 if ($req->{'props'}->{'copyright'} ne $curprops{$itemid}->{'copyright'}) {
3776 LJ::Entry->new($ownerid, jitemid => $itemid)->put_logprop_in_history('copyright', $curprops{$itemid}->{'copyright'},
3777 $req->{'props'}->{'copyright'});
3781 # compatible with depricated 'opt_backdated'
3782 if ($req->{'props'}->{'opt_backdated'} eq "1" &&
3783 $oldevent->{'rlogtime'} != $LJ::EndOfTime) {
3784 my $dberr;
3785 LJ::run_hooks('report_entry_update', $ownerid, $itemid);
3786 $uowner->log2_do(undef, "UPDATE log2 SET rlogtime=$LJ::EndOfTime WHERE ".
3787 "journalid=$ownerid AND jitemid=$itemid");
3788 return fail($err,501,$dberr) if $dberr;
3791 if ($req->{'props'}->{'opt_backdated'} eq "0" &&
3792 $oldevent->{'rlogtime'} == $LJ::EndOfTime) {
3793 my $dberr;
3794 LJ::run_hooks('report_entry_update', $ownerid, $itemid);
3795 $uowner->log2_do(\$dberr, "UPDATE log2 SET rlogtime=$LJ::EndOfTime-UNIX_TIMESTAMP(logtime) ".
3796 "WHERE journalid=$ownerid AND jitemid=$itemid");
3797 return fail($err,501,$dberr) if $dberr;
3799 return fail($err,501,$dbcm->errstr) if $dbcm->err;
3801 if (defined $oldevent->{'anum'}) {
3802 $res->{'anum'} = $oldevent->{'anum'};
3803 $res->{'url'} = LJ::item_link($uowner, $itemid, $oldevent->{'anum'});
3804 $res->{'ditemid'} = $itemid * 256 + $oldevent->{'anum'};
3807 $dbh->do("UPDATE userusage SET timeupdate=NOW() ".
3808 "WHERE userid=$ownerid");
3809 LJ::MemCache::set([$ownerid, "tu:$ownerid"], pack("N", time()), 30*60);
3811 LJ::EventLogRecord::EditEntry->new($entry)->fire;
3812 my @jobs; # jobs to insert into TheSchwartz
3813 LJ::run_hooks("editpost", $entry, \@jobs);
3815 # PubSubHubbub Support
3816 LJ::Feed::generate_hubbub_jobs($uowner, \@jobs) unless $uowner->is_syndicated;
3818 my $sclient = LJ::theschwartz();
3819 if ($sclient && @jobs) {
3820 my @handles = $sclient->insert_jobs(@jobs);
3821 # TODO: error on failure? depends on the job I suppose? property of the job?
3824 $res->{xc3} = {
3825 u => $u,
3826 post => {
3827 url => $res->{url},
3828 coords => $req->{props}->{current_coords},
3829 has_images => ($req->{event} =~ /pics\.livejournal\.com/ ? 1 : 0),
3830 from_mobile => ($req->{event} =~ /m\.livejournal\.com/ ? 1 : 0)
3834 return $res;
3837 sub getevents {
3838 my ($req, $err, $flags) = @_;
3840 $flags->{allow_anonymous} = 1;
3841 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getevents');
3843 $flags->{'ignorecanuse'} = 1; # later we will check security levels, so allow some access to communities
3844 return undef unless check_altusage($req, $err, $flags);
3846 my $u = $flags->{'u'};
3847 my $uowner = $flags->{'u_owner'} || $u;
3849 ### shared-journal support
3850 my $posterid = ($u ? $u->{'userid'} : 0);
3851 my $ownerid = $flags->{'ownerid'};
3853 if( $req->{journalid} ){
3854 $ownerid = $req->{journalid};
3855 $uowner = LJ::load_userid( $req->{journalid} );
3858 my $sticky_id = $uowner->prop("sticky_entry_id") || undef;
3859 my $dbr = LJ::get_db_reader();
3860 my $sth;
3862 # can't pull events from deleted/suspended journal
3863 return fail($err, 307) unless $uowner->{'statusvis'} eq "V" || $uowner->is_readonly;
3865 my $dbcr = LJ::get_cluster_reader($uowner);
3866 return fail($err, 502) unless $dbcr && $dbr;
3868 my $reject_code = $LJ::DISABLE_PROTOCOL{getevents};
3870 if (ref $reject_code eq "CODE") {
3871 my $errmsg = $reject_code->($req, $flags, eval { LJ::request->request });
3873 return fail($err, "311", $errmsg) if $errmsg;
3876 my $can_manage = $u && $u->can_manage($uowner);
3877 my $secmask = 0;
3879 if ($u && ($u->{'journaltype'} eq "P" || $u->{'journaltype'} eq "I") && $posterid != $ownerid) {
3880 $secmask = LJ::get_groupmask($ownerid, $posterid);
3883 # decide what level of security the remote user can see
3884 # 'getevents' used in small count of places and we will not pass 'viewall' through their call chain
3885 my $secwhere = "";
3887 if ($can_manage) {
3888 # no extra where restrictions... user can see all their own stuff
3890 elsif ($secmask) {
3891 # can see public or things with them in the mask
3892 # and own posts in non-sensitive communities
3893 if ($LJ::JOURNALS_WITH_PROTECTED_CONTENT{ $uowner->{user} }) {
3894 $secwhere = "AND (security='public' OR (security='usemask' AND allowmask & $secmask != 0))";
3896 else {
3897 $secwhere = "AND (security='public' OR (security='usemask' AND allowmask & $secmask != 0) OR posterid=$posterid)";
3900 else {
3901 # not a friend? only see public.
3902 # and own posts in non-sensitive communities
3903 if ($LJ::JOURNALS_WITH_PROTECTED_CONTENT{ $uowner->{user} } || !$posterid) {
3904 $secwhere = "AND (security='public')";
3906 else{
3907 $secwhere = "AND (security='public' OR posterid=$posterid)";
3911 # if this is on, we sort things different (logtime vs. posttime)
3912 # to avoid timezone issues
3913 my $is_community = ($uowner->{'journaltype'} eq "C" ||
3914 $uowner->{'journaltype'} eq "S");
3916 # in some cases we'll use the master, to ensure there's no
3917 # replication delay. useful cases: getting one item, use master
3918 # since user might have just made a typo and realizes it as they
3919 # post, or wants to append something they forgot, etc, etc. in
3920 # other cases, slave is pretty sure to have it.
3921 my $use_master = 0;
3923 # just synonym
3924 if ($req->{'itemshow'}){
3925 $req->{'selecttype'} = 'lastn' unless $req->{'selecttype'};
3926 $req->{'howmany'} = $req->{'itemshow'};
3929 my $skip = $req->{'skip'} + 0;
3931 $skip = 500 if $skip > 500;
3933 my $sort_order = $req->{'sort_order'};
3934 $sort_order = ($sort_order && $sort_order =~ /asc|desc|default/ ? $sort_order : 'default');
3936 if ( $req->{ver} > 3 && LJ::is_enabled("delayed_entries") ) {
3937 my $res = {};
3939 if ( $req->{delayed} ) {
3940 return fail( $err, 220 ) if $req->{view} && $req->{view} ne 'stored';
3942 if ( $req->{selecttype} eq 'lastn' ) {
3943 my $uid = $u->userid;
3944 my $howmany = $req->{'howmany'} || 20;
3945 if ($howmany > 50) { $howmany = 50; }
3947 my $ids = LJ::DelayedEntry->get_entries_by_journal(
3948 $uowner,
3949 { 'skip' => $req->{skip} || 0,
3950 'show' => $howmany,
3951 'userid' => $uid, });
3953 for my $did ( @$ids ) {
3954 my $entry = LJ::DelayedEntry->get_entry_by_id(
3955 $uowner,
3956 $did,
3957 { 'userid' => $uid, },
3960 if (!$entry) {
3961 next;
3964 my $re = {};
3966 $re->{$_} = $entry->$_ for qw(delayedid subject event logtime);
3967 my $props = $entry->props;
3968 foreach my $key (keys %$props) {
3969 if (!$props->{$key}) {
3970 delete $props->{$key};
3974 $re->{props} = $props;
3975 $re->{eventtime} = $entry->posttime;
3976 $re->{event_timestamp} = $entry->system_posttime;
3977 $re->{url} = $entry->url;
3978 $re->{security} = $entry->security;
3979 $re->{allowmask} = $entry->allowmask;
3980 $re->{posterid} = $entry->poster->userid;
3981 $re->{poster} = $entry->poster->username;
3983 push @{$res->{events}}, $re;
3986 elsif ( $req->{selecttype} eq 'one' ) {
3987 return fail( $err, 218) unless $req->{delayedid};
3988 my $uid = $u->userid;
3990 my $entry = LJ::DelayedEntry->get_entry_by_id(
3991 $uowner,
3992 $req->{delayedid},
3993 { 'userid' => $uid, },
3996 my $re = {};
3998 if (!$entry) {
3999 next;
4002 $re->{$_} = $entry->$_ for qw(delayedid subject event logtime);
4003 my $props = $entry->props;
4004 foreach my $key (keys %$props) {
4005 if (!$props->{$key}) {
4006 delete $props->{$key};
4010 $re->{props} = $props;
4011 $re->{eventtime} = $entry->posttime;
4012 $re->{event_timestamp} = $entry->system_posttime;
4013 $re->{url} = $entry->url;
4014 $re->{security} = $entry->security;
4015 $re->{allowmask} = $entry->allowmask;
4016 $re->{posterid} = $entry->poster->userid;
4017 $re->{poster} = $entry->poster->username;
4019 push @{$res->{events}}, $re;
4020 } elsif ( $req->{selecttype} eq 'multiple' ) {
4021 return fail( $err, 218) unless $req->{delayedids};
4022 my $uid = $u->userid;
4025 for my $did ( @{$req->{delayedids} }) {
4026 my $entry = LJ::DelayedEntry->get_entry_by_id(
4027 $uowner,
4028 $did,
4029 { 'userid' => $uid, },
4032 if (!$entry) {
4033 next;
4036 my $re = {};
4038 $re->{$_} = $entry->$_ for qw(delayedid subject event logtime);
4039 my $props = $entry->props;
4040 foreach my $key (keys %$props) {
4041 if (!$props->{$key}) {
4042 delete $props->{$key};
4046 $re->{props} = $props;
4047 $re->{eventtime} = $entry->posttime;
4048 $re->{event_timestamp} = $entry->system_posttime;
4049 $re->{url} = $entry->url;
4050 $re->{security} = $entry->security;
4051 $re->{allowmask} = $entry->allowmask;
4052 $re->{posterid} = $entry->poster->userid;
4053 $re->{poster} = $entry->poster->username;
4054 push @{$res->{events}}, $re;
4057 else {
4058 return fail( $err, 218 );
4061 return $res;
4065 # build the query to get log rows. each selecttype branch is
4066 # responsible for either populating the following 3 variables
4067 # OR just populating $sql
4068 my ($orderby, $where, $limit, $offset);
4069 my $sql;
4071 if ($req->{'selecttype'} eq "day") {
4072 return fail($err,203)
4073 unless ($req->{'year'} =~ /^\d\d\d\d$/ &&
4074 $req->{'month'} =~ /^\d\d?$/ &&
4075 $req->{'day'} =~ /^\d\d?$/ &&
4076 $req->{'month'} >= 1 && $req->{'month'} <= 12 &&
4077 $req->{'day'} >= 1 && $req->{'day'} <= 31);
4079 my $qyear = $dbr->quote($req->{'year'});
4080 my $qmonth = $dbr->quote($req->{'month'});
4081 my $qday = $dbr->quote($req->{'day'});
4082 $where = "AND year=$qyear AND month=$qmonth AND day=$qday";
4083 $limit = "LIMIT 200"; # FIXME: unhardcode this constant (also in ljviews.pl)
4085 # see note above about why the sort order is different
4086 $orderby = $is_community ? "ORDER BY logtime" : "ORDER BY eventtime";
4088 elsif ($req->{'selecttype'} eq "lastn") {
4089 my $howmany = $req->{'howmany'} || 20;
4091 if ($howmany > 50) { $howmany = 50; }
4093 $howmany = $howmany + 0;
4094 $limit = "LIMIT $howmany";
4096 $offset = "OFFSET $skip";
4098 # okay, follow me here... see how we add the revttime predicate
4099 # even if no beforedate key is present? you're probably saying,
4100 # that's retarded -- you're saying: "revttime > 0", that's like
4101 # saying, "if entry occurred at all." yes yes, but that hints
4102 # mysql's braindead optimizer to use the right index.
4103 my $rtime_after = 0;
4104 my $rtime_what = $is_community ? "rlogtime" : "revttime";
4106 if ($req->{'beforedate'}) {
4107 return fail($err,203,'xmlrpc.des.bad_value',{'param'=>'beforedate'})
4108 unless ($req->{'beforedate'} =~
4109 /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/);
4110 my $qd = $dbr->quote($req->{'beforedate'});
4111 $rtime_after = "$LJ::EndOfTime-UNIX_TIMESTAMP($qd)";
4114 $where .= "AND $rtime_what > $rtime_after ";
4115 $orderby = "ORDER BY $rtime_what";
4116 unless ($sort_order eq 'default') {
4117 $orderby .= ' '.uc($sort_order);
4120 unless ($skip) {
4121 $where .= "OR ( journalid=$ownerid $secwhere $where AND jitemid=$sticky_id)" if defined $sticky_id;
4124 elsif ($req->{'selecttype'} eq "one" && ($req->{'itemid'} eq "-1" || $req->{'ditemid'} eq "-1")) {
4125 $use_master = 1; # see note above.
4126 $limit = "LIMIT 1";
4127 $orderby = "ORDER BY rlogtime";
4129 elsif ($req->{'selecttype'} eq "one") {
4130 $req->{'itemid'} = int(($req->{'ditemid'} + 0) / 256) unless($req->{'itemid'});
4131 my $id = $req->{'itemid'} + 0;
4132 $where = "AND jitemid=$id";
4134 elsif ($req->{'selecttype'} eq "syncitems") {
4135 return fail($err, 506) if $LJ::DISABLED{'syncitems'};
4137 my $date = $req->{'lastsync'} || "0000-00-00 00:00:00";
4138 return fail($err, 203, 'xmlrpc.des.bad_value',{'param'=>'syncitems'})
4139 unless ($date =~ /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/);
4141 return fail($err, 301, 'xmlrpc.des.syncitems_unavailable') unless($uowner || $u);
4143 my $now = time();
4145 # broken client loop prevention
4146 # TODO: Just add rate limits here instead of that old stuff
4147 my $u_req = ($u ? $u : $uowner);
4148 if ($req->{'lastsync'}) {
4149 my $pname = "rl_syncitems_getevents_loop";
4150 LJ::load_user_props($u_req, $pname);
4152 # format is: time/date/time/date/time/date/... so split
4153 # it into a hash, then delete pairs that are older than an hour
4154 my %reqs = split(m!/!, $u_req->{$pname});
4156 foreach (grep { $_ < $now - 60*60 } keys %reqs) { delete $reqs{$_}; }
4157 my $count = grep { $_ eq $date } values %reqs;
4158 $reqs{$now} = $date;
4160 if ($count >= 2) {
4161 # 2 prior, plus this one = 3 repeated requests for same synctime.
4162 # their client is busted. (doesn't understand syncitems semantics)
4163 return fail($err,406);
4166 $u_req->set_prop( $pname => join( '/', map { $_ => $reqs{$_} }
4167 sort { $b <=> $a }
4168 keys %reqs ) );
4171 my %item;
4172 $sth = $dbcr->prepare("SELECT jitemid, logtime FROM log2 WHERE ".
4173 "journalid=? and logtime > ? $secwhere");
4174 $sth->execute($ownerid, $date);
4176 while (my ($id, $dt) = $sth->fetchrow_array) {
4177 $item{$id} = $dt;
4180 my $p_revtime = LJ::get_prop("log", "revtime");
4181 $sth = $dbcr->prepare("SELECT jitemid, FROM_UNIXTIME(value) ".
4182 "FROM logprop2 WHERE journalid=? ".
4183 "AND propid=$p_revtime->{'id'} ".
4184 "AND value+0 > UNIX_TIMESTAMP(?)");
4185 $sth->execute($ownerid, $date);
4187 while (my ($id, $dt) = $sth->fetchrow_array) {
4188 $item{$id} = $dt;
4191 my $limit = 100;
4192 my @ids = sort { $item{$a} cmp $item{$b} } keys %item;
4194 if (@ids > $limit) { @ids = @ids[0..$limit-1]; }
4196 my $in = join(',', @ids) || "0";
4197 $where = "AND jitemid IN ($in)";
4199 elsif ($req->{'selecttype'} eq "multiple") {
4200 my @ids;
4201 if($req->{'itemids'}) {
4202 foreach my $num (split(/\s*,\s*/, $req->{'itemids'})) {
4203 return fail($err, 203, 'xmlrpc.des.non_arifmetic', {'param'=>'itemid', 'value'=>$num}) unless $num =~ /^\d+$/;
4204 push @ids, $num;
4206 } elsif ($req->{'ditemids'}) {
4207 foreach my $num (split(/\s*,\s*/, $req->{'ditemids'})) {
4208 return fail($err, 203, 'xmlrpc.des.non_arifmetic', {'param'=>'itemid', 'value'=>$num}) unless $num =~ /^\d+$/;
4209 push @ids, int(($num+0)/256);
4212 my $limit = 100;
4213 return fail($err, 209, 'xmlrpc.des.entries_limit', {'limit'=>$limit}) if @ids > $limit;
4215 my $in = join(',', @ids);
4216 $where = "AND jitemid IN ($in)";
4218 elsif ($req->{'selecttype'} eq 'before') {
4219 my $before = $req->{'before'};
4220 my $itemshow = $req->{'howmany'};
4221 my $itemselect = $itemshow + $skip;
4223 my %item;
4224 $sth = $dbcr->prepare("SELECT jitemid, logtime FROM log2 WHERE ".
4225 "journalid=? AND logtime < ? $secwhere LIMIT $itemselect");
4226 $sth->execute($ownerid, $before);
4228 while (my ($id, $dt) = $sth->fetchrow_array) {
4229 $item{$id} = $dt;
4232 my $p_revtime = LJ::get_prop("log", "revtime");
4234 $sth = $dbcr->prepare("SELECT jitemid, FROM_UNIXTIME(value) ".
4235 "FROM logprop2 WHERE journalid=? ".
4236 "AND propid=$p_revtime->{'id'} ".
4237 "AND value+0 < ? LIMIT $itemselect");
4238 $sth->execute($ownerid, $before);
4240 while (my ($id, $dt) = $sth->fetchrow_array) {
4241 $item{$id} = $dt;
4244 my @ids = sort { $item{$a} cmp $item{$b} } keys %item;
4246 $orderby = "ORDER BY jitemid";
4247 unless ($sort_order eq 'default') {
4248 $orderby .= ' '.uc($sort_order);
4251 if (@ids > $skip) {
4252 @ids = @ids[$skip..(@ids-1)];
4253 @ids = @ids[0..$itemshow-1] if @ids > $itemshow;
4255 else {
4256 @ids = ();
4259 my $in = join(',', @ids) || "0";
4260 $where = "AND jitemid IN ($in)";
4262 elsif ($req->{'selecttype'} eq 'tag') {
4264 my $empty_res = {
4265 skip => $skip,
4266 xc3 => { u => $u },
4267 events => [],
4270 my $howmany = $req->{'howmany'} || 20;
4271 if ($howmany > 50) { $howmany = 50; }
4272 $howmany = $howmany + 0;
4274 $limit = "LIMIT $howmany";
4275 $offset = "OFFSET $skip";
4277 my $rtime_what = $is_community ? "rlogtime" : "revttime";
4278 $orderby = "ORDER BY $rtime_what";
4280 unless ($sort_order eq 'default') {
4281 $orderby .= ' '.uc($sort_order);
4284 my $jitemids;
4286 my ($tagids, $tagnames, $tags, $known_tags) = ([], [], {}, {});
4288 return fail($err,225)
4289 unless LJ::Tags::is_valid_tagstring($req->{'tags'}, $tagnames, { omit_underscore_check => 1 });
4291 $tags = LJ::Tags::get_usertags($uowner, { remote => $u });
4293 return $empty_res unless $tags && %$tags;
4295 while ( my ($tid, $tag) = each %$tags ) {
4296 $known_tags->{LJ::Text->normalize_tag_name($tag->{name})} = $tid;
4299 my $tagmode = lc $req->{'tagmode'};
4301 $tagids = [ map {
4302 my $tid = $known_tags->{LJ::Text->normalize_tag_name($_)};
4303 $tid ?
4304 $tid :
4305 ( $tagmode eq 'and' ? return $empty_res : () );
4306 } @$tagnames ];
4308 return $empty_res unless $tagids && @$tagids;
4310 if ($tagmode eq 'and') {
4312 my $limit = $LJ::TAG_INTERSECTION;
4313 $#{$tagids} = $limit - 1 if @{$tagids} > $limit;
4314 my $in = join(',', map { $_+0 } @{$tagids});
4315 my $sth = $dbcr->prepare("SELECT jitemid, kwid FROM logtagsrecent WHERE journalid = ? AND kwid IN ($in)");
4316 $sth->execute($ownerid);
4318 my %mix;
4319 while (my $row = $sth->fetchrow_arrayref) {
4320 my ($jitemid, $kwid) = @$row;
4321 $mix{$jitemid}++;
4324 my $need = @{$tagids};
4325 foreach my $jitemid (keys %mix) {
4326 delete $mix{$jitemid} if $mix{$jitemid} < $need;
4329 $jitemids = [keys %mix];
4330 } else { # mode: 'or'
4331 # select jitemids uniquely
4332 my $in = join(',', map { $_+0 } @{$tagids});
4333 $jitemids = $dbcr->selectcol_arrayref(qq{
4334 SELECT DISTINCT jitemid FROM logtagsrecent WHERE journalid = ? AND kwid IN ($in)
4335 }, undef, $ownerid);
4338 return $empty_res unless @$jitemids;
4340 $where = " AND jitemid IN (" .
4341 join(',', map { $_ + 0 } @$jitemids) .
4342 ")";
4344 else {
4345 return fail($err,200,'xmlrpc.des.bad_value',{'param'=>'selecttype'});
4348 if (my $posterid = int($req->{'posterid'})) {
4349 $where .= " AND posterid=$posterid";
4352 # common SQL template:
4353 unless ($sql) {
4354 $sql = "SELECT jitemid, eventtime, security, allowmask, anum, posterid, replycount, UNIX_TIMESTAMP(eventtime), logtime ".
4355 "FROM log2 WHERE journalid=$ownerid $secwhere $where $orderby $limit $offset";
4358 # whatever selecttype might have wanted us to use the master db.
4359 $dbcr = LJ::get_cluster_def_reader($uowner) if $use_master;
4361 return fail($err, 502) unless $dbcr;
4363 ## load the log rows
4364 ($sth = $dbcr->prepare($sql))->execute;
4365 return fail($err, 501, $dbcr->errstr) if $dbcr->err;
4367 my $count = 0;
4368 my @itemids = ();
4370 my $res = {
4371 skip => $skip,
4372 xc3 => {
4373 u => $u
4377 my $events = $res->{'events'} = [];
4378 my %evt_from_itemid;
4380 while (my ($itemid, $eventtime, $sec, $mask, $anum, $jposterid, $replycount, $event_timestamp, $logtime) = $sth->fetchrow_array) {
4381 $count++;
4384 # construct LJ::Entry object from row
4386 my $evt = {};
4387 my $entry = LJ::Entry->new_from_row(
4388 'journalid' => $ownerid,
4389 'jitemid' => $itemid,
4390 'allowmask' => $mask,
4391 'posterid' => $jposterid,
4392 'eventtime' => $eventtime,
4393 'security' => $sec,
4394 'anum' => $anum,
4398 # final_ownerid, final_anum and $final_itemid could be different
4399 # from ownerid if entry is a repost
4401 my $final_ownerid = $ownerid;
4402 my $final_itemid = $itemid;
4403 my $final_anum = $anum;
4406 # repost_text and repost_subject are using for repost only
4408 my $repost_text;
4409 my $repost_subject;
4412 # prepare list of variables to substiture values
4414 my $repost_entry;
4415 my $content = { 'original_post_obj' => \$entry,
4416 'repost_obj' => \$repost_entry,
4417 'journalid' => \$final_ownerid,
4418 'itemid' => \$final_itemid,
4419 'allowmask' => \$mask,
4420 'posterid' => \$jposterid,
4421 'eventtime' => \$eventtime,
4422 'security' => \$sec,
4423 'anum' => \$final_anum,
4424 'event' => \$repost_text,
4425 'subject' => \$repost_subject,
4426 'reply_count' => \$replycount, };
4429 # use repost signnture before event text
4431 my $repost_props = { use_repost_signature => 0 };
4433 if (LJ::Entry::Repost->substitute_content( $entry, $content, $repost_props )) {
4434 $evt->{'repost_text'} = $repost_text;
4435 $evt->{'repost_subject'} = $repost_subject;
4436 $evt->{'repost_ownerid'} = $final_ownerid;
4437 $evt->{'repost_itemid'} = $final_itemid;
4438 $evt->{'repost_anum'} = $final_anum;
4439 $evt->{'repost_ditemid'} = $final_itemid * 256 + $final_anum;
4440 $evt->{'repost_props'} = $entry->props;
4441 $evt->{'original_entry_url'} = $entry->url;
4442 $evt->{'repostername'} = $repost_entry->poster->username;
4443 $evt->{'journalname'} = $entry->journal->username if $entry->journal;
4444 if ($entry->poster) {
4445 $evt->{'postername'} = $entry->poster->username;
4446 my $userpic = $entry->userpic;
4447 $evt->{'poster_userpic_url'} = $userpic && $userpic->url;
4451 # now my own post, so need to check for suspended prop
4452 if ($jposterid != $posterid) {
4453 next if($entry->is_suspended_for($u));
4456 $evt->{'itemid'} = $itemid;
4457 push @itemids, $itemid;
4459 $evt_from_itemid{$itemid} = $evt;
4461 $evt->{"eventtime"} = $eventtime;
4462 $evt->{"event_timestamp"} = $event_timestamp;
4463 $evt->{"logtime"} = $logtime;
4466 if ($sec ne "public") {
4467 $evt->{'security'} = $sec;
4468 $evt->{'allowmask'} = $mask if $sec eq "usemask";
4471 $evt->{'anum'} = $anum;
4472 $evt->{'ditemid'} = $itemid * 256 + $anum;
4474 if ($jposterid != $final_ownerid) {
4475 my $uposter = LJ::load_userid($jposterid);
4476 $evt->{'poster'} = $uposter->username;
4478 if ($uposter->identity) {
4479 my $i = $uposter->identity;
4480 $evt->{'identity_type'} = $i->pretty_type;
4481 $evt->{'identity_value'} = $i->value;
4482 $evt->{'identity_url'} = $i->url($uposter);
4483 $evt->{'identity_display'} = $uposter->display_name;
4488 # There is using final_ variabled to get correct link
4490 $evt->{'url'} = LJ::item_link(LJ::load_userid($final_ownerid),
4491 $final_itemid,
4492 $final_anum);
4494 $evt->{'reply_count'} = $replycount;
4496 $evt->{'can_comment'} = $u ? $entry->remote_can_comment($u) : $entry->everyone_can_comment;
4498 if ( $itemid == $sticky_id && $req->{'selecttype'} eq "lastn") {
4499 unshift @$events, $evt,
4501 else {
4502 push @$events, $evt;
4506 # load properties. Even if the caller doesn't want them, we need
4507 # them in Unicode installations to recognize older 8bit non-UF-8
4508 # entries.
4509 unless ($req->{'noprops'} && !$LJ::UNICODE) {
4510 ### do the properties now
4511 $count = 0;
4512 my %props = ();
4513 LJ::load_log_props2($dbcr, $ownerid, \@itemids, \%props);
4515 # load the tags for these entries, unless told not to
4516 unless ($req->{notags}) {
4517 # construct %idsbycluster for the multi call to get these tags
4518 my $tags = LJ::Tags::get_logtags($uowner, \@itemids);
4520 # add to props
4521 foreach my $itemid (@itemids) {
4522 next unless $tags->{$itemid};
4523 $props{$itemid}->{taglist} = join(', ', values %{$tags->{$itemid}});
4527 foreach my $itemid (keys %props) {
4528 # 'replycount' is a pseudo-prop, don't send it.
4529 # FIXME: this goes away after we restructure APIs and
4530 # replycounts cease being transferred in props
4531 delete $props{$itemid}->{'replycount'};
4533 unless ($flags->{noauth}) {
4534 delete $props{$itemid}->{repost_offer};
4537 my $evt = $evt_from_itemid{$itemid};
4538 $evt->{'props'} = {};
4540 foreach my $name (keys %{$props{$itemid}}) {
4542 my $value = $props{$itemid}->{$name};
4543 $value =~ s/\n/ /g;
4545 # normalize props
4546 unless ($flags->{'noauth'}) {
4547 my $prop = LJ::get_prop("log", $name);
4548 my $ptype = $prop->{'datatype'};
4550 if ($ptype eq "bool" && $value !~ /^[01]$/) {
4551 $value = $value ? 1 : 0;
4553 if ($ptype eq "num" && $value =~ /[^\d]/) {
4554 $value = int $value;
4558 $evt->{'props'}->{$name} = $value;
4561 if ( $itemid == $sticky_id ) {
4562 $evt->{'props'}->{'sticky'} = 1;
4567 ## load the text
4568 my $text = LJ::cond_no_cache($use_master, sub {
4569 return LJ::get_logtext2($uowner, @itemids);
4572 foreach my $i (@itemids) {
4573 my $t = $text->{$i};
4574 my $evt = $evt_from_itemid{$i};
4576 my $real_uowner = $uowner;
4578 if ($evt->{'repost_text'}) {
4579 $t->[0] = delete $evt->{'repost_subject'};
4580 $t->[1] = delete $evt->{'repost_text'};
4582 $evt->{'props'} = delete $evt->{'repost_props'}
4583 unless $req->{'noprops'};
4585 delete $evt->{'props'}{'repost_offer'} if $evt->{'props'};
4587 $evt->{'itemid'} = delete $evt->{'repost_itemid'};
4588 $evt->{'anum'} = delete $evt->{'repost_anum'};
4589 $evt->{'ownerid'} = delete $evt->{'repost_ownerid'};
4590 $evt->{'repost'} = 1;
4592 $real_uowner = LJ::want_user($evt->{'ownerid'});
4596 # if they want subjects to be events, replace event
4597 # with subject when requested.
4598 if ($req->{'prefersubject'} && length($t->[0])) {
4599 $t->[1] = $t->[0]; # event = subject
4600 $t->[0] = undef; # subject = undef
4603 # now that we have the subject, the event and the props,
4604 # auto-translate them to UTF-8 if they're not in UTF-8.
4605 if ($LJ::UNICODE && $req->{'ver'} >= 1 &&
4606 $evt->{'props'}->{'unknown8bit'}) {
4607 my $error = 0;
4608 $t->[0] = LJ::text_convert($t->[0], $real_uowner, \$error);
4609 $t->[1] = LJ::text_convert($t->[1], $real_uowner, \$error);
4611 foreach (keys %{$evt->{'props'}}) {
4612 $evt->{'props'}->{$_} = LJ::text_convert($evt->{'props'}->{$_}, $real_uowner, \$error);
4615 return fail($err, 208, 'xmlrpc.des.cannnot_display_post',{'siteroot'=>$LJ::SITEROOT})
4616 if $error;
4619 if ($LJ::UNICODE && $req->{'ver'} < 1 && !$evt->{'props'}->{'unknown8bit'}) {
4620 unless ( LJ::is_ascii($t->[0]) &&
4621 LJ::is_ascii($t->[1]) &&
4622 LJ::is_ascii(join(' ', values %{$evt->{'props'}}) )) {
4623 # we want to fail the client that wants to get this entry
4624 # but we make an exception for selecttype=day, in order to allow at least
4625 # viewing the daily summary
4627 if ($req->{'selecttype'} eq 'day') {
4628 $t->[0] = $t->[1] = $CannotBeShown;
4630 else {
4631 return fail($err, 207, 'xmlrpc.des.not_unicode_client', {'siteroot'=>$LJ::SITEROOT});
4636 if ($t->[0]) {
4637 $t->[0] =~ s/[\r\n]/ /g;
4638 $evt->{'subject'} = $t->[0];
4641 $t->[1] = LJ::trim_widgets(
4642 'length' => $req->{trim_widgets},
4643 'img_length' => $req->{widgets_img_length},
4644 'text' => $t->[1],
4645 'read_more' => '<a href="' . $evt->{url} . '"> ...</a>',
4646 ) if $req->{trim_widgets};
4648 LJ::EmbedModule->expand_entry($real_uowner, \$t->[1], get_video_id => 1) if($req->{get_video_ids});
4649 LJ::Poll->expand_entry(\$t->[1], getpolls => 1, viewer => $u) if $req->{get_polls};
4651 if ($req->{view}) {
4652 LJ::EmbedModule->expand_entry($real_uowner, \$t->[1], edit => 1) if $req->{view} eq 'stored';
4654 elsif ($req->{parseljtags}) {
4655 $t->[1] = LJ::convert_lj_tags_to_links(
4656 event => $t->[1],
4657 embed_url => $evt->{url});
4661 # truncate
4662 if ($req->{'truncate'} >= 4) {
4663 my $original = $t->[1];
4665 if ($req->{'ver'} > 1) {
4666 $t->[1] = LJ::text_trim($t->[1], $req->{'truncate'} - 3, 0);
4668 else {
4669 $t->[1] = LJ::text_trim($t->[1], 0, $req->{'truncate'} - 3);
4672 # only append the elipsis if the text was actually truncated
4673 $t->[1] .= "..." if $t->[1] ne $original;
4676 # line endings
4677 $t->[1] =~ s/\r//g;
4679 if ($req->{'asxml'}) {
4680 my $tidy = LJ::Tidy->new();
4681 $evt->{'subject'} = $tidy->clean( $evt->{'subject'} );
4682 $t->[1] = $tidy->clean( $t->[1] );
4685 if ($req->{'lineendings'} eq "unix") {
4686 # do nothing. native format.
4688 elsif ($req->{'lineendings'} eq "mac") {
4689 $t->[1] =~ s/\n/\r/g;
4691 elsif ($req->{'lineendings'} eq "space") {
4692 $t->[1] =~ s/\n/ /g;
4694 elsif ($req->{'lineendings'} eq "dots") {
4695 $t->[1] =~ s/\n/ ... /g;
4697 else { # "pc" -- default
4698 $t->[1] =~ s/\n/\r\n/g;
4701 $evt->{'event'} = $t->[1];
4704 # maybe we don't need the props after all
4705 if ($req->{'noprops'}) {
4706 foreach(@$events) { delete $_->{'props'}; }
4709 return $res;
4712 sub createrepost {
4713 my ($req, $err, $flags) = @_;
4714 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'createrepost');
4716 my $u = $flags->{'u'};
4718 my $timezone = $req->{'tz'} || 'guess';
4719 unless ($timezone eq 'guess' ||
4720 $timezone =~ /^[+\-]\d\d\d\d$/) {
4721 return fail($err, 203, 'xmlrpc.des.bad_value', {'param'=>'tz'});
4724 my $url = $req->{'url'} || return fail($err,200,"url");
4725 my $entry = LJ::Entry->new_from_url($url);
4727 return fail($err, 203, 'url') unless $entry && $entry->valid;
4728 return fail($err, 227) unless $entry->visible_to($u);
4730 my $result = LJ::Entry::Repost->create(
4731 'journalu' => $u,
4732 'source_entry' => $entry,
4733 'timezone' => $timezone,
4736 if ( my $error = $result->{error} ) {
4737 return fail($err, 228, $error->{error_message});
4740 $result->{result}{status} = 'OK';
4742 return $result->{result};
4745 sub deleterepost {
4746 my ($req, $err, $flags) = @_;
4747 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'deleterepost');
4749 my $u = $flags->{'u'};
4751 my $url = $req->{'url'} || return fail($err,200,"url");
4752 my $entry = LJ::Entry->new_from_url($url);
4754 return fail($err, 203, 'url') unless $entry && $entry->valid;
4756 my $result = LJ::Entry::Repost->delete( $u, # destination journal
4757 $entry,); # entry to be reposted
4759 if ( my $error = $result->{error} ) {
4760 return fail($err, 229, $error->{error_message});
4763 $result->{status} = 'OK';
4765 return $result;
4768 sub getrepoststatus {
4769 my ($req, $err, $flags) = @_;
4771 $flags->{allow_anonymous} = 1;
4772 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getevents');
4774 my $u = $flags->{'u'};
4776 my $url = $req->{'url'} || return fail($err,200,"url");
4777 my $entry = LJ::Entry->new_from_url($url);
4779 return fail($err, 203, 'url') unless $entry && $entry->valid;
4780 return fail($err, 227) unless $entry->visible_to($u);
4782 my $result = LJ::Entry::Repost->get_status($entry, $u);
4784 $result->{status} = 'OK';
4786 return $result;
4789 sub editfriends
4791 my ($req, $err, $flags) = @_;
4792 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'editfriends');
4794 my $u = $flags->{'u'};
4795 my $userid = $u->{'userid'};
4796 my $dbh = LJ::get_db_writer();
4797 my $sth;
4799 return fail($err,306) unless $dbh;
4801 # do not let locked people do this
4802 return fail($err, 308) if $u->{statusvis} eq 'L';
4805 # Do not have values for $LJ::ADD_FRIEND_RATE_LIMIT
4807 # # check action frequency
4808 # unless ($flags->{no_rate_check}){
4809 # my $cond = ["ratecheck:add_friend:$userid",
4810 # [ $LJ::ADD_FRIEND_RATE_LIMIT || [ 10, 600 ] ]
4811 # ];
4812 # return fail($err, 411)
4813 # unless LJ::RateLimit->check($u, [ $cond ]);
4816 my $res = {
4817 xc3 => {
4818 u => $u
4822 ## first, figure out who the current friends are to save us work later
4823 my %curfriend;
4824 my $friend_count = 0;
4825 my $friends_changed = 0;
4827 # TAG:FR:protocol:editfriends1
4828 $sth = $dbh->prepare("SELECT u.user FROM useridmap u, friends f ".
4829 "WHERE u.userid=f.friendid AND f.userid=$userid");
4830 $sth->execute;
4831 while (my ($friend) = $sth->fetchrow_array) {
4832 $curfriend{$friend} = 1;
4833 $friend_count++;
4835 $sth->finish;
4837 # perform the deletions
4838 DELETEFRIEND:
4839 foreach (@{$req->{'delete'}})
4841 my $deluser = LJ::canonical_username($_);
4842 next DELETEFRIEND unless ($curfriend{$deluser});
4844 my $friendid = LJ::get_userid($deluser);
4845 # TAG:FR:protocol:editfriends2_del
4846 LJ::remove_friend($userid, $friendid);
4847 $friend_count--;
4848 $friends_changed = 1;
4851 my $error_flag = 0;
4852 my $friends_added = 0;
4853 my $fail = sub {
4854 LJ::memcache_kill($userid, "friends");
4855 LJ::mark_dirty($userid, "friends");
4856 return fail($err, $_[0], $_[1]);
4859 # only people, shared journals, and owned syn feeds can add friends
4860 return $fail->(104, 'xmlrpc.des.friends_add_not_allowed')
4861 unless ($u->{'journaltype'} eq 'P' ||
4862 $u->{'journaltype'} eq 'S' ||
4863 $u->{'journaltype'} eq 'I' ||
4864 ($u->{'journaltype'} eq "Y" && $u->password));
4866 # Don't let suspended users add friend
4867 return $fail->(305, 'xmlrpc.des.suspended_add_friend')
4868 if ($u->is_suspended);
4870 my $sclient = LJ::theschwartz();
4872 # perform the adds
4873 ADDFRIEND:
4874 foreach my $fa (@{$req->{'add'}})
4876 unless (ref $fa eq "HASH") {
4877 $fa = { 'username' => $fa };
4880 my $aname = LJ::canonical_username($fa->{'username'});
4881 unless ($aname) {
4882 $error_flag = 1;
4883 next ADDFRIEND;
4886 $friend_count++ unless $curfriend{$aname};
4888 my $err;
4889 return $fail->(104, "$err")
4890 unless $u->can_add_friends(\$err, { 'numfriends' => $friend_count, friend => $fa });
4892 my $fg = $fa->{'fgcolor'} || "#000000";
4893 my $bg = $fa->{'bgcolor'} || "#FFFFFF";
4894 if ($fg !~ /^\#[0-9A-F]{6,6}$/i || $bg !~ /^\#[0-9A-F]{6,6}$/i) {
4895 return $fail->(203, 'xmlrpc.des.bad_value',{'param'=>'color'});
4898 my $row = LJ::load_user($aname);
4899 my $currently_is_friend = LJ::is_friend($u, $row);
4900 my $currently_is_banned = LJ::is_banned($u, $row);
4902 # XXX - on some errors we fail out, on others we continue and try adding
4903 # any other users in the request. also, error message for redirect should
4904 # point the user to the redirected username.
4905 if (! $row) {
4906 $error_flag = 1;
4907 } elsif ($row->{'journaltype'} eq "R") {
4908 return $fail->(154);
4909 } elsif ($row->{'statusvis'} ne "V") {
4910 $error_flag = 1;
4911 } else {
4912 $friends_added++;
4913 my $added = { 'username' => $aname,
4914 'fullname' => $row->{'name'},
4915 'journaltype' => $row->{journaltype},
4916 'defaultpicurl' => ($row->{'defaultpicid'} && "$LJ::USERPIC_ROOT/$row->{'defaultpicid'}/$row->{'userid'}"),
4917 'fgcolor' => $fg,
4918 'bgcolor' => $bg,
4920 if ($req->{'ver'} >= 1) {
4921 LJ::text_out(\$added->{'fullname'});
4924 if ($row->identity) {
4925 my $i = $row->identity;
4926 $added->{'identity_type'} = $i->pretty_type;
4927 $added->{'identity_value'} = $i->value;
4928 $added->{'identity_url'} = $i->url($row);
4929 $added->{'identity_display'} = $row->display_name;
4931 $added->{"type"} = {
4932 'C' => 'community',
4933 'Y' => 'syndicated',
4934 'N' => 'news',
4935 'S' => 'shared',
4936 'I' => 'identity',
4937 }->{$row->{'journaltype'}} if $row->{'journaltype'} ne 'P';
4939 my $qfg = LJ::color_todb($fg);
4940 my $qbg = LJ::color_todb($bg);
4942 my $friendid = $row->{'userid'};
4944 my $gmask = $fa->{'groupmask'};
4945 if (! $gmask && $curfriend{$aname}) {
4946 # if no group mask sent, use the existing one if this is an existing friend
4947 # TAG:FR:protocol:editfriends3_getmask
4948 my $sth = $dbh->prepare("SELECT groupmask FROM friends ".
4949 "WHERE userid=$userid AND friendid=$friendid");
4950 $sth->execute;
4951 $gmask = $sth->fetchrow_array;
4953 # force bit 0 on.
4954 $gmask |= 1;
4956 $added->{groupmask} = $gmask;
4957 push @{$res->{'added'}}, $added;
4959 # TAG:FR:protocol:editfriends4_addeditfriend
4960 my $cnt = $dbh->do("REPLACE INTO friends (userid, friendid, fgcolor, bgcolor, groupmask) ".
4961 "VALUES ($userid, $friendid, $qfg, $qbg, $gmask)");
4962 return $fail->(501,$dbh->errstr) if $dbh->err;
4964 if ($cnt == 1) {
4965 LJ::run_hooks('befriended', LJ::load_userid($userid), LJ::load_userid($friendid));
4966 LJ::User->increase_friendsof_counter($friendid);
4969 my $memkey = [$userid,"frgmask:$userid:$friendid"];
4970 LJ::MemCacheProxy::set($memkey, $gmask+0, time()+60*15);
4971 LJ::memcache_kill($friendid, 'friendofs');
4972 LJ::memcache_kill($friendid, 'friendofs2');
4974 if ($sclient && !$currently_is_friend && !$currently_is_banned) {
4976 # For Profile
4977 my $friender = LJ::load_userid($userid);
4978 my $friendee = LJ::load_userid($friendid);
4980 $friender->clear_cache_friends($friendee);
4982 ## delay event to accumulate users activity
4983 require LJ::Event::BefriendedDelayed;
4984 LJ::Event::BefriendedDelayed->send(
4985 $friendee, ## to user
4986 $friender ## from user
4988 my @jobs;
4989 push @jobs, TheSchwartz::Job->new(
4990 funcname => "LJ::Worker::FriendChange",
4991 arg => [$userid, 'add', $friendid],
4992 ) unless $LJ::DISABLED{'friendchange-schwartz'};
4994 $sclient->insert_jobs(@jobs) if @jobs;
4996 $friends_changed = 1;
5000 return $fail->(104) if $error_flag;
5002 # invalidate memcache of friends
5003 LJ::memcache_kill($userid, "friends");
5004 LJ::memcache_kill($userid, "friends2");
5005 LJ::mark_dirty($userid, "friends");
5007 LJ::run_hooks('friends_changed', LJ::load_userid($userid)) if $friends_changed;
5009 return $res;
5012 sub editfriendgroups
5014 my ($req, $err, $flags) = @_;
5015 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'editfriendgroups');
5017 my $u = $flags->{'u'};
5018 my $userid = $u->{'userid'};
5019 my ($db, $fgtable, $bmax, $cmax) = $u->{dversion} > 5 ?
5020 ($u->writer, 'friendgroup2', LJ::BMAX_GRPNAME2, LJ::CMAX_GRPNAME2) :
5021 (LJ::get_db_writer(), 'friendgroup', LJ::BMAX_GRPNAME, LJ::CMAX_GRPNAME);
5022 my $sth;
5024 return fail($err,306) unless $db;
5026 # do not let locked people do this
5027 return fail($err, 308) if $u->{statusvis} eq 'L';
5029 my $res = {};
5031 ## make sure tree is how we want it
5032 $req->{'groupmasks'} = {} unless
5033 (ref $req->{'groupmasks'} eq "HASH");
5034 $req->{'set'} = {} unless
5035 (ref $req->{'set'} eq "HASH");
5036 $req->{'delete'} = [] unless
5037 (ref $req->{'delete'} eq "ARRAY");
5039 # Keep track of what bits are already set, so we can know later
5040 # whether to INSERT or UPDATE.
5041 my %bitset;
5042 my $groups = LJ::get_friend_group($userid);
5043 foreach my $bit (keys %{$groups || {}}) {
5044 $bitset{$bit} = 1;
5047 ## before we perform any DB operations, validate input text
5048 # (groups' names) for correctness so we can fail gracefully
5049 if ($LJ::UNICODE) {
5050 foreach my $bit (keys %{$req->{'set'}})
5052 my $name = $req->{'set'}->{$bit}->{'name'};
5053 return fail($err,207,'xmlrpc.des.not_ascii')
5054 if $req->{'ver'} < 1 and not LJ::is_ascii($name);
5055 return fail($err,208,'xmlrpc.des.invalid_group',{'siteroot'=>$LJ::SITEROOT})
5056 unless LJ::text_in($name);
5060 ## figure out deletions we'll do later
5061 foreach my $bit (@{$req->{'delete'}})
5063 $bit += 0;
5064 next unless ($bit >= 1 && $bit <= 30);
5065 $bitset{$bit} = 0; # so later we replace into, not update.
5068 ## do additions/modifications ('set' hash)
5069 my %added;
5070 foreach my $bit (keys %{$req->{'set'}})
5072 $bit += 0;
5073 next unless ($bit >= 1 && $bit <= 30);
5074 my $sa = $req->{'set'}->{$bit};
5075 my $name = LJ::text_trim($sa->{'name'}, $bmax, $cmax);
5077 # can't end with a slash
5078 $name =~ s!/$!!;
5080 # setting it to name is like deleting it.
5081 unless ($name =~ /\S/) {
5082 push @{$req->{'delete'}}, $bit;
5083 next;
5086 my $qname = $db->quote($name);
5087 my $qsort = defined $sa->{'sort'} ? ($sa->{'sort'}+0) : 50;
5088 my $qpublic = $db->quote(defined $sa->{'public'} ? ($sa->{'public'}+0) : 0);
5090 if ($bitset{$bit}) {
5091 # so update it
5092 my $sets;
5093 if (defined $sa->{'public'}) {
5094 $sets .= ", is_public=$qpublic";
5096 $db->do("UPDATE $fgtable SET groupname=$qname, sortorder=$qsort ".
5097 "$sets WHERE userid=$userid AND groupnum=$bit");
5098 } else {
5099 $db->do("REPLACE INTO $fgtable (userid, groupnum, ".
5100 "groupname, sortorder, is_public) VALUES ".
5101 "($userid, $bit, $qname, $qsort, $qpublic)");
5103 $added{$bit} = 1;
5107 ## do deletions ('delete' array)
5108 my $dbcm = LJ::get_cluster_master($u);
5110 # ignore bits that aren't integers or that are outside 1-30 range
5111 my @delete_bits = grep {$_ >= 1 and $_ <= 30} map {$_+0} @{$req->{'delete'}};
5112 my $delete_mask = 0;
5113 foreach my $bit (@delete_bits) {
5114 $delete_mask |= (1 << $bit)
5117 # remove the bits for deleted groups from all friends groupmasks
5118 my $dbh = LJ::get_db_writer();
5119 if ($delete_mask) {
5120 # TAG:FR:protocol:editfriendgroups_removemasks
5121 $dbh->do("UPDATE friends".
5122 " SET groupmask = groupmask & ~$delete_mask".
5123 " WHERE userid = $userid");
5126 foreach my $bit (@delete_bits)
5128 # remove all posts from allowing that group:
5129 my @posts_to_clean = ();
5130 $sth = $dbcm->prepare("SELECT jitemid FROM logsec2 WHERE journalid=$userid AND allowmask & (1 << $bit)");
5131 $sth->execute;
5132 while (my ($id) = $sth->fetchrow_array) { push @posts_to_clean, $id; }
5133 while (@posts_to_clean) {
5134 my @batch;
5135 if (scalar(@posts_to_clean) < 20) {
5136 @batch = @posts_to_clean;
5137 @posts_to_clean = ();
5138 } else {
5139 @batch = splice(@posts_to_clean, 0, 20);
5142 my $in = join(",", @batch);
5143 LJ::run_hooks('report_entry_update', $userid, \@batch);
5144 $u->do("UPDATE log2 SET allowmask=allowmask & ~(1 << $bit) ".
5145 "WHERE journalid=$userid AND jitemid IN ($in) AND security='usemask'");
5146 $u->do("UPDATE logsec2 SET allowmask=allowmask & ~(1 << $bit) ".
5147 "WHERE journalid=$userid AND jitemid IN ($in)");
5149 foreach my $id (@batch) {
5150 LJ::MemCache::delete([$userid, "log2:$userid:$id"]);
5152 LJ::MemCache::delete([$userid, "log2lt:$userid"]);
5154 LJ::Tags::deleted_friend_group($u, $bit);
5156 LJ::load_user_props($u, 'pp_transallow');
5158 $u->set_prop( 'pp_transallow' => -1 )
5159 if $bit == $u->{pp_transallow};
5161 # remove the friend group, unless we just added it this transaction
5162 unless ($added{$bit}) {
5163 $db->do("DELETE FROM $fgtable WHERE ".
5164 "userid=$userid AND groupnum=$bit");
5168 ## change friends' masks
5169 # TAG:FR:protocol:editfriendgroups_changemasks
5170 foreach my $friend (keys %{$req->{'groupmasks'}})
5172 my $mask = int($req->{'groupmasks'}->{$friend}) | 1;
5173 my $friendid = LJ::get_userid($friend);
5175 $dbh->do("UPDATE friends SET groupmask=$mask ".
5176 "WHERE userid=$userid AND friendid=?",
5177 undef, $friendid);
5178 LJ::MemCacheProxy::set([$userid, "frgmask:$userid:$friendid"], $mask);
5181 # invalidate memcache of friends/groups
5182 LJ::memcache_kill($userid, "friends");
5183 LJ::memcache_kill($userid, "fgrp");
5184 LJ::mark_dirty($u, "friends");
5186 # return value for this is nothing.
5187 return {
5188 xc3 => {
5189 u => $u
5194 sub sessionexpire {
5195 my ($req, $err, $flags) = @_;
5196 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'sessionexpire');
5197 my $u = $flags->{u};
5198 my $res = {
5199 xc3 => {
5200 u => $u
5204 # expunge one? or all?
5205 if ($req->{expireall}) {
5206 $u->kill_all_sessions;
5207 return $res;
5210 # just expire a list
5211 my $list = $req->{expire} || [];
5213 return $res unless @$list;
5215 return fail($err,502) unless $u->writer;
5216 $u->kill_sessions(@$list);
5218 return $res;
5221 sub sessiongenerate {
5222 # generate a session
5223 my ($req, $err, $flags) = @_;
5224 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'sessiongenerate');
5226 # sanitize input
5227 $req->{expiration} = 'short' unless $req->{expiration} eq 'long';
5228 my $boundip;
5229 $boundip = LJ::get_remote_ip() if $req->{bindtoip};
5231 my $u = $flags->{u};
5232 my $sess_opts = {
5233 exptype => $req->{expiration},
5234 ipfixed => $boundip,
5237 # do not let locked people do this
5238 return fail($err, 308) if $u->{statusvis} eq 'L';
5240 my $sess = LJ::Session->create($u, %$sess_opts);
5242 # return our hash
5243 return {
5244 ljsession => $sess->master_cookie_string,
5245 xc3 => {
5246 u => $u
5251 sub list_friends {
5252 my ($u, $opts) = @_;
5254 # do not show people in here
5255 my %hide; # userid -> 1
5257 # TAG:FR:protocol:list_friends
5258 my $sql;
5260 unless ($opts->{'friendof'}) {
5261 $sql = "SELECT friendid, fgcolor, bgcolor, groupmask FROM friends WHERE userid=?";
5263 else {
5264 $sql = "SELECT userid FROM friends WHERE friendid=?";
5266 if (my $list = LJ::load_rel_user($u, 'B')) {
5267 $hide{$_} = 1 foreach @$list;
5271 my $dbr = LJ::get_db_reader();
5272 my $sth = $dbr->prepare($sql);
5273 $sth->execute($u->{'userid'});
5275 my @frow;
5277 while (my @row = $sth->fetchrow_array) {
5278 next if $hide{$row[0]};
5279 push @frow, [ @row ];
5282 my $us = LJ::load_userids(map { $_->[0] } @frow);
5283 my $limitnum = $opts->{'limit'}+0;
5285 my $res = [];
5287 foreach my $f (sort { $us->{$a->[0]}{'user'} cmp $us->{$b->[0]}{'user'} }
5288 grep { $us->{$_->[0]} } @frow)
5290 my $u = $us->{$f->[0]};
5291 next if $opts->{'friendof'} && $u->{'statusvis'} ne 'V';
5293 my $r = {
5294 'username' => $u->{'user'},
5295 'fullname' => $u->{'name'},
5299 if ($u->identity) {
5300 my $i = $u->identity;
5301 $r->{'identity_type'} = $i->pretty_type;
5302 $r->{'identity_value'} = $i->value;
5303 $r->{'identity_url'} = $i->url($u);
5304 $r->{'identity_display'} = $u->display_name;
5307 if ($opts->{'includebdays'} &&
5308 $u->{'bdate'} &&
5309 $u->{'bdate'} ne "0000-00-00" &&
5310 $u->can_show_full_bday)
5312 $r->{'birthday'} = $u->{'bdate'};
5315 unless ($opts->{'friendof'}) {
5316 $r->{'fgcolor'} = LJ::color_fromdb($f->[1]);
5317 $r->{'bgcolor'} = LJ::color_fromdb($f->[2]);
5318 $r->{'groupmask'} = $f->[3] if $f->[3] != 1;
5320 else {
5321 $r->{'fgcolor'} = "#000000";
5322 $r->{'bgcolor'} = "#ffffff";
5325 $r->{"type"} = {
5326 'C' => 'community',
5327 'Y' => 'syndicated',
5328 'N' => 'news',
5329 'S' => 'shared',
5330 'I' => 'identity',
5331 }->{$u->{'journaltype'}} if $u->{'journaltype'} ne 'P';
5333 $r->{"status"} = {
5334 'D' => "deleted",
5335 'S' => "suspended",
5336 'X' => "purged",
5337 }->{$u->{'statusvis'}} if $u->{'statusvis'} ne 'V';
5339 $r->{defaultpicurl} = "$LJ::USERPIC_ROOT/$u->{'defaultpicid'}/$u->{'userid'}" if $u->{'defaultpicid'};
5341 push @$res, $r;
5343 # won't happen for zero limit (which means no limit)
5344 last if @$res == $limitnum;
5347 return $res;
5350 sub syncitems {
5351 my ($req, $err, $flags) = @_;
5352 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'syncitems');
5353 return undef unless check_altusage($req, $err, $flags);
5354 return fail($err, 506) if $LJ::DISABLED{'syncitems'};
5356 my $ownerid = $flags->{'ownerid'};
5357 my $uowner = $flags->{'u_owner'} || $flags->{'u'};
5358 my $sth;
5360 my $db = LJ::get_cluster_reader($uowner);
5361 return fail($err, 502) unless $db;
5363 ## have a valid date?
5364 my $date = $req->{'lastsync'};
5366 if ($date) {
5367 return fail($err, 203, 'xmlrpc.des.bad_value', {'param'=>'date'})
5368 unless ($date =~ /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/);
5369 } else {
5370 $date = "0000-00-00 00:00:00";
5373 my $LIMIT = 500;
5374 my $type = $req->{type} || 'posted';
5375 my ( $table, $idfield ) = ( '', 'jitemid');
5377 my $external_ids = $req->{'use_external_ids'};
5379 if ( $req->{ver} > 3 && LJ::is_enabled("delayed_entries") ) {
5380 if ( $type eq 'posted' ) {
5381 $table = '';
5382 $idfield = 'jitemid';
5384 elsif ( $type eq 'delayed' ) {
5385 $table = 'delayed';
5386 $idfield = 'delayedid';
5387 } else {
5388 return fail( $err, 216 );
5392 my %item;
5393 $sth = $db->prepare("SELECT ${idfield}, logtime FROM ${table}log2 WHERE ".
5394 "journalid=? and logtime > ?");
5395 $sth->execute($ownerid, $date);
5396 while (my ($id, $dt, $anum) = $sth->fetchrow_array) {
5397 $item{$id} = [ 'L', $id, $dt, "create", $anum ];
5400 my %cmt;
5402 unless ( $type eq 'delayed' ) {
5403 my $p_calter = LJ::get_prop("log", "commentalter");
5404 my $p_revtime = LJ::get_prop("log", "revtime");
5405 $sth = $db->prepare("SELECT jitemid, propid, FROM_UNIXTIME(value) ".
5406 "FROM logprop2 WHERE journalid=? ".
5407 "AND propid IN ($p_calter->{'id'}, $p_revtime->{'id'}) ".
5408 "AND value+0 > UNIX_TIMESTAMP(?)");
5410 $sth->execute($ownerid, $date);
5411 while (my ($id, $prop, $dt) = $sth->fetchrow_array) {
5412 my $entry = LJ::Entry->new($ownerid, jitemid => $id);
5414 ## sometimes there is no row in log2 table, while there are rows in logprop2
5415 ## it's either corrupted db (replication/failover problem) or lazy/slow deletion of an entry
5416 ## calling $entry->anum on such an entry is a fatal error
5417 next unless $entry && $entry->valid;
5419 if ($prop == $p_calter->{'id'}) {
5420 $cmt{$id} = [ 'C', $id, $dt, "update", $entry->anum ];
5421 } elsif ($prop == $p_revtime->{'id'}) {
5422 $item{$id} = [ 'L', $id, $dt, "update", $entry->anum ];
5426 my @ev = sort { $a->[2] cmp $b->[2] } (values %item, values %cmt);
5428 my $res = {
5429 xc3 => {
5430 u => $flags->{'u'}
5434 my $list = $res->{'syncitems'} = [];
5435 $res->{'total'} = scalar @ev;
5436 my $ct = 0;
5438 while (my $ev = shift @ev) {
5439 $ct++;
5440 push @$list, {
5441 'item' => "$ev->[0]-$ev->[1]",
5442 'time' => $ev->[2],
5443 'action' => $ev->[3],
5444 ( $external_ids ? (ditemid => $ev->[1]*256 + $ev->[4]) : () )
5446 last if $ct >= $LIMIT;
5449 $res->{'count'} = $ct;
5451 return $res;
5454 sub consolecommand {
5455 my ($req, $err, $flags) = @_;
5457 # logging in isn't necessary, but most console commands do require it
5458 LJ::set_remote($flags->{'u'}) if authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'consolecommand');
5460 my $res = {
5461 xc3 => {
5462 u => $flags->{'u'}
5465 my $cmdout = $res->{'results'} = [];
5467 require LJ::Console;
5468 foreach my $cmd (@{$req->{'commands'}}) {
5469 # callee can pre-parse the args, or we can do it bash-style
5470 my @args = ref $cmd eq "ARRAY" ? @$cmd
5471 : LJ::Console->parse_line($cmd);
5472 my $c = LJ::Console->parse_array(@args);
5473 my $rv = $c->execute_safely;
5475 my @output;
5476 push @output, [$_->status, $_->text] foreach $c->responses;
5478 push @{$cmdout}, {
5479 'success' => $rv,
5480 'output' => \@output,
5484 return $res;
5487 sub getchallenge
5489 my ($req, $err, $flags) = @_;
5490 my $res = {};
5491 my $now = time();
5492 my $etime = 60;
5493 return {
5494 challenge => LJ::challenge_generate($etime),
5495 server_time => $now,
5496 expire_time => $now + $etime,
5497 auth_scheme => "c0", # fixed for now, might support others later
5498 xc3 => {}
5502 sub login_message
5504 my ($req, $res, $flags) = @_;
5505 my $u = $flags->{'u'};
5507 my $msg = sub {
5508 my $code = shift;
5509 my $args = shift || {};
5510 $args->{'sitename'} = $LJ::SITENAME;
5511 $args->{'siteroot'} = $LJ::SITEROOT;
5512 my $pre = delete $args->{'pre'};
5513 $res->{'message'} = $pre . translate($u, $code, $args);
5516 return $msg->("readonly") if LJ::get_cap($u, "readonly");
5517 return $msg->("not_validated") if ($u->{'status'} eq "N" and not $LJ::EVERYONE_VALID);
5518 return $msg->("must_revalidate") if ($u->{'status'} eq "T" and not $LJ::EVERYONE_VALID);
5520 return $msg->("old_win32_client") if $req->{'clientversion'} =~ /^Win32-MFC\/(1.2.[0123456])$/;
5521 return $msg->("old_win32_client") if $req->{'clientversion'} =~ /^Win32-MFC\/(1.3.[01234])\b/;
5522 return $msg->("hello_test") if grep { $u->{user} eq $_ } @LJ::TESTACCTS;
5525 sub list_friendgroups
5527 my $u = shift;
5529 # get the groups for this user, return undef if error
5530 my $groups = LJ::get_friend_group($u);
5531 return undef unless $groups;
5533 # we got all of the groups, so put them into an arrayref sorted by the
5534 # group sortorder; also note that the map is used to construct a new hashref
5535 # out of the old group hashref so that we have all of the field names converted
5536 # to a format our callers can recognize
5537 my @res = map { { id => $_->{groupnum}, name => $_->{groupname},
5538 public => $_->{is_public}, sortorder => $_->{sortorder}, } }
5539 sort { $a->{sortorder} <=> $b->{sortorder} }
5540 values %$groups;
5542 return \@res;
5545 sub list_usejournals {
5546 my $u = shift;
5548 my @us = $u->posting_access_list;
5549 my @unames = map { $_->{user} } @us;
5551 return \@unames;
5554 sub hash_menus
5556 my $u = shift;
5557 my $user = $u->{'user'};
5559 my $menu = [
5560 { 'text' => "Recent Entries",
5561 'url' => "$LJ::SITEROOT/users/$user/", },
5562 { 'text' => "Calendar View",
5563 'url' => "$LJ::SITEROOT/users/$user/calendar", },
5564 { 'text' => "Friends View",
5565 'url' => "$LJ::SITEROOT/users/$user/friends", },
5566 { 'text' => "-", },
5567 { 'text' => "Your Profile",
5568 'url' => "$LJ::SITEROOT/userinfo.bml?user=$user", },
5569 { 'text' => "Your To-Do List",
5570 'url' => "$LJ::SITEROOT/todo/?user=$user", },
5571 { 'text' => "-", },
5572 { 'text' => "Change Settings",
5573 'sub' => [ { 'text' => "Personal Info",
5574 'url' => "$LJ::SITEROOT/manage/profile/", },
5575 { 'text' => "Customize Journal",
5576 'url' =>"$LJ::SITEROOT/customize/", }, ] },
5577 { 'text' => "-", },
5578 { 'text' => "Support",
5579 'url' => "$LJ::SITEROOT/support/", }
5582 LJ::run_hooks("modify_login_menu", {
5583 'menu' => $menu,
5584 'u' => $u,
5585 'user' => $user,
5588 return $menu;
5591 sub list_pickws
5593 my $u = shift;
5595 my $pi = LJ::get_userpic_info($u);
5596 my @res;
5598 my %seen; # mashifiedptr -> 1
5600 # FIXME: should be a utf-8 sort
5601 foreach my $kw (sort keys %{$pi->{'kw'}}) {
5602 my $pic = $pi->{'kw'}{$kw};
5603 $seen{$pic} = 1;
5604 next if $pic->{'state'} eq "I";
5605 push @res, [ $kw, $pic->{'picid'} ];
5608 # now add all the pictures that don't have a keyword
5609 foreach my $picid (keys %{$pi->{'pic'}}) {
5610 my $pic = $pi->{'pic'}{$picid};
5611 next if $seen{$pic};
5612 push @res, [ "pic#$picid", $picid ];
5615 return \@res;
5618 sub list_moods
5620 my $mood_max = int(shift);
5621 LJ::load_moods();
5623 my $res = [];
5624 return $res if $mood_max >= $LJ::CACHED_MOOD_MAX;
5626 for (my $id = $mood_max+1; $id <= $LJ::CACHED_MOOD_MAX; $id++) {
5627 next unless defined $LJ::CACHE_MOODS{$id};
5628 my $mood = $LJ::CACHE_MOODS{$id};
5629 next unless $mood->{'name'};
5630 push @$res, { 'id' => $id,
5631 'name' => $mood->{'name'},
5632 'parent' => $mood->{'parent'} };
5635 return $res;
5638 sub check_altusage
5640 my ($req, $err, $flags) = @_;
5642 # see note in LJ::can_use_journal about why we return
5643 # both 'ownerid' and 'u_owner' in $flags
5645 my $alt = $req->{'usejournal'} || $req->{'journal'};
5646 my $u = $flags->{'u'};
5648 unless ($u) {
5649 my $username = $req->{'username'};
5650 if ($username) {
5651 my $dbr = LJ::get_db_reader();
5652 return fail($err,502) unless $dbr;
5653 $u = $flags->{'u'} = LJ::load_user($username);
5654 } else {
5655 if ($flags->{allow_anonymous}) {
5656 return fail($err,200) unless $alt;
5657 my $uowner = LJ::load_user($alt);
5658 return fail($err,206) unless $uowner;
5659 $flags->{'u_owner'} = $uowner;
5660 $flags->{'ownerid'} = $uowner->{'userid'};
5661 return 1;
5663 return fail($err,200);
5667 $flags->{'ownerid'} = $u->{'userid'};
5669 # all good if not using an alt journal
5670 return 1 unless $alt;
5672 # complain if the username is invalid
5673 my $uowner = LJ::load_user($alt);
5674 return fail($err,206) unless $uowner;
5676 # allow usage if we're told explicitly that it's okay
5677 if ($flags->{'usejournal_okay'}) {
5678 $flags->{'u_owner'} = $uowner;
5679 $flags->{'ownerid'} = $uowner->{'userid'};
5680 LJ::Request->notes("journalid" => $flags->{'ownerid'}) if LJ::Request->is_inited && !LJ::Request->notes("journalid");
5681 return 1;
5684 # otherwise, check for access:
5685 my $info = {};
5686 my $canuse = LJ::can_use_journal($u->{'userid'}, $alt, $info);
5687 $flags->{'ownerid'} = $info->{'ownerid'};
5688 $flags->{'u_owner'} = $info->{'u_owner'};
5689 LJ::Request->notes("journalid" => $flags->{'ownerid'}) if LJ::Request->is_inited && !LJ::Request->notes("journalid");
5691 return 1 if $canuse || $flags->{'ignorecanuse'};
5693 # not allowed to access it
5694 return fail($err,300);
5697 sub authenticate
5699 my ($req, $err, $flags) = @_;
5701 my $u;
5703 my $auth_meth = $req->{'auth_method'} || "clear";
5704 my $username = $req->{'username'} || '';
5706 my $check_user = sub {
5707 return fail($err,100) unless $u;
5708 return fail($err,100) if ($u->{'statusvis'} eq "X");
5709 return fail($err,505) unless $u->{'clusterid'};
5710 return 1;
5713 unless ($auth_meth eq "oauth") {
5715 # add flag to avoid authentication
5716 if (!$username && $flags->{'allow_anonymous'}) {
5717 undef $flags->{'u'};
5718 return 1;
5721 return fail($err,200) unless $username;
5722 return fail($err,100) unless LJ::canonical_username($username);
5724 $u = $flags->{'u'};
5725 unless ($u) {
5726 my $dbr = LJ::get_db_reader();
5727 return fail($err,502) unless $dbr;
5728 $u = LJ::load_user($username);
5731 return unless $check_user->();
5734 my $ip_banned = 0;
5735 my $chal_expired = 0;
5736 my $auth_check = sub {
5738 if ($auth_meth eq "clear") {
5739 my $res = LJ::auth_okay($u,
5740 $req->{'password'},
5741 $req->{'hpassword'},
5742 $u->password,
5743 \$ip_banned);
5745 if ($res) {
5746 LJ::Session->record_login($u);
5748 return $res;
5750 if ($auth_meth eq "challenge") {
5751 my $chal_opts = {};
5752 my $chall_ok = LJ::challenge_check_login($u,
5753 $req->{'auth_challenge'},
5754 $req->{'auth_response'},
5755 \$ip_banned,
5756 $chal_opts);
5757 $chal_expired = 1 if $chal_opts->{expired};
5758 if ($chall_ok && !$chal_opts->{expired}) {
5759 LJ::Session->record_login($u);
5761 return $chall_ok;
5763 if ($auth_meth eq "cookie") {
5764 return unless LJ::Request->is_inited && LJ::Request->header_in("X-LJ-Auth") eq "cookie";
5765 my $remote = LJ::get_remote();
5766 return $remote && $remote->{'user'} eq $username ? 1 : 0;
5768 if ($auth_meth eq "oauth"){
5769 my $rate_limiter = LJ::Request->is_inited ?
5770 LJ::API::RateLimiter->new(LJ::Request->request) :
5771 LJ::API::RateLimiter->new();
5773 my $oauth = LJ::OAuth->new(rate_limiter => $rate_limiter);
5775 my $result = $oauth->have_access;
5776 unless ($result->{http_status} == 200) {
5777 return fail($err,331,$result->{oauth_problem}) if $result->{http_status} == 400;
5778 return fail($err,332,$result->{oauth_problem}) if $result->{http_status} == 401;
5779 return fail($err,334,$result->{oauth_problem}) if $result->{http_status} == 403;
5780 return fail($err,413,$result->{oauth_problem}) if $result->{http_status} == 503;
5781 return fail($err,101);
5783 $u = $result->{user};
5784 return unless $check_user->();
5785 $flags->{'user_access'} = $result->{access};
5786 LJ::Session->record_login($u);
5790 unless ($flags->{'nopassword'} ||
5791 $flags->{'noauth'} ||
5792 $auth_check->() )
5794 return undef if $$err;
5795 return fail($err,402) if $ip_banned;
5796 return fail($err,105) if $chal_expired;
5797 return fail($err,101);
5800 return 1 if ($flags->{'allow_anonymous'} && !$u);
5802 # if there is a require TOS revision, check for it now
5803 return fail($err, 156) unless $u->tosagree_verify;
5805 # remember the user record for later.
5806 $flags->{'u'} = $u;
5808 if (LJ::Request->is_inited) {
5809 LJ::Request->notes("ljuser" => $u->{'user'}) unless LJ::Request->notes("ljuser");
5810 LJ::Request->notes("journalid" => $u->{'userid'}) unless LJ::Request->notes("journalid");
5813 return 1;
5816 sub authorize
5818 my ($req, $err, $flags, $method) = @_;
5820 my $auth_method = $req->{'auth_method'};
5822 return 1 if ($flags->{noauth} || $flags->{nopassword});
5824 if ($auth_method eq 'oauth') {
5826 return fail($err,333) unless $flags->{'user_access'};
5827 return fail($err,333) unless defined $LJ::XMLRPC_USER_ACCESS{$method};
5829 my $access_required = ref $LJ::XMLRPC_USER_ACCESS{$method} ? $LJ::XMLRPC_USER_ACCESS{$method} : [$LJ::XMLRPC_USER_ACCESS{$method}];
5831 my %user_access = map {$_ => 1} @{$flags->{'user_access'}};
5833 foreach my $p (@$access_required){
5834 return fail($err,333) unless ( $user_access{$p} || ($p =~ /(.+)_ro$/) && $user_access{"$1_rw"} );
5838 if ($LJ::XMLRPC_VALIDATION_METHOD{$method}) {
5839 # Deny access for accounts that have not validated their email
5840 my $u = $flags->{'u'} || LJ::load_user($req->{'username'});
5841 unless ($u){
5842 return fail($err,335);
5844 unless ($u->is_validated) {
5845 return fail($err,336);
5849 return 1;
5852 sub fail
5854 my $err = shift;
5855 my $code = shift;
5856 my $des = shift;
5857 my $vars = shift;
5858 $code .= ":".($des =~ /^xmlrpc\.des\./ ? LJ::Lang::ml($des, $vars) : $des) if $des;
5859 $$err = $code if (ref $err eq "SCALAR");
5860 return undef;
5863 # PROBLEM: a while back we used auto_increment fields in our tables so that we could have
5864 # automatically incremented itemids and such. this was eventually phased out in favor of
5865 # the more portable alloc_user_counter function which uses the 'counter' table. when the
5866 # counter table has no data, it finds the highest id already in use in the database and adds
5867 # one to it.
5869 # a problem came about when users who last posted before alloc_user_counter went
5870 # and deleted all their entries and posted anew. alloc_user_counter would find no entries,
5871 # this no ids, and thus assign id 1, thinking it's all clean and new. but, id 1 had been
5872 # used previously, and now has comments attached to it.
5874 # the comments would happen because there was an old bug that wouldn't delete comments when
5875 # an entry was deleted. this has since been fixed. so this all combines to make this
5876 # a necessity, at least until no buggy data exist anymore!
5878 # this code here removes any comments that happen to exist for the id we're now using.
5879 sub new_entry_cleanup_hack {
5880 my ($u, $jitemid) = @_;
5882 # sanitize input
5883 $jitemid += 0;
5884 return unless $jitemid;
5885 my $ownerid = LJ::want_userid($u);
5886 return unless $ownerid;
5888 # delete logprops
5889 $u->do("DELETE FROM logprop2 WHERE journalid=$ownerid AND jitemid=$jitemid");
5891 # delete comments
5892 my $ids = LJ::Talk::get_talk_data($u, 'L', $jitemid);
5893 return unless ref $ids eq 'HASH' && %$ids;
5894 my $list = join ',', map { $_+0 } keys %$ids;
5895 $u->do("DELETE FROM talk2 WHERE journalid=$ownerid AND jtalkid IN ($list)");
5896 $u->do("DELETE FROM talktext2 WHERE journalid=$ownerid AND jtalkid IN ($list)");
5897 $u->do("DELETE FROM talkprop2 WHERE journalid=$ownerid AND jtalkid IN ($list)");
5900 sub un_utf8_request {
5901 my $req = shift;
5902 $req->{$_} = LJ::no_utf8_flag($req->{$_}) foreach qw(subject event);
5903 my $props = $req->{props} || {};
5904 foreach my $k (keys %$props) {
5905 next if ref $props->{$k}; # if this is multiple levels deep? don't think so.
5906 $props->{$k} = LJ::no_utf8_flag($props->{$k});
5910 # registerpush: adding push-notification params to user prop
5911 # specific for each mobile platform (windows phone 7, android, iOS)
5913 # takes:
5914 # - platform: wp7 / android / ios
5915 # - registrationid: argument which we use in communication with notification
5916 # servers, specific for each OS
5917 # - deviceid: id of registred device (not use yet)
5919 # returns: { status => 'OK'} if success
5921 sub registerpush {
5922 my ($req, $err, $flags) = @_;
5924 return undef
5925 unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'registerpush');
5927 my $u = $flags->{u};
5929 return fail($err, 200)
5930 unless $u && $req->{platform} && $req->{deviceid};
5932 my $error = LJ::PushNotification->subscribe($u, $req);
5933 return fail($error, 412) if $error;
5935 return { status => 'OK' }
5938 # unregisterpush: deletes subscription on push notification and clears user prop
5939 # with notification servers connection arguments
5941 # takes:
5942 # - platform: wp7 / android / ios
5943 # - deviceid: id of registred device (not use yet)
5945 # returns: { status => 'OK'} if success
5947 sub unregisterpush {
5948 my ($req, $err, $flags) = @_;
5949 return undef
5950 unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'unregisterpush');
5952 my $u = $flags->{u};
5954 return fail($err,200)
5955 unless $req->{platform};
5957 my $error = LJ::PushNotification->unsubscribe($u, $req);
5958 return $error if $error;
5960 return { status => 'OK' };
5963 sub pushsubscriptions {
5964 my ($req, $err, $flags) = @_;
5965 return undef
5966 unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'pushsubscriptions');
5968 my $u = $flags->{u};
5969 my @errors;
5970 foreach my $event (@{$req->{events}}) {
5971 if($event->{action} =~ /^(un)?subscribe$/) {
5973 my $res = eval{
5974 LJ::PushNotification->manage(
5976 app_name => $req->{app_name},
5977 platform => $req->{platform},
5978 deviceid => $req->{deviceid},
5979 optional_data => $req->{optional_data},
5980 %$event,
5984 push @errors, $@
5985 if $@;
5987 } else {
5988 push @errors, "wrong action '$event->{action}'";
5992 return { status => 'Has errors', errors => join "; ", @errors }
5993 if @errors;
5995 return { status => 'OK' };
5999 sub resetpushcounter {
6000 my ($req, $err, $flags) = @_;
6001 return undef
6002 unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'resetpushcounter');
6004 my $u = $flags->{u};
6006 return fail($err,200)
6007 unless $req->{platform} && $req->{deviceid};
6009 return fail($err,200)
6010 if $req->{platform} eq 'android';
6013 if(LJ::PushNotification::Storage->reset_counter($u, $req)) {
6014 return { status => 'OK' }
6017 return { status => 'Error', error => "Can't reset counter"}
6021 sub getpushlist {
6022 my ($req, $err, $flags) = @_;
6023 return undef
6024 unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getpushlist');
6026 my $u = $flags->{u};
6028 return fail($err,200)
6029 unless $req->{platform} && $req->{deviceid};
6031 my @subs = grep { $_->{ntypeid} == LJ::NotificationMethod::Push->ntypeid } ($u->subscriptions);
6033 my @events;
6034 foreach my $s (@subs) {
6036 my ($event) = $s->event_class =~ /LJ::Event::(.*)/;
6038 my $journal = LJ::load_userid($s->{journalid});
6040 my %event = (
6041 event => $event,
6044 $event{journal} = LJ::load_userid($s->{journalid})->user
6045 if $s->{journalid} != $s->{userid};
6047 if($event eq 'JournalNewComment' && $s->arg1) {
6048 $event{ditemid} = $s->arg1;
6051 if($event eq 'JournalNewComment' && $s->arg2) {
6052 my $comment = LJ::Comment->instance($s->{journalid}, jtalkid => $s->arg2);
6053 $event{dtalkid} = $comment->dtalkid;
6056 push @events, \%event;
6059 return {
6060 status => 'OK',
6061 events => \@events,
6066 sub geteventsrating {
6067 my ($req, $err, $flags) = @_;
6069 $flags->{allow_anonymous} = 1;
6070 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'geteventsrating');
6072 my $user_id = $flags->{u} ? $flags->{u}->id : 0;
6074 return fail($err, 200, 'region') unless $req->{region};
6076 return fail($err, 203, 'region') unless $req->{region} =~ /^cyr|noncyr|ua$/;
6078 return fail($err, 203, 'sort') if $req->{sort} && $req->{sort} !~ /^hits|visitors|default$/;
6080 foreach my $p (qw(skip itemshow user_id)){
6081 return fail($err, 203, 'xmlrpc.des.non_arifmetic', {'param'=>$p, 'value'=>$req->{$p}}) if ($req->{$p} && $req->{$p} =~ /\D/);
6082 $req->{$p} += 0;
6085 return fail($err, 209, 'xmlrpc.des.bad_value', {'param'=>'itemshow'}) if $req->{itemshow} > 100;
6087 $req->{getselfpromo} = 1 unless defined $req->{getselfpromo};
6089 my ($res, @err) = LJ::PersonalStats::Ratings::Posts->get_rating_segment( {
6090 rating_country => $req->{region},
6091 ($req->{sort} ne 'default' ? (sort => $req->{sort}) : ()),
6092 offset => $req->{skip} || 0,
6093 length => $req->{itemshow} || 30,
6094 show_selfpromo => $req->{getselfpromo},
6095 filter_selfpromo => $req->{getselfpromo},
6096 user_id => $user_id,
6099 return fail($err, 500, $err[0]) unless $res && ref $res && $res->{data} && ref $res->{data};
6101 my (@events, $selfpromo);
6103 my $entry_opts = {
6104 attrs => [qw(ditemid subject_raw event_raw)],
6105 remote_id => $user_id,
6106 map { $_ => $req->{$_} } qw(trim_widgets img_length get_video_ids get_polls asxml parseljtags),
6109 my $user_opts = {
6110 attrs => [qw(userid username)],
6113 foreach my $row (@{$res->{data}}) {
6114 $row->{ditemid} = delete $row->{post_id} if $row->{post_id};
6115 $row->{journalid} = delete $row->{journal_id} if $row->{journal_id};
6116 LJ::get_aggregated_entry($row, $entry_opts);
6118 $row->{userid} = delete $row->{journalid} if $row->{journalid};
6120 LJ::get_aggregated_user($row, $user_opts);
6122 push @events, {
6123 # rating data
6124 position => $row->{position},
6125 delta => $row->{delta},
6126 isnew => $row->{is_new} || 0,
6127 was_in_promo => $row->{was_in_promo} || 0,
6128 # entry data
6129 ditemid => $row->{ditemid},
6130 subject => $row->{subject_raw},
6131 event => $row->{event_raw},
6132 # user data
6133 posterid => $row->{userid},
6134 poster => $row->{username}
6138 if (my $sp = $res->{selfpromo} && $res->{selfpromo}->get_template_params ) {
6140 my $obj = $sp->{object}->[0];
6142 $obj->{ditemid} = delete $obj->{post_id} if $obj->{post_id};
6143 $obj->{journalid} = delete $obj->{journal_id} if $obj->{journal_id};
6144 LJ::get_aggregated_entry($obj, $entry_opts);
6146 $selfpromo = {
6147 # selfpromo data
6148 remaning_time => $obj->{timeleft},
6149 price => $obj->{buyout},
6150 # entry data
6151 ditemid => $obj->{ditemid},
6152 subject => $obj->{subject_raw},
6153 event => $obj->{event_raw},
6154 # user data
6155 posterid => $obj->{journalid},
6156 poster => $obj->{username},
6160 return {
6161 status => 'OK',
6162 skip => $req->{skip} || 0,
6163 region => $req->{region},
6164 events => \@events,
6165 ($req->{getselfpromo} ? (selfpromo => $selfpromo) : ())
6169 sub getusersrating {
6170 my ($req, $err, $flags) = @_;
6172 $flags->{allow_anonymous} = 1;
6173 return undef unless authenticate($req, $err, $flags) && authorize($req, $err, $flags, 'getusersrating');
6175 my $user_id = $flags->{u} ? $flags->{u}->id : 0;
6177 return fail($err, 200, 'region') unless $req->{region};
6179 return fail($err, 203, 'region') unless $req->{region} =~ /^cyr|noncyr|ua$/;
6181 return fail($err, 203, 'sort') if $req->{sort} && $req->{sort} !~ /^hits|friends|authority|default$/;
6183 foreach my $p (qw(skip itemshow user_id)){
6184 return fail($err, 203, 'xmlrpc.des.non_arifmetic', {'param'=>$p, 'value'=>$req->{$p}}) if ($req->{$p} && $req->{$p} =~ /\D/);
6187 return fail($err, 209, 'xmlrpc.des.bad_value', {'param'=>'itemshow'}) if $req->{itemshow} > 100;
6189 $req->{getselfpromo} = 1 unless defined $req->{getselfpromo};
6191 my ($res, @err) = LJ::PersonalStats::Ratings::Journals->get_rating_segment( {
6192 rating_country => $req->{region},
6193 ($req->{sort} ne 'default' ? (sort => $req->{sort}) : ()),
6194 is_community => $req->{journaltype} eq 'C' ? 1 : 0,
6195 offset => $req->{skip} || 0,
6196 length => $req->{itemshow} || 30,
6197 show_selfpromo => $req->{getselfpromo},
6198 filter_selfpromo => $req->{getselfpromo},
6201 return fail($err, 500, $err[0]) unless $res && ref $res && $res->{data} && ref $res->{data};
6203 my (@users, $selfpromo);
6205 my $user_opts = {
6206 attrs => [qw(username display_name profile_url journal_base userpic userhead_url name_raw
6207 identity_pretty_type identity_value identity_url )],
6210 foreach my $row (@{$res->{data}}) {
6212 $row->{userid} = delete $row->{journal_id} if $row->{journal_id};
6213 LJ::get_aggregated_user($row, $user_opts);
6215 push @users, {
6216 # rating data
6217 rating_value => $row->{value},
6218 position => $row->{position},
6219 delta => $row->{delta},
6220 isnew => $row->{is_new} || 0,
6221 was_in_promo => $row->{was_in_promo} || 0,
6222 # user data
6223 username => $row->{username},
6224 identity_display => $row->{display_name},
6225 identity_url => $row->{identity_url},
6226 identity_type => $row->{identity_pretty_type},
6227 identity_value => $row->{identity_value},
6228 userpic_url => $row->{userpic} ? $row->{userpic}->url : '',
6229 journal_url => $row->{journal_base},
6230 userhead_url => $row->{userhead_url},
6231 title => $row->{name_raw},
6235 if (my $sp = $res->{selfpromo} && $res->{selfpromo}->get_template_params ) {
6237 $selfpromo = {
6238 # selfpromo data
6239 remaning_time => $sp->{timeleft},
6240 price => $sp->{buyout},
6243 $sp = $sp->{object}->[0];
6245 $sp->{userid} = delete $sp->{journal_id} if $sp->{journal_id};
6246 LJ::get_aggregated_user($sp, $user_opts);
6248 $selfpromo = {
6249 %$selfpromo,
6250 # user data
6251 username => $sp->{username},
6252 identity_display => $sp->{display_name},
6253 identity_url => $sp->{identity_url},
6254 identity_type => $sp->{identity_pretty_type},
6255 identity_value => $sp->{identity_value},
6256 userpic_url => $sp->{userpic} ? $sp->{userpic}->url : '',
6257 journal_url => $sp->{journal_base},
6258 userhead_url => $sp->{userhead_url},
6259 title => $sp->{name_raw},
6263 return {
6264 status => 'OK',
6265 skip => $req->{skip} || 0,
6266 region => $req->{region},
6267 users => \@users,
6268 ($req->{getselfpromo} ? (selfpromo => $selfpromo) : ())
6272 #### Old interface (flat key/values) -- wrapper aruond LJ::Protocol
6273 package LJ;
6275 sub do_request {
6276 # get the request and response hash refs
6277 my ($req, $res, $flags) = @_;
6279 # initialize some stuff
6280 %$res = (); # clear the given response hash
6281 $flags = {} unless (ref $flags eq "HASH");
6283 # did they send a mode?
6284 unless ($req->{'mode'}) {
6285 $res->{'success'} = "FAIL";
6286 $res->{'errmsg'} = "Client error: No mode specified.";
6287 return;
6290 # this method doesn't require auth
6291 if ($req->{'mode'} eq "getchallenge") {
6292 return getchallenge($req, $res, $flags);
6295 # mode from here on out require a username
6296 my $user = LJ::canonical_username($req->{'user'});
6297 unless ($user) {
6298 $res->{'success'} = "FAIL";
6299 $res->{'errmsg'} = "Client error: No username sent.";
6300 return;
6303 ### see if the server's under maintenance now
6304 if ($LJ::SERVER_DOWN) {
6305 $res->{'success'} = "FAIL";
6306 $res->{'errmsg'} = $LJ::SERVER_DOWN_MESSAGE;
6307 return;
6310 ## dispatch wrappers
6311 if ($req->{'mode'} eq "login") {
6312 return login($req, $res, $flags);
6314 if ($req->{'mode'} eq "getfriendgroups") {
6315 return getfriendgroups($req, $res, $flags);
6317 if ($req->{'mode'} eq "getfriends") {
6318 return getfriends($req, $res, $flags);
6320 if ($req->{'mode'} eq "friendof") {
6321 return friendof($req, $res, $flags);
6323 if ($req->{'mode'} eq "checkfriends") {
6324 return checkfriends($req, $res, $flags);
6326 if ($req->{'mode'} eq "getdaycounts") {
6327 return getdaycounts($req, $res, $flags);
6329 if ($req->{'mode'} eq "postevent") {
6330 return postevent($req, $res, $flags);
6332 if ($req->{'mode'} eq "editevent") {
6333 return editevent($req, $res, $flags);
6335 if ($req->{'mode'} eq "syncitems") {
6336 return syncitems($req, $res, $flags);
6338 if ($req->{'mode'} eq "getevents") {
6339 return getevents($req, $res, $flags);
6341 if ($req->{'mode'} eq "editfriends") {
6342 return editfriends($req, $res, $flags);
6344 if ($req->{'mode'} eq "editfriendgroups") {
6345 return editfriendgroups($req, $res, $flags);
6347 if ($req->{'mode'} eq "consolecommand") {
6348 return consolecommand($req, $res, $flags);
6350 if ($req->{'mode'} eq "sessiongenerate") {
6351 return sessiongenerate($req, $res, $flags);
6353 if ($req->{'mode'} eq "sessionexpire") {
6354 return sessionexpire($req, $res, $flags);
6356 if ($req->{'mode'} eq "getusertags") {
6357 return getusertags($req, $res, $flags);
6359 if ($req->{'mode'} eq "getfriendspage") {
6360 return getfriendspage($req, $res, $flags);
6363 ### unknown mode!
6364 $res->{'success'} = "FAIL";
6365 $res->{'errmsg'} = "Client error: Unknown mode ($req->{'mode'})";
6366 return;
6369 ## flat wrapper
6370 sub getfriendspage
6372 my ($req, $res, $flags) = @_;
6374 my $err = 0;
6375 my $rq = upgrade_request($req);
6377 my $rs = LJ::Protocol::do_request("getfriendspage", $rq, \$err, $flags);
6378 unless ($rs) {
6379 $res->{'success'} = "FAIL";
6380 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6381 return 0;
6384 my $ect = 0;
6385 foreach my $evt (@{$rs->{'entries'}}) {
6386 $ect++;
6387 foreach my $f (qw(subject_raw journalname journaltype postername postertype ditemid security)) {
6388 if (defined $evt->{$f}) {
6389 $res->{"entries_${ect}_$f"} = $evt->{$f};
6392 $res->{"entries_${ect}_event"} = LJ::eurl($evt->{'event_raw'});
6395 $res->{'entries_count'} = $ect;
6396 $res->{'success'} = "OK";
6398 return 1;
6401 ## flat wrapper
6402 sub login
6404 my ($req, $res, $flags) = @_;
6406 my $err = 0;
6407 my $rq = upgrade_request($req);
6409 my $rs = LJ::Protocol::do_request("login", $rq, \$err, $flags);
6410 unless ($rs) {
6411 $res->{'success'} = "FAIL";
6412 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6413 return 0;
6416 $res->{'success'} = "OK";
6417 $res->{'name'} = $rs->{'fullname'};
6418 $res->{'message'} = $rs->{'message'} if $rs->{'message'};
6419 $res->{'fastserver'} = 1 if $rs->{'fastserver'};
6420 $res->{'caps'} = $rs->{'caps'} if $rs->{'caps'};
6422 # shared journals
6423 my $access_count = 0;
6424 foreach my $user (@{$rs->{'usejournals'}}) {
6425 $access_count++;
6426 $res->{"access_${access_count}"} = $user;
6428 if ($access_count) {
6429 $res->{"access_count"} = $access_count;
6432 # friend groups
6433 populate_friend_groups($res, $rs->{'friendgroups'});
6435 my $flatten = sub {
6436 my ($prefix, $listref) = @_;
6437 my $ct = 0;
6438 foreach (@$listref) {
6439 $ct++;
6440 $res->{"${prefix}_$ct"} = $_;
6442 $res->{"${prefix}_count"} = $ct;
6445 ### picture keywords
6446 $flatten->("pickw", $rs->{'pickws'})
6447 if defined $req->{"getpickws"};
6448 $flatten->("pickwurl", $rs->{'pickwurls'})
6449 if defined $req->{"getpickwurls"};
6450 $res->{'defaultpicurl'} = $rs->{'defaultpicurl'} if $rs->{'defaultpicurl'};
6452 ### report new moods that this client hasn't heard of, if they care
6453 if (defined $req->{"getmoods"}) {
6454 my $mood_count = 0;
6455 foreach my $m (@{$rs->{'moods'}}) {
6456 $mood_count++;
6457 $res->{"mood_${mood_count}_id"} = $m->{'id'};
6458 $res->{"mood_${mood_count}_name"} = $m->{'name'};
6459 $res->{"mood_${mood_count}_parent"} = $m->{'parent'};
6461 if ($mood_count) {
6462 $res->{"mood_count"} = $mood_count;
6466 #### send web menus
6467 if ($req->{"getmenus"} == 1) {
6468 my $menu = $rs->{'menus'};
6469 my $menu_num = 0;
6470 populate_web_menu($res, $menu, \$menu_num);
6473 return 1;
6476 ## flat wrapper
6477 sub getfriendgroups
6479 my ($req, $res, $flags) = @_;
6481 my $err = 0;
6482 my $rq = upgrade_request($req);
6484 my $rs = LJ::Protocol::do_request("getfriendgroups", $rq, \$err, $flags);
6485 unless ($rs) {
6486 $res->{'success'} = "FAIL";
6487 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6488 return 0;
6490 $res->{'success'} = "OK";
6491 populate_friend_groups($res, $rs->{'friendgroups'});
6493 return 1;
6496 ## flat wrapper
6497 sub getusertags
6499 my ($req, $res, $flags) = @_;
6501 my $err = 0;
6502 my $rq = upgrade_request($req);
6504 my $rs = LJ::Protocol::do_request("getusertags", $rq, \$err, $flags);
6505 unless ($rs) {
6506 $res->{'success'} = "FAIL";
6507 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6508 return 0;
6511 $res->{'success'} = "OK";
6513 my $ct = 0;
6514 foreach my $tag (@{$rs->{tags}}) {
6515 $ct++;
6516 $res->{"tag_${ct}_security"} = $tag->{security_level};
6517 $res->{"tag_${ct}_uses"} = $tag->{uses} if $tag->{uses};
6518 $res->{"tag_${ct}_display"} = $tag->{display} if $tag->{display};
6519 $res->{"tag_${ct}_name"} = $tag->{name};
6520 foreach my $lev (qw(friends private public)) {
6521 $res->{"tag_${ct}_sb_$_"} = $tag->{security}->{$_}
6522 if $tag->{security}->{$_};
6524 my $gm = 0;
6525 foreach my $grpid (keys %{$tag->{security}->{groups}}) {
6526 next unless $tag->{security}->{groups}->{$grpid};
6527 $gm++;
6528 $res->{"tag_${ct}_sb_group_${gm}_id"} = $grpid;
6529 $res->{"tag_${ct}_sb_group_${gm}_count"} = $tag->{security}->{groups}->{$grpid};
6531 $res->{"tag_${ct}_sb_group_count"} = $gm if $gm;
6533 $res->{'tag_count'} = $ct;
6535 return 1;
6538 ## flat wrapper
6539 sub getfriends
6541 my ($req, $res, $flags) = @_;
6543 my $err = 0;
6544 my $rq = upgrade_request($req);
6546 my $rs = LJ::Protocol::do_request("getfriends", $rq, \$err, $flags);
6547 unless ($rs) {
6548 $res->{'success'} = "FAIL";
6549 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6550 return 0;
6553 $res->{'success'} = "OK";
6554 if ($req->{'includegroups'}) {
6555 populate_friend_groups($res, $rs->{'friendgroups'});
6557 if ($req->{'includefriendof'}) {
6558 populate_friends($res, "friendof", $rs->{'friendofs'});
6560 populate_friends($res, "friend", $rs->{'friends'});
6562 return 1;
6565 ## flat wrapper
6566 sub friendof
6568 my ($req, $res, $flags) = @_;
6570 my $err = 0;
6571 my $rq = upgrade_request($req);
6573 my $rs = LJ::Protocol::do_request("friendof", $rq, \$err, $flags);
6574 unless ($rs) {
6575 $res->{'success'} = "FAIL";
6576 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6577 return 0;
6580 $res->{'success'} = "OK";
6581 populate_friends($res, "friendof", $rs->{'friendofs'});
6582 return 1;
6585 ## flat wrapper
6586 sub checkfriends
6588 my ($req, $res, $flags) = @_;
6590 my $err = 0;
6591 my $rq = upgrade_request($req);
6593 my $rs = LJ::Protocol::do_request("checkfriends", $rq, \$err, $flags);
6594 unless ($rs) {
6595 $res->{'success'} = "FAIL";
6596 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6597 return 0;
6600 $res->{'success'} = "OK";
6601 $res->{'new'} = $rs->{'new'};
6602 $res->{'lastupdate'} = $rs->{'lastupdate'};
6603 $res->{'interval'} = $rs->{'interval'};
6604 return 1;
6607 ## flat wrapper
6608 sub getdaycounts
6610 my ($req, $res, $flags) = @_;
6612 my $err = 0;
6613 my $rq = upgrade_request($req);
6615 my $rs = LJ::Protocol::do_request("getdaycounts", $rq, \$err, $flags);
6616 unless ($rs) {
6617 $res->{'success'} = "FAIL";
6618 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6619 return 0;
6622 $res->{'success'} = "OK";
6623 foreach my $d (@{ $rs->{'daycounts'} }) {
6624 $res->{$d->{'date'}} = $d->{'count'};
6626 return 1;
6629 ## flat wrapper
6630 sub syncitems
6632 my ($req, $res, $flags) = @_;
6634 my $err = 0;
6635 my $rq = upgrade_request($req);
6637 my $rs = LJ::Protocol::do_request("syncitems", $rq, \$err, $flags);
6638 unless ($rs) {
6639 $res->{'success'} = "FAIL";
6640 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6641 return 0;
6644 $res->{'success'} = "OK";
6645 $res->{'sync_total'} = $rs->{'total'};
6646 $res->{'sync_count'} = $rs->{'count'};
6648 my $ct = 0;
6649 foreach my $s (@{ $rs->{'syncitems'} }) {
6650 $ct++;
6651 foreach my $a (qw(item action time)) {
6652 $res->{"sync_${ct}_$a"} = $s->{$a};
6655 return 1;
6658 ## flat wrapper: limited functionality. (1 command only, server-parsed only)
6659 sub consolecommand
6661 my ($req, $res, $flags) = @_;
6663 my $err = 0;
6664 my $rq = upgrade_request($req);
6665 delete $rq->{'command'};
6667 $rq->{'commands'} = [ $req->{'command'} ];
6669 my $rs = LJ::Protocol::do_request("consolecommand", $rq, \$err, $flags);
6670 unless ($rs) {
6671 $res->{'success'} = "FAIL";
6672 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6673 return 0;
6676 $res->{'cmd_success'} = $rs->{'results'}->[0]->{'success'};
6677 $res->{'cmd_line_count'} = 0;
6678 foreach my $l (@{$rs->{'results'}->[0]->{'output'}}) {
6679 $res->{'cmd_line_count'}++;
6680 my $line = $res->{'cmd_line_count'};
6681 $res->{"cmd_line_${line}_type"} = $l->[0]
6682 if $l->[0];
6683 $res->{"cmd_line_${line}"} = $l->[1];
6686 $res->{'success'} = "OK";
6690 ## flat wrapper
6691 sub getchallenge
6693 my ($req, $res, $flags) = @_;
6694 my $err = 0;
6695 my $rs = LJ::Protocol::do_request("getchallenge", $req, \$err, $flags);
6697 # stupid copy (could just return $rs), but it might change in the future
6698 # so this protects us from future accidental harm.
6699 foreach my $k (qw(challenge server_time expire_time auth_scheme)) {
6700 $res->{$k} = $rs->{$k};
6703 $res->{'success'} = "OK";
6704 return $res;
6707 ## flat wrapper
6708 sub editfriends
6710 my ($req, $res, $flags) = @_;
6712 my $err = 0;
6713 my $rq = upgrade_request($req);
6715 $rq->{'add'} = [];
6716 $rq->{'delete'} = [];
6718 foreach (keys %$req) {
6719 if (/^editfriend_add_(\d+)_user$/) {
6720 my $n = $1;
6721 next unless ($req->{"editfriend_add_${n}_user"} =~ /\S/);
6722 my $fa = { 'username' => $req->{"editfriend_add_${n}_user"},
6723 'fgcolor' => $req->{"editfriend_add_${n}_fg"},
6724 'bgcolor' => $req->{"editfriend_add_${n}_bg"},
6725 'groupmask' => $req->{"editfriend_add_${n}_groupmask"},
6727 push @{$rq->{'add'}}, $fa;
6728 } elsif (/^editfriend_delete_(\w+)$/) {
6729 push @{$rq->{'delete'}}, $1;
6733 my $rs = LJ::Protocol::do_request("editfriends", $rq, \$err, $flags);
6734 unless ($rs) {
6735 $res->{'success'} = "FAIL";
6736 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6737 return 0;
6740 $res->{'success'} = "OK";
6742 my $ct = 0;
6743 foreach my $fa (@{ $rs->{'added'} }) {
6744 $ct++;
6745 $res->{"friend_${ct}_user"} = $fa->{'username'};
6746 $res->{"friend_${ct}_name"} = $fa->{'fullname'};
6749 $res->{'friends_added'} = $ct;
6751 return 1;
6754 ## flat wrapper
6755 sub editfriendgroups
6757 my ($req, $res, $flags) = @_;
6759 my $err = 0;
6760 my $rq = upgrade_request($req);
6762 $rq->{'groupmasks'} = {};
6763 $rq->{'set'} = {};
6764 $rq->{'delete'} = [];
6766 foreach (keys %$req) {
6767 if (/^efg_set_(\d+)_name$/) {
6768 next unless ($req->{$_} ne "");
6769 my $n = $1;
6770 my $fs = {
6771 'name' => $req->{"efg_set_${n}_name"},
6772 'sort' => $req->{"efg_set_${n}_sort"},
6774 if (defined $req->{"efg_set_${n}_public"}) {
6775 $fs->{'public'} = $req->{"efg_set_${n}_public"};
6777 $rq->{'set'}->{$n} = $fs;
6779 elsif (/^efg_delete_(\d+)$/) {
6780 if ($req->{$_}) {
6781 # delete group if value is true
6782 push @{$rq->{'delete'}}, $1;
6785 elsif (/^editfriend_groupmask_(\w+)$/) {
6786 $rq->{'groupmasks'}->{$1} = $req->{$_};
6790 my $rs = LJ::Protocol::do_request("editfriendgroups", $rq, \$err, $flags);
6791 unless ($rs) {
6792 $res->{'success'} = "FAIL";
6793 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6794 return 0;
6797 $res->{'success'} = "OK";
6798 return 1;
6801 sub flatten_props
6803 my ($req, $rq) = @_;
6805 ## changes prop_* to props hashref
6806 foreach my $k (keys %$req) {
6807 next unless ($k =~ /^prop_(.+)/);
6808 $rq->{'props'}->{$1} = $req->{$k};
6812 ## flat wrapper
6813 sub postevent
6815 my ($req, $res, $flags) = @_;
6817 my $err = 0;
6818 my $rq = upgrade_request($req);
6819 flatten_props($req, $rq);
6820 $rq->{'props'}->{'interface'} = "flat";
6822 my $rs = LJ::Protocol::do_request("postevent", $rq, \$err, $flags);
6823 unless ($rs) {
6824 $res->{'success'} = "FAIL";
6825 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6826 return 0;
6829 $res->{'message'} = $rs->{'message'} if $rs->{'message'};
6830 $res->{'extra_result_message'} = $rs->{'extra_result_message'} if $rs->{'extra_result_message'};
6831 $res->{'success'} = "OK";
6832 $res->{'itemid'} = $rs->{'itemid'};
6833 $res->{'anum'} = $rs->{'anum'} if defined $rs->{'anum'};
6834 $res->{'url'} = $rs->{'url'} if defined $rs->{'url'};
6835 # we may not translate 'warnings' here, because it may contain \n characters
6836 return 1;
6839 ## flat wrapper
6840 sub editevent
6842 my ($req, $res, $flags) = @_;
6844 my $err = 0;
6845 my $rq = upgrade_request($req);
6846 flatten_props($req, $rq);
6848 my $rs = LJ::Protocol::do_request("editevent", $rq, \$err, $flags);
6849 unless ($rs) {
6850 $res->{'success'} = "FAIL";
6851 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6852 return 0;
6855 $res->{'success'} = "OK";
6856 $res->{'itemid'} = $rs->{'itemid'};
6857 $res->{'anum'} = $rs->{'anum'} if defined $rs->{'anum'};
6858 $res->{'url'} = $rs->{'url'} if defined $rs->{'url'};
6859 return 1;
6862 ## flat wrapper
6863 sub sessiongenerate {
6864 my ($req, $res, $flags) = @_;
6866 my $err = 0;
6867 my $rq = upgrade_request($req);
6869 my $rs = LJ::Protocol::do_request('sessiongenerate', $rq, \$err, $flags);
6870 unless ($rs) {
6871 $res->{success} = 'FAIL';
6872 $res->{errmsg} = LJ::Protocol::error_message($err);
6875 $res->{success} = 'OK';
6876 $res->{ljsession} = $rs->{ljsession};
6877 return 1;
6880 ## flat wrappre
6881 sub sessionexpire {
6882 my ($req, $res, $flags) = @_;
6884 my $err = 0;
6885 my $rq = upgrade_request($req);
6887 $rq->{expire} = [];
6888 foreach my $k (keys %$rq) {
6889 push @{$rq->{expire}}, $1
6890 if $k =~ /^expire_id_(\d+)$/;
6893 my $rs = LJ::Protocol::do_request('sessionexpire', $rq, \$err, $flags);
6894 unless ($rs) {
6895 $res->{success} = 'FAIL';
6896 $res->{errmsg} = LJ::Protocol::error_message($err);
6899 $res->{success} = 'OK';
6900 return 1;
6903 ## flat wrapper
6904 sub getevents {
6905 my ($req, $res, $flags) = @_;
6907 my $err = 0;
6908 my $rq = upgrade_request($req);
6910 my $rs = LJ::Protocol::do_request("getevents", $rq, \$err, $flags);
6911 unless ($rs) {
6912 $res->{'success'} = "FAIL";
6913 $res->{'errmsg'} = LJ::Protocol::error_message($err);
6914 return 0;
6917 my $pct = 0;
6918 my $ect = 0;
6920 foreach my $evt (@{$rs->{'events'}}) {
6921 $ect++;
6922 foreach my $f (qw(itemid eventtime security allowmask subject anum url poster)) {
6923 if (defined $evt->{$f}) {
6924 $res->{"events_${ect}_$f"} = $evt->{$f};
6927 $res->{"events_${ect}_event"} = LJ::eurl($evt->{'event'});
6929 if ($evt->{'props'}) {
6930 foreach my $k (sort keys %{$evt->{'props'}}) {
6931 $pct++;
6932 $res->{"prop_${pct}_itemid"} = $evt->{'itemid'};
6933 $res->{"prop_${pct}_name"} = $k;
6934 $res->{"prop_${pct}_value"} = $evt->{'props'}->{$k};
6939 unless ($req->{'noprops'}) {
6940 $res->{'prop_count'} = $pct;
6943 $res->{'events_count'} = $ect;
6944 $res->{'success'} = "OK";
6946 return 1;
6950 sub populate_friends
6952 my ($res, $pfx, $list) = @_;
6953 my $count = 0;
6954 foreach my $f (@$list)
6956 $count++;
6957 $res->{"${pfx}_${count}_name"} = $f->{'fullname'};
6958 $res->{"${pfx}_${count}_user"} = $f->{'username'};
6959 $res->{"${pfx}_${count}_birthday"} = $f->{'birthday'} if $f->{'birthday'};
6960 $res->{"${pfx}_${count}_bg"} = $f->{'bgcolor'};
6961 $res->{"${pfx}_${count}_fg"} = $f->{'fgcolor'};
6962 if (defined $f->{'groupmask'}) {
6963 $res->{"${pfx}_${count}_groupmask"} = $f->{'groupmask'};
6965 if (defined $f->{'type'}) {
6966 $res->{"${pfx}_${count}_type"} = $f->{'type'};
6967 if ($f->{'type'} eq 'identity') {
6968 $res->{"${pfx}_${count}_identity_type"} = $f->{'identity_type'};
6969 $res->{"${pfx}_${count}_identity_value"} = $f->{'identity_value'};
6970 $res->{"${pfx}_${count}_identity_display"} = $f->{'identity_display'};
6973 if (defined $f->{'status'}) {
6974 $res->{"${pfx}_${count}_status"} = $f->{'status'};
6977 $res->{"${pfx}_count"} = $count;
6981 sub upgrade_request
6983 my $r = shift;
6984 my $new = { %{ $r } };
6985 $new->{'username'} = $r->{'user'};
6987 # but don't delete $r->{'user'}, as it might be, say, %FORM,
6988 # that'll get reused in a later request in, say, update.bml after
6989 # the login before postevent. whoops.
6991 return $new;
6994 ## given a $res hashref and friend group subtree (arrayref), flattens it
6995 sub populate_friend_groups
6997 my ($res, $fr) = @_;
6999 my $maxnum = 0;
7000 foreach my $fg (@$fr)
7002 my $num = $fg->{'id'};
7003 $res->{"frgrp_${num}_name"} = $fg->{'name'};
7004 $res->{"frgrp_${num}_sortorder"} = $fg->{'sortorder'};
7005 if ($fg->{'public'}) {
7006 $res->{"frgrp_${num}_public"} = 1;
7008 if ($num > $maxnum) { $maxnum = $num; }
7010 $res->{'frgrp_maxnum'} = $maxnum;
7013 ## given a menu tree, flattens it into $res hashref
7014 sub populate_web_menu
7016 my ($res, $menu, $numref) = @_;
7017 my $mn = $$numref; # menu number
7018 my $mi = 0; # menu item
7019 foreach my $it (@$menu) {
7020 $mi++;
7021 $res->{"menu_${mn}_${mi}_text"} = $it->{'text'};
7022 if ($it->{'text'} eq "-") { next; }
7023 if ($it->{'sub'}) {
7024 $$numref++;
7025 $res->{"menu_${mn}_${mi}_sub"} = $$numref;
7026 &populate_web_menu($res, $it->{'sub'}, $numref);
7027 next;
7030 $res->{"menu_${mn}_${mi}_url"} = $it->{'url'};
7032 $res->{"menu_${mn}_count"} = $mi;