3288 lines
111 KiB
Perl
Executable File
3288 lines
111 KiB
Perl
Executable File
#!/usr/bin/perl
|
|
#
|
|
|
|
use strict;
|
|
use Unicode::MapUTF8 ();
|
|
|
|
BEGIN {
|
|
# declare some charset aliases
|
|
# we need this at least for cases when the only name supported
|
|
# by MapUTF8.pm isn't recognized by browsers
|
|
# note: newer versions of MapUTF8 know these
|
|
{
|
|
my %alias = ( 'windows-1251' => 'cp1251',
|
|
'windows-1252' => 'cp1252',
|
|
'windows-1253' => 'cp1253', );
|
|
foreach (keys %alias) {
|
|
next if Unicode::MapUTF8::utf8_supported_charset($_);
|
|
Unicode::MapUTF8::utf8_charset_alias($_, $alias{$_});
|
|
}
|
|
}
|
|
}
|
|
|
|
require "$ENV{'LJHOME'}/cgi-bin/ljpoll.pl";
|
|
require "$ENV{'LJHOME'}/cgi-bin/ljconfig.pl";
|
|
require "$ENV{'LJHOME'}/cgi-bin/console.pl";
|
|
require "$ENV{'LJHOME'}/cgi-bin/taglib.pl";
|
|
|
|
# have to do this else mailgate will croak with email posting, but only want
|
|
# to do it if the site has enabled the hack
|
|
require "$ENV{'LJHOME'}/cgi-bin/talklib.pl" if $LJ::NEW_ENTRY_CLEANUP_HACK;
|
|
|
|
#### New interface (meta handler) ... other handlers should call into this.
|
|
package LJ::Protocol;
|
|
|
|
# global declaration of this text since we use it in two places
|
|
our $CannotBeShown = '(cannot be shown)';
|
|
|
|
sub translate
|
|
{
|
|
my ($u, $msg, $vars) = @_;
|
|
|
|
LJ::load_user_props($u, "browselang") unless $u->{'browselang'};
|
|
return LJ::Lang::get_text($u->{'browselang'}, "protocol.$msg", undef, $vars);
|
|
}
|
|
|
|
sub error_message
|
|
{
|
|
my $code = shift;
|
|
my $des;
|
|
if ($code =~ /^(\d\d\d):(.+)/) {
|
|
($code, $des) = ($1, $2);
|
|
}
|
|
my %e = (
|
|
# User Errors
|
|
"100" => "Invalid username",
|
|
"101" => "Invalid password",
|
|
"102" => "Can't use custom/private security on shared/community journals.",
|
|
"103" => "Poll error",
|
|
"104" => "Error adding one or more friends",
|
|
"105" => "Challenge expired",
|
|
"150" => "Can't post as non-user",
|
|
"151" => "Banned from journal",
|
|
"152" => "Can't make back-dated entries in non-personal journal.",
|
|
"153" => "Incorrect time value",
|
|
"154" => "Can't add a redirected account as a friend",
|
|
"155" => "Non-authenticated email address",
|
|
"156" => sub { # to reload w/o restart
|
|
LJ::tosagree_str('protocol' => 'text') ||
|
|
LJ::tosagree_str('protocol' => 'title')
|
|
},
|
|
|
|
# Client Errors
|
|
"200" => "Missing required argument(s)",
|
|
"201" => "Unknown method",
|
|
"202" => "Too many arguments",
|
|
"203" => "Invalid argument(s)",
|
|
"204" => "Invalid metadata datatype",
|
|
"205" => "Unknown metadata",
|
|
"206" => "Invalid destination journal username.",
|
|
"207" => "Protocol version mismatch",
|
|
"208" => "Invalid text encoding",
|
|
"209" => "Parameter out of range",
|
|
"210" => "Client tried to edit with corrupt data. Preventing.",
|
|
"211" => "Invalid or malformed tag list",
|
|
|
|
# Access Errors
|
|
"300" => "Don't have access to requested journal",
|
|
"301" => "Access of restricted feature",
|
|
"302" => "Can't edit post from requested journal",
|
|
"303" => "Can't edit post in community journal",
|
|
"304" => "Can't delete post in this community journal",
|
|
"305" => "Action forbidden; account is suspended.",
|
|
"306" => "This journal is temporarily in read-only mode. Try again in a couple minutes.",
|
|
"307" => "Selected journal no longer exists.",
|
|
"308" => "Account is locked and cannot be used.",
|
|
"309" => "Account is marked as a memorial.",
|
|
"310" => "Account needs to be age verified before use.",
|
|
"311" => "Access temporarily disabled.",
|
|
"312" => "Not allowed to add tags to entries in this journal",
|
|
"313" => "Must use existing tags for entries in this journal (can't create new ones)",
|
|
|
|
# Limit errors
|
|
"402" => "Your IP address is temporarily banned for exceeding the login failure rate.",
|
|
"404" => "Cannot post",
|
|
"405" => "Post frequency limit.",
|
|
"406" => "Client is making repeated requests. Perhaps it's broken?",
|
|
"407" => "Moderation queue full",
|
|
"408" => "Maximum queued posts for this community+poster combination reached.",
|
|
"409" => "Post too large.",
|
|
"410" => "Your trial account has expired. Posting now disabled.",
|
|
|
|
# Server Errors
|
|
"500" => "Internal server error",
|
|
"501" => "Database error",
|
|
"502" => "Database temporarily unavailable",
|
|
"503" => "Error obtaining necessary database lock",
|
|
"504" => "Protocol mode no longer supported.",
|
|
"505" => "Account data format on server is old and needs to be upgraded.", # cluster0
|
|
"506" => "Journal sync temporarily unavailable.",
|
|
);
|
|
|
|
my $prefix = "";
|
|
my $error = (ref $e{$code} eq 'CODE' ? $e{$code}->() : $e{$code}) || "BUG: Unknown error code!";
|
|
if ($code >= 200) { $prefix = "Client error: "; }
|
|
if ($code >= 500) { $prefix = "Server error: "; }
|
|
my $totalerror = "$prefix$error";
|
|
$totalerror .= ": $des" if $des;
|
|
return $totalerror;
|
|
}
|
|
|
|
sub do_request
|
|
{
|
|
# get the request and response hash refs
|
|
my ($method, $req, $err, $flags) = @_;
|
|
|
|
# if version isn't specified explicitly, it's version 0
|
|
if (ref $req eq "HASH") {
|
|
$req->{'ver'} = 0 unless defined $req->{'ver'};
|
|
}
|
|
|
|
$flags ||= {};
|
|
my @args = ($req, $err, $flags);
|
|
|
|
my $r = eval { Apache->request };
|
|
$r->notes("codepath" => "protocol.$method")
|
|
if $r && ! $r->notes("codepath");
|
|
|
|
if ($method eq "login") { return login(@args); }
|
|
if ($method eq "getfriendgroups") { return getfriendgroups(@args); }
|
|
if ($method eq "getfriends") { return getfriends(@args); }
|
|
if ($method eq "friendof") { return friendof(@args); }
|
|
if ($method eq "checkfriends") { return checkfriends(@args); }
|
|
if ($method eq "getdaycounts") { return getdaycounts(@args); }
|
|
if ($method eq "postevent") { return postevent(@args); }
|
|
if ($method eq "editevent") { return editevent(@args); }
|
|
if ($method eq "syncitems") { return syncitems(@args); }
|
|
if ($method eq "getevents") { return getevents(@args); }
|
|
if ($method eq "editfriends") { return editfriends(@args); }
|
|
if ($method eq "editfriendgroups") { return editfriendgroups(@args); }
|
|
if ($method eq "consolecommand") { return consolecommand(@args); }
|
|
if ($method eq "getchallenge") { return getchallenge(@args); }
|
|
if ($method eq "sessiongenerate") { return sessiongenerate(@args); }
|
|
if ($method eq "sessionexpire") { return sessionexpire(@args); }
|
|
|
|
$r->notes("codepath" => "") if $r;
|
|
return fail($err,201);
|
|
}
|
|
|
|
sub login
|
|
{
|
|
my ($req, $err, $flags) = @_;
|
|
return undef unless authenticate($req, $err, $flags);
|
|
|
|
my $u = $flags->{'u'};
|
|
my $res = {};
|
|
my $ver = $req->{'ver'};
|
|
|
|
## check for version mismatches
|
|
## non-Unicode installations can't handle versions >=1
|
|
|
|
return fail($err,207, "This installation does not support Unicode clients")
|
|
if $ver>=1 and not $LJ::UNICODE;
|
|
|
|
# do not let locked people log in
|
|
return fail($err, 308) if $u->{statusvis} eq 'L';
|
|
|
|
## return a message to the client to be displayed (optional)
|
|
login_message($req, $res, $flags);
|
|
LJ::text_out(\$res->{'message'}) if $ver>=1 and defined $res->{'message'};
|
|
|
|
## report what shared journals this user may post in
|
|
$res->{'usejournals'} = list_usejournals($u);
|
|
|
|
## return their friend groups
|
|
$res->{'friendgroups'} = list_friendgroups($u);
|
|
return fail($err, 502, "Error loading friend groups") unless $res->{'friendgroups'};
|
|
if ($ver >= 1) {
|
|
foreach (@{$res->{'friendgroups'}}) {
|
|
LJ::text_out(\$_->{'name'});
|
|
}
|
|
}
|
|
|
|
## if they gave us a number of moods to get higher than, then return them
|
|
if (defined $req->{'getmoods'}) {
|
|
$res->{'moods'} = list_moods($req->{'getmoods'});
|
|
if ($ver >= 1) {
|
|
# currently all moods are in English, but this might change
|
|
foreach (@{$res->{'moods'}}) { LJ::text_out(\$_->{'name'}) }
|
|
}
|
|
}
|
|
|
|
### picture keywords, if they asked for them.
|
|
if ($req->{'getpickws'}) {
|
|
my $pickws = list_pickws($u);
|
|
$res->{'pickws'} = [ map { $_->[0] } @$pickws ];
|
|
if ($req->{'getpickwurls'}) {
|
|
if ($u->{'defaultpicid'}) {
|
|
$res->{'defaultpicurl'} = "$LJ::USERPIC_ROOT/$u->{'defaultpicid'}/$u->{'userid'}";
|
|
}
|
|
$res->{'pickwurls'} = [ map {
|
|
"$LJ::USERPIC_ROOT/$_->[1]/$u->{'userid'}"
|
|
} @$pickws ];
|
|
}
|
|
if ($ver >= 1) {
|
|
# validate all text
|
|
foreach(@{$res->{'pickws'}}) { LJ::text_out(\$_); }
|
|
foreach(@{$res->{'pickwurls'}}) { LJ::text_out(\$_); }
|
|
LJ::text_out(\$res->{'defaultpicurl'});
|
|
}
|
|
}
|
|
|
|
## return client menu tree, if requested
|
|
if ($req->{'getmenus'}) {
|
|
$res->{'menus'} = hash_menus($u);
|
|
if ($ver >= 1) {
|
|
# validate all text, just in case, even though currently
|
|
# it's all English
|
|
foreach (@{$res->{'menus'}}) {
|
|
LJ::text_out(\$_->{'text'});
|
|
LJ::text_out(\$_->{'url'}); # should be redundant
|
|
}
|
|
}
|
|
}
|
|
|
|
## tell some users they can hit the fast servers later.
|
|
$res->{'fastserver'} = 1 if LJ::get_cap($u, "fastserver");
|
|
|
|
## user info
|
|
$res->{'userid'} = $u->{'userid'};
|
|
$res->{'fullname'} = $u->{'name'};
|
|
LJ::text_out(\$res->{'fullname'}) if $ver >= 1;
|
|
|
|
if ($req->{'clientversion'} =~ /^\S+\/\S+$/) {
|
|
eval {
|
|
my $r = Apache->request;
|
|
$r->notes("clientver", $req->{'clientversion'});
|
|
};
|
|
}
|
|
|
|
## update or add to clientusage table
|
|
if ($req->{'clientversion'} =~ /^\S+\/\S+$/ &&
|
|
! $LJ::DISABLED{'clientversionlog'})
|
|
{
|
|
my $client = $req->{'clientversion'};
|
|
|
|
return fail($err, 208, "Bad clientversion string")
|
|
if $ver >= 1 and not LJ::text_in($client);
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
my $qclient = $dbh->quote($client);
|
|
my $cu_sql = "REPLACE INTO clientusage (userid, clientid, lastlogin) " .
|
|
"SELECT $u->{'userid'}, clientid, NOW() FROM clients WHERE client=$qclient";
|
|
my $sth = $dbh->prepare($cu_sql);
|
|
$sth->execute;
|
|
unless ($sth->rows) {
|
|
# only way this can be 0 is if client doesn't exist in clients table, so
|
|
# we need to add a new row there, to get a new clientid for this new client:
|
|
$dbh->do("INSERT INTO clients (client) VALUES ($qclient)");
|
|
# and now we can do the query from before and it should work:
|
|
$sth = $dbh->prepare($cu_sql);
|
|
$sth->execute;
|
|
}
|
|
}
|
|
|
|
return $res;
|
|
}
|
|
|
|
sub getfriendgroups
|
|
{
|
|
my ($req, $err, $flags) = @_;
|
|
return undef unless authenticate($req, $err, $flags);
|
|
my $u = $flags->{'u'};
|
|
my $res = {};
|
|
$res->{'friendgroups'} = list_friendgroups($u);
|
|
return fail($err, 502, "Error loading friend groups") unless $res->{'friendgroups'};
|
|
if ($req->{'ver'} >= 1) {
|
|
foreach (@{$res->{'friendgroups'} || []}) {
|
|
LJ::text_out(\$_->{'name'});
|
|
}
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
sub getfriends
|
|
{
|
|
my ($req, $err, $flags) = @_;
|
|
return undef unless authenticate($req, $err, $flags);
|
|
return fail($req,502) unless LJ::get_db_reader();
|
|
my $u = $flags->{'u'};
|
|
my $res = {};
|
|
if ($req->{'includegroups'}) {
|
|
$res->{'friendgroups'} = list_friendgroups($u);
|
|
return fail($err, 502, "Error loading friend groups") unless $res->{'friendgroups'};
|
|
if ($req->{'ver'} >= 1) {
|
|
foreach (@{$res->{'friendgroups'} || []}) {
|
|
LJ::text_out(\$_->{'name'});
|
|
}
|
|
}
|
|
}
|
|
# TAG:FR:protocol:getfriends_of
|
|
if ($req->{'includefriendof'}) {
|
|
$res->{'friendofs'} = list_friends($u, {
|
|
'limit' => $req->{'friendoflimit'},
|
|
'friendof' => 1,
|
|
});
|
|
if ($req->{'ver'} >= 1) {
|
|
foreach(@{$res->{'friendofs'}}) { LJ::text_out(\$_->{'fullname'}) };
|
|
}
|
|
}
|
|
# TAG:FR:protocol:getfriends
|
|
$res->{'friends'} = list_friends($u, {
|
|
'limit' => $req->{'friendlimit'},
|
|
'includebdays' => $req->{'includebdays'},
|
|
});
|
|
if ($req->{'ver'} >= 1) {
|
|
foreach(@{$res->{'friends'}}) { LJ::text_out(\$_->{'fullname'}) };
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
sub friendof
|
|
{
|
|
my ($req, $err, $flags) = @_;
|
|
return undef unless authenticate($req, $err, $flags);
|
|
return fail($req,502) unless LJ::get_db_reader();
|
|
my $u = $flags->{'u'};
|
|
my $res = {};
|
|
|
|
# TAG:FR:protocol:getfriends_of2 (same as TAG:FR:protocol:getfriends_of)
|
|
$res->{'friendofs'} = list_friends($u, {
|
|
'friendof' => 1,
|
|
'limit' => $req->{'friendoflimit'},
|
|
});
|
|
if ($req->{'ver'} >= 1) {
|
|
foreach(@{$res->{'friendofs'}}) { LJ::text_out(\$_->{'fullname'}) };
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
sub checkfriends
|
|
{
|
|
my ($req, $err, $flags) = @_;
|
|
return undef unless authenticate($req, $err, $flags);
|
|
my $u = $flags->{'u'};
|
|
my $res = {};
|
|
|
|
# return immediately if they can't use this mode
|
|
unless (LJ::get_cap($u, "checkfriends")) {
|
|
$res->{'new'} = 0;
|
|
$res->{'interval'} = 36000; # tell client to bugger off
|
|
return $res;
|
|
}
|
|
|
|
## have a valid date?
|
|
my $lastupdate = $req->{'lastupdate'};
|
|
if ($lastupdate) {
|
|
return fail($err,203) unless
|
|
($lastupdate =~ /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/);
|
|
} else {
|
|
$lastupdate = "0000-00-00 00:00:00";
|
|
}
|
|
|
|
my $interval = LJ::get_cap_min($u, "checkfriends_interval");
|
|
$res->{'interval'} = $interval;
|
|
|
|
my $mask;
|
|
if ($req->{'mask'} and $req->{'mask'} !~ /\D/) {
|
|
$mask = $req->{'mask'};
|
|
}
|
|
|
|
my $memkey = [$u->{'userid'},"checkfriends:$u->{userid}:$mask"];
|
|
my $update = LJ::MemCache::get($memkey);
|
|
unless ($update) {
|
|
# TAG:FR:protocol:checkfriends (wants reading list of mask, not "friends")
|
|
my $fr = LJ::get_friends($u, $mask);
|
|
unless ($fr && %$fr) {
|
|
$res->{'new'} = 0;
|
|
$res->{'lastupdate'} = $lastupdate;
|
|
return $res;
|
|
}
|
|
if (@LJ::MEMCACHE_SERVERS) {
|
|
my $tu = LJ::get_timeupdate_multi({ memcache_only => 1 }, keys %$fr);
|
|
my $max = 0;
|
|
while ($_ = each %$tu) {
|
|
$max = $tu->{$_} if $tu->{$_} > $max;
|
|
}
|
|
$update = LJ::mysql_time($max) if $max;
|
|
} else {
|
|
my $dbr = LJ::get_db_reader();
|
|
unless ($dbr) {
|
|
# rather than return a 502 no-db error, just say no updates,
|
|
# because problem'll be fixed soon enough by db admins
|
|
$res->{'new'} = 0;
|
|
$res->{'lastupdate'} = $lastupdate;
|
|
return $res;
|
|
}
|
|
my $list = join(", ", map { int($_) } keys %$fr);
|
|
if ($list) {
|
|
my $sql = "SELECT MAX(timeupdate) FROM userusage ".
|
|
"WHERE userid IN ($list)";
|
|
$update = $dbr->selectrow_array($sql);
|
|
}
|
|
}
|
|
LJ::MemCache::set($memkey,$update,time()+$interval) if $update;
|
|
}
|
|
$update ||= "0000-00-00 00:00:00";
|
|
|
|
if ($req->{'lastupdate'} && $update gt $lastupdate) {
|
|
$res->{'new'} = 1;
|
|
} else {
|
|
$res->{'new'} = 0;
|
|
}
|
|
|
|
$res->{'lastupdate'} = $update;
|
|
return $res;
|
|
}
|
|
|
|
sub getdaycounts
|
|
{
|
|
my ($req, $err, $flags) = @_;
|
|
return undef unless authenticate($req, $err, $flags);
|
|
return undef unless check_altusage($req, $err, $flags);
|
|
|
|
my $u = $flags->{'u'};
|
|
my $uowner = $flags->{'u_owner'} || $u;
|
|
my $ownerid = $flags->{'ownerid'};
|
|
|
|
my $res = {};
|
|
my $daycts = LJ::get_daycounts($uowner, $u);
|
|
return fail($err,502) unless $daycts;
|
|
|
|
foreach my $day (@$daycts) {
|
|
my $date = sprintf("%04d-%02d-%02d", $day->[0], $day->[1], $day->[2]);
|
|
push @{$res->{'daycounts'}}, { 'date' => $date, 'count' => $day->[3] };
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
sub common_event_validation
|
|
{
|
|
my ($req, $err, $flags) = @_;
|
|
|
|
# clean up event whitespace
|
|
# remove surrounding whitespace
|
|
$req->{event} =~ s/^\s+//;
|
|
$req->{event} =~ s/\s+$//;
|
|
|
|
# convert line endings to unix format
|
|
if ($req->{'lineendings'} eq "mac") {
|
|
$req->{event} =~ s/\r/\n/g;
|
|
} else {
|
|
$req->{event} =~ s/\r//g;
|
|
}
|
|
|
|
# date validation
|
|
if ($req->{'year'} !~ /^\d\d\d\d$/ ||
|
|
$req->{'year'} < 1970 || # before unix time started = bad
|
|
$req->{'year'} > 2037) # after unix time ends = worse! :)
|
|
{
|
|
return fail($err,203,"Invalid year value.");
|
|
}
|
|
if ($req->{'mon'} !~ /^\d{1,2}$/ ||
|
|
$req->{'mon'} < 1 ||
|
|
$req->{'mon'} > 12)
|
|
{
|
|
return fail($err,203,"Invalid month value.");
|
|
}
|
|
if ($req->{'day'} !~ /^\d{1,2}$/ || $req->{'day'} < 1 ||
|
|
$req->{'day'} > LJ::days_in_month($req->{'mon'},
|
|
$req->{'year'}))
|
|
{
|
|
return fail($err,203,"Invalid day of month value.");
|
|
}
|
|
if ($req->{'hour'} !~ /^\d{1,2}$/ ||
|
|
$req->{'hour'} < 0 || $req->{'hour'} > 23)
|
|
{
|
|
return fail($err,203,"Invalid hour value.");
|
|
}
|
|
if ($req->{'min'} !~ /^\d{1,2}$/ ||
|
|
$req->{'min'} < 0 || $req->{'min'} > 59)
|
|
{
|
|
return fail($err,203,"Invalid minute value.");
|
|
}
|
|
|
|
# column width
|
|
# we only trim Unicode data
|
|
|
|
if ($req->{'ver'} >=1 ) {
|
|
$req->{'subject'} = LJ::text_trim($req->{'subject'}, LJ::BMAX_SUBJECT, LJ::CMAX_SUBJECT);
|
|
$req->{'event'} = LJ::text_trim($req->{'event'}, LJ::BMAX_EVENT, LJ::CMAX_EVENT);
|
|
foreach (keys %{$req->{'props'}}) {
|
|
# do not trim these properties as they're magical and handled later
|
|
next if $_ eq 'taglist';
|
|
|
|
$req->{'props'}->{$_} = LJ::text_trim($req->{'props'}->{$_}, LJ::BMAX_PROP, LJ::CMAX_PROP);
|
|
}
|
|
}
|
|
|
|
# setup non-user meta-data. it's important we define this here to
|
|
# 0. if it's not defined at all, then an editevent where a user
|
|
# removes random 8bit data won't remove the metadata. not that
|
|
# that matters much. but having this here won't hurt. false
|
|
# meta-data isn't saved anyway. so the only point of this next
|
|
# line is making the metadata be deleted on edit.
|
|
$req->{'props'}->{'unknown8bit'} = 0;
|
|
|
|
# we don't want attackers sending something that looks like gzipped data
|
|
# in protocol version 0 (unknown8bit allowed), otherwise they might
|
|
# inject a 100MB string of single letters in a few bytes.
|
|
return fail($err,208,"Cannot send gzipped data")
|
|
if substr($req->{'event'},0,2) eq "\037\213";
|
|
|
|
# non-ASCII?
|
|
unless ( LJ::is_ascii($req->{'event'}) &&
|
|
LJ::is_ascii($req->{'subject'}) &&
|
|
LJ::is_ascii(join(' ', values %{$req->{'props'}}) ))
|
|
{
|
|
if ($req->{'ver'} < 1) { # client doesn't support Unicode
|
|
# only people should have unknown8bit entries.
|
|
my $uowner = $flags->{u_owner} || $flags->{u};
|
|
return fail($err,207,'Posting in a community with international or special characters require a Unicode-capable LiveJournal client. Download one at http://www.livejournal.com/download/.')
|
|
if $uowner->{journaltype} ne 'P';
|
|
|
|
# so rest of site can change chars to ? marks until
|
|
# default user's encoding is set. (legacy support)
|
|
$req->{'props'}->{'unknown8bit'} = 1;
|
|
} else {
|
|
return fail($err,207, "This installation does not support Unicode clients") unless $LJ::UNICODE;
|
|
# validate that the text is valid UTF-8
|
|
if (!LJ::text_in($req->{'subject'}) ||
|
|
!LJ::text_in($req->{'event'}) ||
|
|
grep { !LJ::text_in($_) } values %{$req->{'props'}}) {
|
|
return fail($err, 208, "The text entered is not a valid UTF-8 stream");
|
|
}
|
|
}
|
|
}
|
|
|
|
## handle meta-data (properties)
|
|
LJ::load_props("log");
|
|
foreach my $pname (keys %{$req->{'props'}})
|
|
{
|
|
my $p = LJ::get_prop("log", $pname);
|
|
|
|
# does the property even exist?
|
|
unless ($p) {
|
|
$pname =~ s/[^\w]//g;
|
|
return fail($err,205,$pname);
|
|
}
|
|
|
|
# don't validate its type if it's 0 or undef (deleting)
|
|
next unless ($req->{'props'}->{$pname});
|
|
|
|
my $ptype = $p->{'datatype'};
|
|
my $val = $req->{'props'}->{$pname};
|
|
|
|
if ($ptype eq "bool" && $val !~ /^[01]$/) {
|
|
return fail($err,204,"Property \"$pname\" should be 0 or 1");
|
|
}
|
|
if ($ptype eq "num" && $val =~ /[^\d]/) {
|
|
return fail($err,204,"Property \"$pname\" should be numeric");
|
|
}
|
|
}
|
|
|
|
# check props for inactive userpic
|
|
if (my $pickwd = $req->{'props'}->{'picture_keyword'}) {
|
|
my $pic = LJ::get_pic_from_keyword($flags->{'u'}, $pickwd);
|
|
|
|
# need to make sure they aren't trying to post with an inactive keyword, but also
|
|
# we don't want to allow them to post with a keyword that has no pic at all to prevent
|
|
# them from deleting the keyword, posting, then adding it back with editpics.bml
|
|
delete $req->{'props'}->{'picture_keyword'} if ! $pic || $pic->{'state'} eq 'I';
|
|
}
|
|
|
|
# validate incoming list of tags
|
|
return fail($err, 211)
|
|
if $req->{props}->{taglist} &&
|
|
! LJ::Tags::is_valid_tagstring($req->{props}->{taglist});
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub postevent
|
|
{
|
|
my ($req, $err, $flags) = @_;
|
|
return undef unless authenticate($req, $err, $flags);
|
|
return undef unless check_altusage($req, $err, $flags);
|
|
|
|
my $u = $flags->{'u'};
|
|
my $ownerid = $flags->{'ownerid'}+0;
|
|
my $uowner = $flags->{'u_owner'} || $u;
|
|
# Make sure we have a real user object here
|
|
$uowner = LJ::want_user($uowner) unless LJ::isu($uowner);
|
|
my $clusterid = $uowner->{'clusterid'};
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
my $dbcm = LJ::get_cluster_master($uowner);
|
|
|
|
return fail($err,306) unless $dbh && $dbcm && $uowner->writer;
|
|
return fail($err,200) unless $req->{'event'} =~ /\S/;
|
|
|
|
### make sure community, shared, or news journals don't post
|
|
### note: shared and news journals are deprecated. every shared journal
|
|
## should one day be a community journal, of some form.
|
|
return fail($err,150) if ($u->{'journaltype'} eq "C" ||
|
|
$u->{'journaltype'} eq "S" ||
|
|
$u->{'journaltype'} eq "I" ||
|
|
$u->{'journaltype'} eq "N");
|
|
|
|
# underage users can't do this
|
|
return fail($err,310) if $u->underage;
|
|
|
|
# suspended users can't post
|
|
return fail($err,305) if ($u->{'statusvis'} eq "S");
|
|
|
|
# memorials can't post
|
|
return fail($err,309) if $u->{statusvis} eq 'M';
|
|
|
|
# locked accounts can't post
|
|
return fail($err,308) if $u->{statusvis} eq 'L';
|
|
|
|
# check the journal's read-only bit
|
|
return fail($err,306) if LJ::get_cap($uowner, "readonly");
|
|
|
|
# is the user allowed to post?
|
|
return fail($err,404,$LJ::MSG_NO_POST) unless LJ::get_cap($u, "can_post");
|
|
|
|
# is the user allowed to post?
|
|
return fail($err,410) if LJ::get_cap($u, "disable_can_post");
|
|
|
|
# can't post to deleted/suspended community
|
|
return fail($err,307) unless $uowner->{'statusvis'} eq "V";
|
|
|
|
# must have a validated email address to post to a community
|
|
return fail($err, 155, "You must have an authenticated email address in order to post to another account")
|
|
unless LJ::u_equals($u, $uowner) || $u->{'status'} eq 'A';
|
|
|
|
# post content too large
|
|
# NOTE: requires $req->{event} be binary data, but we've already
|
|
# removed the utf-8 flag in the XML-RPC path, and it never gets
|
|
# set in the "flat" protocol path.
|
|
return fail($err,409) if length($req->{'event'}) >= LJ::BMAX_EVENT;
|
|
|
|
my $time_was_faked = 0;
|
|
my $offset = 0; # assume gmt at first.
|
|
|
|
if (defined $req->{'tz'}) {
|
|
if ($req->{tz} eq 'guess') {
|
|
LJ::get_timezone($uowner, \$offset, \$time_was_faked);
|
|
} elsif ($req->{'tz'} =~ /^[+\-]\d\d\d\d$/) {
|
|
# FIXME we ought to store this timezone and make use of it somehow.
|
|
$offset = $req->{'tz'} / 100.0;
|
|
} else {
|
|
return fail($err, 203, "Invalid tz");
|
|
}
|
|
}
|
|
|
|
if (defined $req->{'tz'} and not grep { defined $req->{$_} } qw(year mon day hour min)) {
|
|
my @ltime = gmtime(time() + ($offset*3600));
|
|
$req->{'year'} = $ltime[5]+1900;
|
|
$req->{'mon'} = $ltime[4]+1;
|
|
$req->{'day'} = $ltime[3];
|
|
$req->{'hour'} = $ltime[2];
|
|
$req->{'min'} = $ltime[1];
|
|
}
|
|
|
|
return undef
|
|
unless common_event_validation($req, $err, $flags);
|
|
|
|
# confirm we can add tags, at least
|
|
return fail($err, 312)
|
|
if $req->{props} && $req->{props}->{taglist} &&
|
|
! LJ::Tags::can_add_tags($uowner, $u);
|
|
|
|
my $event = $req->{'event'};
|
|
|
|
### allow for posting to journals that aren't yours (if you have permission)
|
|
my $posterid = $u->{'userid'}+0;
|
|
|
|
# make the proper date format
|
|
my $eventtime = sprintf("%04d-%02d-%02d %02d:%02d",
|
|
$req->{'year'}, $req->{'mon'},
|
|
$req->{'day'}, $req->{'hour'},
|
|
$req->{'min'});
|
|
my $qeventtime = $dbh->quote($eventtime);
|
|
|
|
# load userprops all at once
|
|
my @poster_props = qw(newesteventtime dupsig_post);
|
|
my @owner_props = qw(newpost_minsecurity moderated);
|
|
push @owner_props, 'opt_weblogscom' unless $req->{'props'}->{'opt_backdated'};
|
|
|
|
LJ::load_user_props($u, @poster_props, @owner_props);
|
|
if ($uowner->{'userid'} == $u->{'userid'}) {
|
|
$uowner->{$_} = $u->{$_} foreach (@owner_props);
|
|
} else {
|
|
LJ::load_user_props($uowner, @owner_props);
|
|
}
|
|
|
|
# are they trying to post back in time?
|
|
if ($posterid == $ownerid && !$time_was_faked &&
|
|
$u->{'newesteventtime'} && $eventtime lt $u->{'newesteventtime'} &&
|
|
!$req->{'props'}->{'opt_backdated'}) {
|
|
return fail($err, 153, "Your most recent journal entry is dated $u->{'newesteventtime'}, but you're trying to post one at $eventtime without the backdate option turned on. Please check your computer's clock. Or, if you really mean to post in the past, use the backdate option.");
|
|
}
|
|
|
|
my $qallowmask = $req->{'allowmask'}+0;
|
|
my $security = "public";
|
|
my $uselogsec = 0;
|
|
if ($req->{'security'} eq "usemask" || $req->{'security'} eq "private") {
|
|
$security = $req->{'security'};
|
|
}
|
|
if ($req->{'security'} eq "usemask") {
|
|
$uselogsec = 1;
|
|
}
|
|
|
|
## if newpost_minsecurity is set, new entries have to be
|
|
## a minimum security level
|
|
$security = "private"
|
|
if $uowner->{'newpost_minsecurity'} eq "private";
|
|
($security, $qallowmask) = ("usemask", 1)
|
|
if $uowner->{'newpost_minsecurity'} eq "friends"
|
|
and $security eq "public";
|
|
|
|
my $qsecurity = $dbh->quote($security);
|
|
|
|
### make sure user can't post with "custom/private security" on shared journals
|
|
return fail($err,102)
|
|
if ($ownerid != $posterid && # community post
|
|
($req->{'security'} eq "private" ||
|
|
($req->{'security'} eq "usemask" && $qallowmask != 1 )));
|
|
|
|
# make sure this user isn't banned from posting here (if
|
|
# this is a community journal)
|
|
return fail($err,151) if
|
|
LJ::is_banned($posterid, $ownerid);
|
|
|
|
# don't allow backdated posts in communities
|
|
return fail($err,152) if
|
|
($req->{'props'}->{"opt_backdated"} &&
|
|
$uowner->{'journaltype'} ne "P");
|
|
|
|
# do processing of embedded polls (doesn't add to database, just
|
|
# does validity checking)
|
|
my @polls = ();
|
|
if (LJ::Poll::contains_new_poll(\$event))
|
|
{
|
|
return fail($err,301,"Your account type doesn't permit creating polls.")
|
|
unless (LJ::get_cap($u, "makepoll")
|
|
|| ($uowner->{'journaltype'} eq "C"
|
|
&& LJ::get_cap($uowner, "makepoll")
|
|
&& LJ::can_manage_other($u, $uowner)));
|
|
|
|
my $error = "";
|
|
@polls = LJ::Poll::parse(\$event, \$error, {
|
|
'journalid' => $ownerid,
|
|
'posterid' => $posterid,
|
|
});
|
|
return fail($err,103,$error) if $error;
|
|
}
|
|
|
|
my $now = $dbcm->selectrow_array("SELECT UNIX_TIMESTAMP()");
|
|
my $anum = int(rand(256));
|
|
|
|
# by default we record the true reverse time that the item was entered.
|
|
# however, if backdate is on, we put the reverse time at the end of time
|
|
# (which makes it equivalent to 1969, but get_recent_items will never load
|
|
# it... where clause there is: < $LJ::EndOfTime). but this way we can
|
|
# have entries that don't show up on friends view, now that we don't have
|
|
# the hints table to not insert into.
|
|
my $rlogtime = $LJ::EndOfTime;
|
|
unless ($req->{'props'}->{"opt_backdated"}) {
|
|
$rlogtime -= $now;
|
|
}
|
|
|
|
my $dupsig = Digest::MD5::md5_hex(join('', map { $req->{$_} }
|
|
qw(subject event usejournal security allowmask)));
|
|
my $lock_key = "post-$ownerid";
|
|
|
|
# release our duplicate lock
|
|
my $release = sub { $dbcm->do("SELECT RELEASE_LOCK(?)", undef, $lock_key); };
|
|
|
|
# our own local version of fail that releases our lock first
|
|
my $fail = sub { $release->(); return fail(@_); };
|
|
|
|
my $res = {};
|
|
my $res_done = 0; # set true by getlock when post was duplicate, or error getting lock
|
|
|
|
my $getlock = sub {
|
|
my $r = $dbcm->selectrow_array("SELECT GET_LOCK(?, 2)", undef, $lock_key);
|
|
unless ($r) {
|
|
$res = undef; # a failure case has an undef result
|
|
fail($err,503); # set error flag to "can't get lock";
|
|
$res_done = 1; # tell caller to bail out
|
|
return;
|
|
}
|
|
my @parts = split(/:/, $u->{'dupsig_post'});
|
|
if ($parts[0] eq $dupsig) {
|
|
# duplicate! let's make the client think this was just the
|
|
# normal first response.
|
|
$res->{'itemid'} = $parts[1];
|
|
$res->{'anum'} = $parts[2];
|
|
$res_done = 1;
|
|
$release->();
|
|
}
|
|
};
|
|
|
|
# if posting to a moderated community, store and bail out here
|
|
if ($uowner->{'journaltype'} eq 'C' && $uowner->{'moderated'} && !$flags->{'nomod'}) {
|
|
# don't moderate admins, moderators & pre-approved users
|
|
my $dbh = LJ::get_db_writer();
|
|
my $relcount = $dbh->selectrow_array("SELECT COUNT(*) FROM reluser ".
|
|
"WHERE userid=$ownerid AND targetid=$posterid ".
|
|
"AND type IN ('A','M','N')");
|
|
unless ($relcount) {
|
|
# moderation queue full?
|
|
my $modcount = $dbcm->selectrow_array("SELECT COUNT(*) FROM modlog WHERE journalid=$ownerid");
|
|
return fail($err, 407) if $modcount >= LJ::get_cap($uowner, "mod_queue");
|
|
|
|
$modcount = $dbcm->selectrow_array("SELECT COUNT(*) FROM modlog ".
|
|
"WHERE journalid=$ownerid AND posterid=$posterid");
|
|
return fail($err, 408) if $modcount >= LJ::get_cap($uowner, "mod_queue_per_poster");
|
|
|
|
$req->{'_moderate'}->{'authcode'} = LJ::make_auth_code(15);
|
|
my $fr = $dbcm->quote(Storable::freeze($req));
|
|
return fail($err, 409) if length($fr) > 200_000;
|
|
|
|
# store
|
|
my $modid = LJ::alloc_user_counter($uowner, "M");
|
|
return fail($err, 501) unless $modid;
|
|
|
|
$uowner->do("INSERT INTO modlog (journalid, modid, posterid, subject, logtime) ".
|
|
"VALUES ($ownerid, $modid, $posterid, ?, NOW())", undef,
|
|
LJ::text_trim($req->{'subject'}, 30, 0));
|
|
return fail($err, 501) if $uowner->err;
|
|
|
|
$uowner->do("INSERT INTO modblob (journalid, modid, request_stor) ".
|
|
"VALUES ($ownerid, $modid, $fr)");
|
|
if ($uowner->err) {
|
|
$uowner->do("DELETE FROM modlog WHERE journalid=$ownerid AND modid=$modid");
|
|
return fail($err, 501);
|
|
}
|
|
|
|
# alert moderator(s)
|
|
my $mods = LJ::load_rel_user($dbh, $ownerid, 'M') || [];
|
|
if (@$mods) {
|
|
# load up all these mods and figure out if they want email or not
|
|
my $modlist = LJ::load_userids(@$mods);
|
|
my @mailtomods;
|
|
foreach my $mod (values %$modlist) {
|
|
LJ::load_user_props($mod, 'opt_nomodemail');
|
|
push @mailtomods, $mod->{userid}
|
|
unless $mod->{opt_nomodemail};
|
|
}
|
|
|
|
# now get the email addresses of people who want email
|
|
if (@mailtomods) {
|
|
my $in = join(", ", map { $_+0 } @mailtomods );
|
|
my $emails = $dbh->selectcol_arrayref("SELECT email FROM user USE INDEX (PRIMARY) ".
|
|
"WHERE userid IN ($in) AND status='A'") || [];
|
|
my $ct;
|
|
foreach my $to (@$emails) {
|
|
last if ++$ct > 20; # don't send more than 20 emails.
|
|
my $body = ("There has been a new submission into the community '$uowner->{'user'}'\n".
|
|
"which you moderate.\n\n".
|
|
" User: $u->{'user'}\n".
|
|
" Subject: $req->{'subject'}\n\n".
|
|
"To accept or reject the submission, please go to this address:\n\n" .
|
|
" $LJ::SITEROOT/community/moderate.bml?comm=$uowner->{'user'}\n\n".
|
|
"Regards,\n$LJ::SITENAME Team\n\n$LJ::SITEROOT/\n");
|
|
LJ::send_mail({
|
|
'to' => $to,
|
|
'from' => $LJ::ADMIN_EMAIL,
|
|
'charset' => 'utf-8',
|
|
'subject' => "Moderated submission notification",
|
|
'body' => $body,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
my $msg = translate($u, "modpost", undef);
|
|
return { 'message' => $msg };
|
|
}
|
|
} # /moderated comms
|
|
|
|
# posting:
|
|
|
|
$getlock->(); return $res if $res_done;
|
|
|
|
# do rate-checking
|
|
if ($u->{'journaltype'} ne "Y" && ! LJ::rate_log($u, "post", 1)) {
|
|
return $fail->($err,405);
|
|
}
|
|
|
|
my $jitemid = LJ::alloc_user_counter($uowner, "L");
|
|
return $fail->($err,501,"No itemid could be generated.") unless $jitemid;
|
|
|
|
LJ::replycount_do($uowner, $jitemid, "init");
|
|
|
|
# remove comments and logprops on new entry ... see comment by this sub for clarification
|
|
LJ::Protocol::new_entry_cleanup_hack($u, $jitemid) if $LJ::NEW_ENTRY_CLEANUP_HACK;
|
|
my $verb = $LJ::NEW_ENTRY_CLEANUP_HACK ? 'REPLACE' : 'INSERT';
|
|
|
|
my $dberr;
|
|
$uowner->log2_do(\$dberr, "INSERT INTO log2 (journalid, jitemid, posterid, eventtime, logtime, security, ".
|
|
"allowmask, replycount, year, month, day, revttime, rlogtime, anum) ".
|
|
"VALUES ($ownerid, $jitemid, $posterid, $qeventtime, FROM_UNIXTIME($now), $qsecurity, $qallowmask, ".
|
|
"0, $req->{'year'}, $req->{'mon'}, $req->{'day'}, $LJ::EndOfTime-".
|
|
"UNIX_TIMESTAMP($qeventtime), $rlogtime, $anum)");
|
|
return $fail->($err,501,$dberr) if $dberr;
|
|
|
|
LJ::MemCache::incr([$ownerid, "log2ct:$ownerid"]);
|
|
LJ::memcache_kill($ownerid, "dayct");
|
|
|
|
# set userprops.
|
|
{
|
|
my %set_userprop;
|
|
|
|
# keep track of itemid/anum for later potential duplicates
|
|
$set_userprop{"dupsig_post"} = "$dupsig:$jitemid:$anum";
|
|
|
|
# record the eventtime of the last update (for own journals only)
|
|
$set_userprop{"newesteventtime"} = $eventtime
|
|
if $posterid == $ownerid and not $req->{'props'}->{'opt_backdated'} and not $time_was_faked;
|
|
|
|
LJ::set_userprop($u, \%set_userprop);
|
|
}
|
|
|
|
# end duplicate locking section
|
|
$release->();
|
|
|
|
my $ditemid = $jitemid * 256 + $anum;
|
|
|
|
### finish embedding stuff now that we have the itemid
|
|
{
|
|
### this should NOT return an error, and we're mildly fucked by now
|
|
### if it does (would have to delete the log row up there), so we're
|
|
### not going to check it for now.
|
|
|
|
my $error = "";
|
|
LJ::Poll::register(\$event, \$error, $ditemid, @polls);
|
|
}
|
|
#### /embedding
|
|
|
|
### extract links for meme tracking
|
|
unless ($req->{'security'} eq "usemask" ||
|
|
$req->{'security'} eq "private")
|
|
{
|
|
foreach my $url (LJ::get_urls($event)) {
|
|
LJ::record_meme($url, $posterid, $ditemid, $ownerid);
|
|
}
|
|
}
|
|
|
|
# record journal's disk usage
|
|
my $bytes = length($event) + length($req->{'subject'});
|
|
$uowner->dudata_set('L', $jitemid, $bytes);
|
|
|
|
$uowner->do("$verb INTO logtext2 (journalid, jitemid, subject, event) ".
|
|
"VALUES ($ownerid, $jitemid, ?, ?)", undef, $req->{'subject'},
|
|
LJ::text_compress($event));
|
|
if ($uowner->err) {
|
|
my $msg = $uowner->errstr;
|
|
LJ::delete_entry($uowner, $jitemid); # roll-back
|
|
return fail($err,501,"logtext:$msg");
|
|
}
|
|
LJ::MemCache::set([$ownerid,"logtext:$clusterid:$ownerid:$jitemid"],
|
|
[ $req->{'subject'}, $event ]);
|
|
|
|
# keep track of custom security stuff in other table.
|
|
if ($uselogsec) {
|
|
$uowner->do("INSERT INTO logsec2 (journalid, jitemid, allowmask) ".
|
|
"VALUES ($ownerid, $jitemid, $qallowmask)");
|
|
if ($uowner->err) {
|
|
my $msg = $uowner->errstr;
|
|
LJ::delete_entry($uowner, $jitemid); # roll-back
|
|
return fail($err,501,"logsec2:$msg");
|
|
}
|
|
}
|
|
|
|
# construct valid prop list
|
|
if ($req->{props} && $req->{props}->{taglist}) {
|
|
my $tags = [];
|
|
LJ::Tags::is_valid_tagstring($req->{props}->{taglist}, $tags);
|
|
$req->{props}->{taglist} = join(', ', @$tags);
|
|
|
|
# handle tags if they're defined
|
|
LJ::Tags::update_logtags($uowner, $jitemid, {
|
|
set_string => $req->{props}->{taglist},
|
|
remote => $u,
|
|
});
|
|
}
|
|
|
|
# meta-data
|
|
if (%{$req->{'props'}}) {
|
|
my $propset = {};
|
|
foreach my $pname (keys %{$req->{'props'}}) {
|
|
next unless $req->{'props'}->{$pname};
|
|
next if $pname eq "revnum" || $pname eq "revtime";
|
|
my $p = LJ::get_prop("log", $pname);
|
|
next unless $p;
|
|
next unless $req->{'props'}->{$pname};
|
|
$propset->{$pname} = $req->{'props'}->{$pname};
|
|
}
|
|
my %logprops;
|
|
LJ::set_logprop($uowner, $jitemid, $propset, \%logprops) if %$propset;
|
|
|
|
# if set_logprop modified props above, we can set the memcache key
|
|
# to be the hashref of modified props, since this is a new post
|
|
LJ::MemCache::set([$uowner->{'userid'}, "logprop:$uowner->{'userid'}:$jitemid"],
|
|
\%logprops) if %logprops;
|
|
}
|
|
|
|
$dbh->do("UPDATE userusage SET timeupdate=NOW(), lastitemid=$jitemid ".
|
|
"WHERE userid=$ownerid");
|
|
LJ::MemCache::set([$ownerid, "tu:$ownerid"], pack("N", time()), 30*60);
|
|
|
|
# note this post in recentactions table
|
|
LJ::note_recent_action($uowner, 'post');
|
|
|
|
# update user update table (on which friends views rely)
|
|
# NOTE: as of Mar-25-2003, we don't actually use this yet. we might
|
|
# use it in the future though, for faster ?skip=0 friends views.
|
|
# for now, we'll keep it disabled to lessen writes
|
|
if (0) {
|
|
my @bits;
|
|
if ($security eq "public") {
|
|
push @bits, 31; # 31 means public
|
|
} elsif ($security eq "private") {
|
|
push @bits, 32; # 1<<32 doesn't exist (too big), but we'll use it in this table
|
|
} else {
|
|
for (my $i=0; $i<=30; $i++) {
|
|
next unless $qallowmask & (1<<$i);
|
|
push @bits, $i;
|
|
}
|
|
}
|
|
if (@bits) {
|
|
$dbh->do("REPLACE INTO userupdate (userid, groupbit, timeupdate) VALUES ".
|
|
join(",", map { "($ownerid, $_, NOW())" } @bits));
|
|
}
|
|
}
|
|
|
|
# notify weblogs.com of post if necessary
|
|
if ($u->{'opt_weblogscom'} && LJ::get_cap($u, "weblogscom") &&
|
|
$security eq "public" && ! $req->{'props'}->{'opt_backdated'})
|
|
{
|
|
LJ::cmd_buffer_add($uowner->{clusterid}, $u->{'userid'}, 'weblogscom', {
|
|
'user' => $u->{'user'},
|
|
'title' => $u->{'journaltitle'} || $u->{'name'},
|
|
'url' => LJ::journal_base($u) . "/",
|
|
});
|
|
}
|
|
|
|
# run local site-specific actions
|
|
LJ::run_hooks("postpost", {
|
|
'itemid' => $jitemid,
|
|
'anum' => $anum,
|
|
'journal' => $uowner,
|
|
'poster' => $u,
|
|
'event' => $event,
|
|
'subject' => $req->{'subject'},
|
|
'security' => $security,
|
|
'allowmask' => $qallowmask,
|
|
'props' => $req->{'props'},
|
|
});
|
|
|
|
# cluster tracking
|
|
LJ::mark_user_active($u, 'post');
|
|
LJ::mark_user_active($uowner, 'post') unless LJ::u_equals($u, $uowner);
|
|
|
|
$res->{'itemid'} = $jitemid; # by request of mart
|
|
$res->{'anum'} = $anum;
|
|
$res->{'url'} = LJ::item_link($uowner, $jitemid, $anum);
|
|
return $res;
|
|
}
|
|
|
|
sub editevent
|
|
{
|
|
my ($req, $err, $flags) = @_;
|
|
return undef unless authenticate($req, $err, $flags);
|
|
|
|
# we check later that user owns entry they're modifying, so all
|
|
# we care about for check_altusage is that the target journal
|
|
# exists, and we want it to setup some data in $flags.
|
|
$flags->{'ignorecanuse'} = 1;
|
|
return undef unless check_altusage($req, $err, $flags);
|
|
|
|
my $u = $flags->{'u'};
|
|
my $ownerid = $flags->{'ownerid'};
|
|
my $uowner = $flags->{'u_owner'} || $u;
|
|
# Make sure we have a user object here
|
|
$uowner = LJ::want_user($uowner) unless LJ::isu($uowner);
|
|
my $clusterid = $uowner->{'clusterid'};
|
|
my $posterid = $u->{'userid'};
|
|
my $qallowmask = $req->{'allowmask'}+0;
|
|
my $sth;
|
|
|
|
my $itemid = $req->{'itemid'}+0;
|
|
|
|
# underage users can't do this
|
|
return fail($err,310) if $u->underage;
|
|
|
|
# check the journal's read-only bit
|
|
return fail($err,306) if LJ::get_cap($uowner, "readonly");
|
|
|
|
# can't edit in deleted/suspended community
|
|
return fail($err,307) unless $uowner->{'statusvis'} eq "V";
|
|
|
|
my $dbcm = LJ::get_cluster_master($uowner);
|
|
return fail($err,306) unless $dbcm;
|
|
|
|
### make sure user can't change a post to "custom/private security" on shared journals
|
|
return fail($err,102)
|
|
if ($ownerid != $posterid && # community post
|
|
($req->{'security'} eq "private" ||
|
|
($req->{'security'} eq "usemask" && $qallowmask != 1 )));
|
|
|
|
# fetch the old entry from master database so we know what we
|
|
# really have to update later. usually people just edit one part,
|
|
# not every field in every table. reads are quicker than writes,
|
|
# so this is worth it.
|
|
my $oldevent = $dbcm->selectrow_hashref
|
|
("SELECT journalid AS 'ownerid', posterid, eventtime, logtime, ".
|
|
"compressed, security, allowmask, year, month, day, ".
|
|
"rlogtime, anum FROM log2 WHERE journalid=$ownerid AND jitemid=$itemid");
|
|
|
|
($oldevent->{event}, $oldevent->{subject}) = $dbcm->selectrow_array
|
|
("SELECT subject, event FROM logtext2 ".
|
|
"WHERE journalid=$ownerid AND jitemid=$itemid");
|
|
|
|
LJ::text_uncompress(\$oldevent->{'event'});
|
|
|
|
# kill seconds in eventtime, since we don't use it, then we can use 'eq' and such
|
|
$oldevent->{'eventtime'} =~ s/:00$//;
|
|
|
|
### make sure this user is allowed to edit this entry
|
|
return fail($err,302)
|
|
unless ($ownerid == $oldevent->{'ownerid'});
|
|
|
|
### what can they do to somebody elses entry? (in shared journal)
|
|
if ($posterid != $oldevent->{'posterid'})
|
|
{
|
|
## deleting.
|
|
return fail($err,304)
|
|
if ($req->{'event'} !~ /\S/ && !
|
|
($ownerid == $u->{'userid'} ||
|
|
# community account can delete it (ick)
|
|
|
|
LJ::can_manage_other($posterid, $ownerid)
|
|
# if user is a community maintainer they can delete
|
|
# it too (good)
|
|
));
|
|
|
|
## editing:
|
|
return fail($err,303)
|
|
if ($req->{'event'} =~ /\S/);
|
|
}
|
|
|
|
# simple logic for deleting an entry
|
|
if ($req->{'event'} !~ /\S/)
|
|
{
|
|
# if their newesteventtime prop equals the time of the one they're deleting
|
|
# then delete their newesteventtime.
|
|
if ($u->{'userid'} == $uowner->{'userid'}) {
|
|
LJ::load_user_props($u, { use_master => 1 }, "newesteventtime");
|
|
if ($u->{'newesteventtime'} eq $oldevent->{'eventtime'}) {
|
|
LJ::set_userprop($u, "newesteventtime", undef);
|
|
}
|
|
}
|
|
|
|
# log this event, unless noauth is on, which means it is being done internally and we should
|
|
# rely on them to log why they're deleting the entry if they need to. that way we don't have
|
|
# double entries, and we have as much information available as possible at the location the
|
|
# delete is initiated.
|
|
$uowner->log_event('delete_entry', {
|
|
remote => $u,
|
|
actiontarget => ($req->{itemid} * 256 + $oldevent->{anum}),
|
|
method => 'protocol',
|
|
})
|
|
unless $flags->{noauth};
|
|
|
|
LJ::delete_entry($uowner, $req->{'itemid'}, 'quick', $oldevent->{'anum'});
|
|
|
|
# clear their duplicate protection, so they can later repost
|
|
# what they just deleted. (or something... probably rare.)
|
|
LJ::set_userprop($u, "dupsig_post", undef);
|
|
|
|
my $res = { 'itemid' => $itemid,
|
|
'anum' => $oldevent->{'anum'} };
|
|
return $res;
|
|
}
|
|
|
|
# now make sure the new entry text isn't $CannotBeShown
|
|
return fail($err, 210)
|
|
if $req->{event} eq $CannotBeShown;
|
|
|
|
# don't allow backdated posts in communities
|
|
return fail($err,152) if
|
|
($req->{'props'}->{"opt_backdated"} &&
|
|
$uowner->{'journaltype'} ne "P");
|
|
|
|
# make year/mon/day/hour/min optional in an edit event,
|
|
# and just inherit their old values
|
|
{
|
|
$oldevent->{'eventtime'} =~ /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d)/;
|
|
$req->{'year'} = $1 unless defined $req->{'year'};
|
|
$req->{'mon'} = $2+0 unless defined $req->{'mon'};
|
|
$req->{'day'} = $3+0 unless defined $req->{'day'};
|
|
$req->{'hour'} = $4+0 unless defined $req->{'hour'};
|
|
$req->{'min'} = $5+0 unless defined $req->{'min'};
|
|
}
|
|
|
|
# updating an entry:
|
|
return undef
|
|
unless common_event_validation($req, $err, $flags);
|
|
|
|
### load existing meta-data
|
|
my %curprops;
|
|
|
|
LJ::load_log_props2($dbcm, $ownerid, [ $itemid ], \%curprops);
|
|
|
|
## handle meta-data (properties)
|
|
my %props_byname = ();
|
|
foreach my $key (keys %{$req->{'props'}}) {
|
|
## changing to something else?
|
|
if ($curprops{$itemid}->{$key} ne $req->{'props'}->{$key}) {
|
|
$props_byname{$key} = $req->{'props'}->{$key};
|
|
}
|
|
}
|
|
|
|
my $event = $req->{'event'};
|
|
my $bytes = length($event) + length($req->{'subject'});
|
|
|
|
my $eventtime = sprintf("%04d-%02d-%02d %02d:%02d",
|
|
map { $req->{$_} } qw(year mon day hour min));
|
|
my $qeventtime = $dbcm->quote($eventtime);
|
|
|
|
# preserve old security by default, use user supplied if it's understood
|
|
my $security = $oldevent->{security};
|
|
$security = $req->{security}
|
|
if $req->{security} &&
|
|
$req->{security} =~ /^(?:public|private|usemask)$/;
|
|
|
|
my $do_tags = $req->{props} && $req->{props}->{taglist};
|
|
if ($oldevent->{security} ne $security || $qallowmask != $oldevent->{allowmask}) {
|
|
# FIXME: this is a hopefully temporary hack which deletes tags from the entry
|
|
# when the security has changed. the real fix is to make update_logtags aware
|
|
# of security changes so it can update logkwsum appropriately.
|
|
|
|
unless ($do_tags) {
|
|
# we need to fix security on this entry's tags, but the user didn't give us a tag list
|
|
# to work with, so we have to go get the tags on the entry, and construct a tag list,
|
|
# in order to pass to update_logtags down at the bottom of this whole update
|
|
my $tags = LJ::Tags::get_logtags($uowner, $itemid);
|
|
$tags = $tags->{"$uowner->{userid} $itemid"};
|
|
$req->{props}->{taglist} = join(',', sort map { $_->{name} } values %{$tags || {}});
|
|
$do_tags = 1; # bleh, force the update later
|
|
}
|
|
|
|
LJ::Tags::delete_logtags($uowner, $itemid);
|
|
}
|
|
|
|
my $qyear = $req->{'year'}+0;
|
|
my $qmonth = $req->{'mon'}+0;
|
|
my $qday = $req->{'day'}+0;
|
|
|
|
if ($eventtime ne $oldevent->{'eventtime'} ||
|
|
$security ne $oldevent->{'security'} ||
|
|
(!$curprops{opt_backdated} && $req->{props}{opt_backdated}) ||
|
|
$qallowmask != $oldevent->{'allowmask'})
|
|
{
|
|
# are they changing their most recent post?
|
|
LJ::load_user_props($u, "newesteventtime");
|
|
if ($u->{userid} == $uowner->{userid} &&
|
|
$u->{newesteventtime} eq $oldevent->{eventtime}) {
|
|
# did they change the time?
|
|
if ($eventtime ne $oldevent->{eventtime}) {
|
|
# the newesteventtime is this event's new time.
|
|
LJ::set_userprop($u, "newesteventtime", $eventtime);
|
|
} elsif (!$curprops{opt_backdated} && $req->{props}{opt_backdated}) {
|
|
# otherwise, if they set the backdated flag,
|
|
# then we no longer know the newesteventtime.
|
|
LJ::set_userprop($u, "newesteventtime", undef);
|
|
}
|
|
}
|
|
|
|
my $qsecurity = $uowner->quote($security);
|
|
my $dberr;
|
|
$uowner->log2_do(\$dberr, "UPDATE log2 SET eventtime=$qeventtime, revttime=$LJ::EndOfTime-".
|
|
"UNIX_TIMESTAMP($qeventtime), year=$qyear, month=$qmonth, day=$qday, ".
|
|
"security=$qsecurity, allowmask=$qallowmask WHERE journalid=$ownerid ".
|
|
"AND jitemid=$itemid");
|
|
return fail($err,501,$dberr) if $dberr;
|
|
|
|
# update memcached
|
|
my $sec = $qallowmask;
|
|
$sec = 0 if $security eq 'private';
|
|
$sec = 2**31 if $security eq 'public';
|
|
|
|
my $row = pack("NNNNN", $oldevent->{'posterid'},
|
|
LJ::mysqldate_to_time($eventtime, 1),
|
|
LJ::mysqldate_to_time($oldevent->{'logtime'}, 1),
|
|
$sec,
|
|
$itemid*256 + $oldevent->{'anum'});
|
|
|
|
LJ::MemCache::set([$ownerid, "log2:$ownerid:$itemid"], $row);
|
|
|
|
}
|
|
|
|
if ($security ne $oldevent->{'security'} ||
|
|
$qallowmask != $oldevent->{'allowmask'})
|
|
{
|
|
if ($security eq "public" || $security eq "private") {
|
|
$uowner->do("DELETE FROM logsec2 WHERE journalid=$ownerid AND jitemid=$itemid");
|
|
} else {
|
|
$uowner->do("REPLACE INTO logsec2 (journalid, jitemid, allowmask) ".
|
|
"VALUES ($ownerid, $itemid, $qallowmask)");
|
|
}
|
|
return fail($err,501,$dbcm->errstr) if $uowner->err;
|
|
}
|
|
|
|
LJ::MemCache::set([$ownerid,"logtext:$clusterid:$ownerid:$itemid"],
|
|
[ $req->{'subject'}, $event ]);
|
|
|
|
if ($event ne $oldevent->{'event'} ||
|
|
$req->{'subject'} ne $oldevent->{'subject'})
|
|
{
|
|
$uowner->do("UPDATE logtext2 SET subject=?, event=? ".
|
|
"WHERE journalid=$ownerid AND jitemid=$itemid", undef,
|
|
$req->{'subject'}, LJ::text_compress($event));
|
|
return fail($err,501,$uowner->errstr) if $uowner->err;
|
|
|
|
# update disk usage
|
|
$uowner->dudata_set('L', $itemid, $bytes);
|
|
}
|
|
|
|
# up the revision number
|
|
$req->{'props'}->{'revnum'} = ($curprops{$itemid}->{'revnum'} || 0) + 1;
|
|
$req->{'props'}->{'revtime'} = time();
|
|
|
|
# handle tags if they're defined
|
|
LJ::Tags::update_logtags($uowner, $itemid, {
|
|
set_string => $req->{props}->{taglist},
|
|
remote => $u,
|
|
})
|
|
if $do_tags;
|
|
|
|
# handle the props
|
|
{
|
|
my $propset = {};
|
|
foreach my $pname (keys %{$req->{'props'}}) {
|
|
my $p = LJ::get_prop("log", $pname);
|
|
next unless $p;
|
|
$propset->{$pname} = $req->{'props'}->{$pname};
|
|
}
|
|
LJ::set_logprop($uowner, $itemid, $propset);
|
|
}
|
|
|
|
# deal with backdated changes. if the entry's rlogtime is
|
|
# $EndOfTime, then it's backdated. if they want that off, need to
|
|
# reset rlogtime to real reverse log time. also need to set
|
|
# rlogtime to $EndOfTime if they're turning backdate on.
|
|
if ($req->{'props'}->{'opt_backdated'} eq "1" &&
|
|
$oldevent->{'rlogtime'} != $LJ::EndOfTime) {
|
|
my $dberr;
|
|
$uowner->log2_do(undef, "UPDATE log2 SET rlogtime=$LJ::EndOfTime WHERE ".
|
|
"journalid=$ownerid AND jitemid=$itemid");
|
|
return fail($err,501,$dberr) if $dberr;
|
|
}
|
|
if ($req->{'props'}->{'opt_backdated'} eq "0" &&
|
|
$oldevent->{'rlogtime'} == $LJ::EndOfTime) {
|
|
my $dberr;
|
|
$uowner->log2_do(\$dberr, "UPDATE log2 SET rlogtime=$LJ::EndOfTime-UNIX_TIMESTAMP(logtime) ".
|
|
"WHERE journalid=$ownerid AND jitemid=$itemid");
|
|
return fail($err,501,$dberr) if $dberr;
|
|
}
|
|
return fail($err,501,$dbcm->errstr) if $dbcm->err;
|
|
|
|
LJ::memcache_kill($ownerid, "dayct");
|
|
|
|
my $res = { 'itemid' => $itemid };
|
|
if (defined $oldevent->{'anum'}) {
|
|
$res->{'anum'} = $oldevent->{'anum'};
|
|
$res->{'url'} = LJ::item_link($uowner, $itemid, $oldevent->{'anum'});
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
sub getevents
|
|
{
|
|
my ($req, $err, $flags) = @_;
|
|
return undef unless authenticate($req, $err, $flags);
|
|
return undef unless check_altusage($req, $err, $flags);
|
|
|
|
my $u = $flags->{'u'};
|
|
my $uowner = $flags->{'u_owner'} || $u;
|
|
|
|
### shared-journal support
|
|
my $posterid = $u->{'userid'};
|
|
my $ownerid = $flags->{'ownerid'};
|
|
|
|
my $dbr = LJ::get_db_reader();
|
|
my $sth;
|
|
|
|
my $dbcr = LJ::get_cluster_reader($uowner);
|
|
return fail($err,502) unless $dbcr && $dbr;
|
|
|
|
# can't pull events from deleted/suspended journal
|
|
return fail($err,307) unless $uowner->{'statusvis'} eq "V";
|
|
|
|
my $reject_code = $LJ::DISABLE_PROTOCOL{getevents};
|
|
if (ref $reject_code eq "CODE") {
|
|
my $r = eval { Apache->request };
|
|
my $errmsg = $reject_code->($req, $flags, $r);
|
|
if ($errmsg) { return fail($err, "311", $errmsg); }
|
|
}
|
|
|
|
# if this is on, we sort things different (logtime vs. posttime)
|
|
# to avoid timezone issues
|
|
my $is_community = ($uowner->{'journaltype'} eq "C" ||
|
|
$uowner->{'journaltype'} eq "S");
|
|
|
|
# in some cases we'll use the master, to ensure there's no
|
|
# replication delay. useful cases: getting one item, use master
|
|
# since user might have just made a typo and realizes it as they
|
|
# post, or wants to append something they forgot, etc, etc. in
|
|
# other cases, slave is pretty sure to have it.
|
|
my $use_master = 0;
|
|
|
|
# the benefit of this mode over actually doing 'lastn/1' is
|
|
# the $use_master usage.
|
|
if ($req->{'selecttype'} eq "one" && $req->{'itemid'} eq "-1") {
|
|
$req->{'selecttype'} = "lastn";
|
|
$req->{'howmany'} = 1;
|
|
undef $req->{'itemid'};
|
|
$use_master = 1; # see note above.
|
|
}
|
|
|
|
# build the query to get log rows. each selecttype branch is
|
|
# responsible for either populating the following 3 variables
|
|
# OR just populating $sql
|
|
my ($orderby, $where, $limit);
|
|
my $sql;
|
|
if ($req->{'selecttype'} eq "day")
|
|
{
|
|
return fail($err,203)
|
|
unless ($req->{'year'} =~ /^\d\d\d\d$/ &&
|
|
$req->{'month'} =~ /^\d\d?$/ &&
|
|
$req->{'day'} =~ /^\d\d?$/ &&
|
|
$req->{'month'} >= 1 && $req->{'month'} <= 12 &&
|
|
$req->{'day'} >= 1 && $req->{'day'} <= 31);
|
|
|
|
my $qyear = $dbr->quote($req->{'year'});
|
|
my $qmonth = $dbr->quote($req->{'month'});
|
|
my $qday = $dbr->quote($req->{'day'});
|
|
$where = "AND year=$qyear AND month=$qmonth AND day=$qday";
|
|
$limit = "LIMIT 200"; # FIXME: unhardcode this constant (also in ljviews.pl)
|
|
|
|
# see note above about why the sort order is different
|
|
$orderby = $is_community ? "ORDER BY logtime" : "ORDER BY eventtime";
|
|
}
|
|
elsif ($req->{'selecttype'} eq "lastn")
|
|
{
|
|
my $howmany = $req->{'howmany'} || 20;
|
|
if ($howmany > 50) { $howmany = 50; }
|
|
$howmany = $howmany + 0;
|
|
$limit = "LIMIT $howmany";
|
|
|
|
# okay, follow me here... see how we add the revttime predicate
|
|
# even if no beforedate key is present? you're probably saying,
|
|
# that's retarded -- you're saying: "revttime > 0", that's like
|
|
# saying, "if entry occurred at all." yes yes, but that hints
|
|
# mysql's braindead optimizer to use the right index.
|
|
my $rtime_after = 0;
|
|
my $rtime_what = $is_community ? "rlogtime" : "revttime";
|
|
if ($req->{'beforedate'}) {
|
|
return fail($err,203,"Invalid beforedate format.")
|
|
unless ($req->{'beforedate'} =~
|
|
/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/);
|
|
my $qd = $dbr->quote($req->{'beforedate'});
|
|
$rtime_after = "$LJ::EndOfTime-UNIX_TIMESTAMP($qd)";
|
|
}
|
|
$where .= "AND $rtime_what > $rtime_after ";
|
|
$orderby = "ORDER BY $rtime_what";
|
|
}
|
|
elsif ($req->{'selecttype'} eq "one")
|
|
{
|
|
my $id = $req->{'itemid'} + 0;
|
|
$where = "AND jitemid=$id";
|
|
}
|
|
elsif ($req->{'selecttype'} eq "syncitems")
|
|
{
|
|
return fail($err,506) if $LJ::DISABLED{'syncitems'};
|
|
my $date = $req->{'lastsync'} || "0000-00-00 00:00:00";
|
|
return fail($err,203,"Invalid syncitems date format")
|
|
unless ($date =~ /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/);
|
|
|
|
my $now = time();
|
|
# broken client loop prevention
|
|
if ($req->{'lastsync'}) {
|
|
my $pname = "rl_syncitems_getevents_loop";
|
|
LJ::load_user_props($u, $pname);
|
|
# format is: time/date/time/date/time/date/... so split
|
|
# it into a hash, then delete pairs that are older than an hour
|
|
my %reqs = split(m!/!, $u->{$pname});
|
|
foreach (grep { $_ < $now - 60*60 } keys %reqs) { delete $reqs{$_}; }
|
|
my $count = grep { $_ eq $date } values %reqs;
|
|
$reqs{$now} = $date;
|
|
if ($count >= 2) {
|
|
# 2 prior, plus this one = 3 repeated requests for same synctime.
|
|
# their client is busted. (doesn't understand syncitems semantics)
|
|
return fail($err,406);
|
|
}
|
|
LJ::set_userprop($u, $pname,
|
|
join('/', map { $_, $reqs{$_} }
|
|
sort { $b <=> $a } keys %reqs));
|
|
}
|
|
|
|
my %item;
|
|
$sth = $dbcr->prepare("SELECT jitemid, logtime FROM log2 WHERE ".
|
|
"journalid=? and logtime > ?");
|
|
$sth->execute($ownerid, $date);
|
|
while (my ($id, $dt) = $sth->fetchrow_array) {
|
|
$item{$id} = $dt;
|
|
}
|
|
|
|
my $p_revtime = LJ::get_prop("log", "revtime");
|
|
$sth = $dbcr->prepare("SELECT jitemid, FROM_UNIXTIME(value) ".
|
|
"FROM logprop2 WHERE journalid=? ".
|
|
"AND propid=$p_revtime->{'id'} ".
|
|
"AND value+0 > UNIX_TIMESTAMP(?)");
|
|
$sth->execute($ownerid, $date);
|
|
while (my ($id, $dt) = $sth->fetchrow_array) {
|
|
$item{$id} = $dt;
|
|
}
|
|
|
|
my $limit = 100;
|
|
my @ids = sort { $item{$a} cmp $item{$b} } keys %item;
|
|
if (@ids > $limit) { @ids = @ids[0..$limit-1]; }
|
|
|
|
my $in = join(',', @ids) || "0";
|
|
$where = "AND jitemid IN ($in)";
|
|
}
|
|
elsif ($req->{'selecttype'} eq "multiple")
|
|
{
|
|
my @ids;
|
|
foreach my $num (split(/\s*,\s*/, $req->{'itemids'})) {
|
|
return fail($err,203,"Non-numeric itemid") unless $num =~ /^\d+$/;
|
|
push @ids, $num;
|
|
}
|
|
my $limit = 100;
|
|
return fail($err,209,"Can't retrieve more than $limit entries at once") if @ids > $limit;
|
|
my $in = join(',', @ids);
|
|
$where = "AND jitemid IN ($in)";
|
|
}
|
|
else
|
|
{
|
|
return fail($err,200,"Invalid selecttype.");
|
|
}
|
|
|
|
# common SQL template:
|
|
unless ($sql) {
|
|
$sql = "SELECT jitemid, eventtime, security, allowmask, anum, posterid ".
|
|
"FROM log2 WHERE journalid=$ownerid $where $orderby $limit";
|
|
}
|
|
|
|
# whatever selecttype might have wanted us to use the master db.
|
|
$dbcr = LJ::get_cluster_def_reader($uowner) if $use_master;
|
|
|
|
return fail($err,502) unless $dbcr;
|
|
|
|
## load the log rows
|
|
($sth = $dbcr->prepare($sql))->execute;
|
|
return fail($err,501,$dbcr->errstr) if $dbcr->err;
|
|
|
|
my $count = 0;
|
|
my @itemids = ();
|
|
my $res = {};
|
|
my $events = $res->{'events'} = [];
|
|
my %evt_from_itemid;
|
|
|
|
while (my ($itemid, $eventtime, $sec, $mask, $anum, $jposterid) = $sth->fetchrow_array)
|
|
{
|
|
$count++;
|
|
my $evt = {};
|
|
$evt->{'itemid'} = $itemid;
|
|
push @itemids, $itemid;
|
|
|
|
$evt_from_itemid{$itemid} = $evt;
|
|
|
|
$evt->{"eventtime"} = $eventtime;
|
|
if ($sec ne "public") {
|
|
$evt->{'security'} = $sec;
|
|
$evt->{'allowmask'} = $mask if $sec eq "usemask";
|
|
}
|
|
$evt->{'anum'} = $anum;
|
|
$evt->{'poster'} = LJ::get_username($dbr, $jposterid) if $jposterid != $ownerid;
|
|
$evt->{'url'} = LJ::item_link($uowner, $itemid, $anum);
|
|
push @$events, $evt;
|
|
}
|
|
|
|
# load properties. Even if the caller doesn't want them, we need
|
|
# them in Unicode installations to recognize older 8bit non-UF-8
|
|
# entries.
|
|
unless ($req->{'noprops'} && !$LJ::UNICODE)
|
|
{
|
|
### do the properties now
|
|
$count = 0;
|
|
my %props = ();
|
|
LJ::load_log_props2($dbcr, $ownerid, \@itemids, \%props);
|
|
|
|
# load the tags for these entries, unless told not to
|
|
unless ($req->{notags}) {
|
|
# construct %idsbycluster for the multi call to get these tags
|
|
my $tags = LJ::Tags::get_logtags($uowner, \@itemids);
|
|
|
|
# add to props
|
|
foreach my $itemid (@itemids) {
|
|
next unless $tags->{$itemid};
|
|
$props{$itemid}->{taglist} = join(', ', values %{$tags->{$itemid}});
|
|
}
|
|
}
|
|
|
|
foreach my $itemid (keys %props) {
|
|
# 'replycount' is a pseudo-prop, don't send it.
|
|
# FIXME: this goes away after we restructure APIs and
|
|
# replycounts cease being transferred in props
|
|
delete $props{$itemid}->{'replycount'};
|
|
|
|
my $evt = $evt_from_itemid{$itemid};
|
|
$evt->{'props'} = {};
|
|
foreach my $name (keys %{$props{$itemid}}) {
|
|
my $value = $props{$itemid}->{$name};
|
|
$value =~ s/\n/ /g;
|
|
$evt->{'props'}->{$name} = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
## load the text
|
|
my $gt_opts = {
|
|
'usemaster' => $use_master,
|
|
};
|
|
my $text = LJ::get_logtext2($uowner, $gt_opts, @itemids);
|
|
|
|
foreach my $i (@itemids)
|
|
{
|
|
my $t = $text->{$i};
|
|
my $evt = $evt_from_itemid{$i};
|
|
|
|
# if they want subjects to be events, replace event
|
|
# with subject when requested.
|
|
if ($req->{'prefersubject'} && length($t->[0])) {
|
|
$t->[1] = $t->[0]; # event = subject
|
|
$t->[0] = undef; # subject = undef
|
|
}
|
|
|
|
# now that we have the subject, the event and the props,
|
|
# auto-translate them to UTF-8 if they're not in UTF-8.
|
|
if ($LJ::UNICODE && $req->{'ver'} >= 1 &&
|
|
$evt->{'props'}->{'unknown8bit'}) {
|
|
my $error = 0;
|
|
$t->[0] = LJ::text_convert($t->[0], $uowner, \$error);
|
|
$t->[1] = LJ::text_convert($t->[1], $uowner, \$error);
|
|
foreach (keys %{$evt->{'props'}}) {
|
|
$evt->{'props'}->{$_} = LJ::text_convert($evt->{'props'}->{$_}, $uowner, \$error);
|
|
}
|
|
return fail($err,208,"Cannot display this post. Please see $LJ::SITEROOT/support/encodings.bml for more information.")
|
|
if $error;
|
|
}
|
|
|
|
if ($LJ::UNICODE && $req->{'ver'} < 1 && !$evt->{'props'}->{'unknown8bit'}) {
|
|
unless ( LJ::is_ascii($t->[0]) &&
|
|
LJ::is_ascii($t->[1]) &&
|
|
LJ::is_ascii(join(' ', values %{$evt->{'props'}}) )) {
|
|
# we want to fail the client that wants to get this entry
|
|
# but we make an exception for selecttype=day, in order to allow at least
|
|
# viewing the daily summary
|
|
|
|
if ($req->{'selecttype'} eq 'day') {
|
|
$t->[0] = $t->[1] = $CannotBeShown;
|
|
} else {
|
|
return fail($err,207,"Cannot display/edit a Unicode post with a non-Unicode client. Please see $LJ::SITEROOT/support/encodings.bml for more information.");
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($t->[0]) {
|
|
$t->[0] =~ s/[\r\n]/ /g;
|
|
$evt->{'subject'} = $t->[0];
|
|
}
|
|
|
|
# truncate
|
|
if ($req->{'truncate'} >= 4) {
|
|
my $original = $t->[1];
|
|
if ($req->{'ver'} > 1) {
|
|
$t->[1] = LJ::text_trim($t->[1], $req->{'truncate'} - 3, 0);
|
|
} else {
|
|
$t->[1] = LJ::text_trim($t->[1], 0, $req->{'truncate'} - 3);
|
|
}
|
|
# only append the elipsis if the text was actually truncated
|
|
$t->[1] .= "..." if $t->[1] ne $original;
|
|
}
|
|
|
|
# line endings
|
|
$t->[1] =~ s/\r//g;
|
|
if ($req->{'lineendings'} eq "unix") {
|
|
# do nothing. native format.
|
|
} elsif ($req->{'lineendings'} eq "mac") {
|
|
$t->[1] =~ s/\n/\r/g;
|
|
} elsif ($req->{'lineendings'} eq "space") {
|
|
$t->[1] =~ s/\n/ /g;
|
|
} elsif ($req->{'lineendings'} eq "dots") {
|
|
$t->[1] =~ s/\n/ ... /g;
|
|
} else { # "pc" -- default
|
|
$t->[1] =~ s/\n/\r\n/g;
|
|
}
|
|
$evt->{'event'} = $t->[1];
|
|
}
|
|
|
|
# maybe we don't need the props after all
|
|
if ($req->{'noprops'}) {
|
|
foreach(@$events) { delete $_->{'props'}; }
|
|
}
|
|
|
|
return $res;
|
|
}
|
|
|
|
sub editfriends
|
|
{
|
|
my ($req, $err, $flags) = @_;
|
|
return undef unless authenticate($req, $err, $flags);
|
|
|
|
my $u = $flags->{'u'};
|
|
my $userid = $u->{'userid'};
|
|
my $dbh = LJ::get_db_writer();
|
|
my $sth;
|
|
|
|
return fail($err,306) unless $dbh;
|
|
|
|
# do not let locked people do this
|
|
return fail($err, 308) if $u->{statusvis} eq 'L';
|
|
|
|
my $res = {};
|
|
|
|
## first, figure out who the current friends are to save us work later
|
|
my %curfriend;
|
|
my $friend_count = 0;
|
|
# TAG:FR:protocol:editfriends1
|
|
$sth = $dbh->prepare("SELECT u.user FROM useridmap u, friends f ".
|
|
"WHERE u.userid=f.friendid AND f.userid=$userid");
|
|
$sth->execute;
|
|
while (my ($friend) = $sth->fetchrow_array) {
|
|
$curfriend{$friend} = 1;
|
|
$friend_count++;
|
|
}
|
|
$sth->finish;
|
|
|
|
# perform the deletions
|
|
DELETEFRIEND:
|
|
foreach (@{$req->{'delete'}})
|
|
{
|
|
my $deluser = LJ::canonical_username($_);
|
|
next DELETEFRIEND unless ($curfriend{$deluser});
|
|
|
|
my $friendid = LJ::get_userid($deluser);
|
|
# TAG:FR:protocol:editfriends2_del
|
|
LJ::remove_friend($userid, $friendid);
|
|
$friend_count--;
|
|
}
|
|
|
|
my $error_flag = 0;
|
|
my $friends_added = 0;
|
|
my $fail = sub {
|
|
LJ::memcache_kill($userid, "friends");
|
|
LJ::mark_dirty($userid, "friends");
|
|
return fail($err, $_[0], $_[1]);
|
|
};
|
|
|
|
# only people, shared journals, and owned syn feeds can add friends
|
|
return $fail->(104, "Journal type cannot add friends")
|
|
unless ($u->{'journaltype'} eq 'P' ||
|
|
$u->{'journaltype'} eq 'S' ||
|
|
$u->{'journaltype'} eq 'I' ||
|
|
($u->{'journaltype'} eq "Y" && $u->{'password'}));
|
|
|
|
# perform the adds
|
|
ADDFRIEND:
|
|
foreach my $fa (@{$req->{'add'}})
|
|
{
|
|
unless (ref $fa eq "HASH") {
|
|
$fa = { 'username' => $fa };
|
|
}
|
|
|
|
my $aname = LJ::canonical_username($fa->{'username'});
|
|
unless ($aname) {
|
|
$error_flag = 1;
|
|
next ADDFRIEND;
|
|
}
|
|
|
|
$friend_count++ unless $curfriend{$aname};
|
|
|
|
my $maxfriends = LJ::get_cap($u, "maxfriends");
|
|
return $fail->(104, "Exceeded $maxfriends friends limit (now: $friend_count)")
|
|
if ($friend_count > $maxfriends);
|
|
|
|
my $fg = $fa->{'fgcolor'} || "#000000";
|
|
my $bg = $fa->{'bgcolor'} || "#FFFFFF";
|
|
if ($fg !~ /^\#[0-9A-F]{6,6}$/i || $bg !~ /^\#[0-9A-F]{6,6}$/i) {
|
|
return $fail->(203, "Invalid color values");
|
|
}
|
|
|
|
my $row = LJ::load_user($aname);
|
|
|
|
# XXX - on some errors we fail out, on others we continue and try adding
|
|
# any other users in the request. also, error message for redirect should
|
|
# point the user to the redirected username.
|
|
if (! $row) {
|
|
$error_flag = 1;
|
|
} elsif ($row->{'journaltype'} eq "R") {
|
|
return $fail->(154);
|
|
} elsif ($row->{'statusvis'} ne "V") {
|
|
$error_flag = 1;
|
|
} else {
|
|
$friends_added++;
|
|
my $added = { 'username' => $aname,
|
|
'fullname' => $row->{'name'},
|
|
};
|
|
if ($req->{'ver'} >= 1) {
|
|
LJ::text_out(\$added->{'fullname'});
|
|
}
|
|
push @{$res->{'added'}}, $added;
|
|
|
|
my $qfg = LJ::color_todb($fg);
|
|
my $qbg = LJ::color_todb($bg);
|
|
|
|
my $friendid = $row->{'userid'};
|
|
|
|
my $gmask = $fa->{'groupmask'};
|
|
if (! $gmask && $curfriend{$aname}) {
|
|
# if no group mask sent, use the existing one if this is an existing friend
|
|
# TAG:FR:protocol:editfriends3_getmask
|
|
my $sth = $dbh->prepare("SELECT groupmask FROM friends ".
|
|
"WHERE userid=$userid AND friendid=$friendid");
|
|
$sth->execute;
|
|
$gmask = $sth->fetchrow_array;
|
|
}
|
|
# force bit 0 on.
|
|
$gmask |= 1;
|
|
|
|
# TAG:FR:protocol:editfriends4_addeditfriend
|
|
$sth = $dbh->prepare("REPLACE INTO friends (userid, friendid, fgcolor, bgcolor, groupmask) ".
|
|
"VALUES ($userid, $friendid, $qfg, $qbg, $gmask)");
|
|
$sth->execute;
|
|
|
|
unless ($dbh->err) {
|
|
my $memkey = [$userid,"frgmask:$userid:$friendid"];
|
|
LJ::MemCache::set($memkey, $gmask+0, time()+60*15);
|
|
LJ::memcache_kill($friendid, 'friendofs');
|
|
}
|
|
return $fail->(501,$dbh->errstr) if $dbh->err;
|
|
}
|
|
}
|
|
|
|
return $fail->(104) if $error_flag;
|
|
|
|
# invalidate memcache of friends
|
|
LJ::memcache_kill($userid, "friends");
|
|
LJ::mark_dirty($userid, "friends");
|
|
|
|
return $res;
|
|
}
|
|
|
|
sub editfriendgroups
|
|
{
|
|
my ($req, $err, $flags) = @_;
|
|
return undef unless authenticate($req, $err, $flags);
|
|
|
|
my $u = $flags->{'u'};
|
|
my $userid = $u->{'userid'};
|
|
my ($db, $fgtable, $bmax, $cmax) = $u->{dversion} > 5 ?
|
|
($u->writer, 'friendgroup2', LJ::BMAX_GRPNAME2, LJ::CMAX_GRPNAME2) :
|
|
(LJ::get_db_writer(), 'friendgroup', LJ::BMAX_GRPNAME, LJ::CMAX_GRPNAME);
|
|
my $sth;
|
|
|
|
return fail($err,306) unless $db;
|
|
|
|
# do not let locked people do this
|
|
return fail($err, 308) if $u->{statusvis} eq 'L';
|
|
|
|
my $res = {};
|
|
|
|
## make sure tree is how we want it
|
|
$req->{'groupmasks'} = {} unless
|
|
(ref $req->{'groupmasks'} eq "HASH");
|
|
$req->{'set'} = {} unless
|
|
(ref $req->{'set'} eq "HASH");
|
|
$req->{'delete'} = [] unless
|
|
(ref $req->{'delete'} eq "ARRAY");
|
|
|
|
# Keep track of what bits are already set, so we can know later
|
|
# whether to INSERT or UPDATE.
|
|
my %bitset;
|
|
my $groups = LJ::get_friend_group($userid);
|
|
foreach my $bit (keys %{$groups || {}}) {
|
|
$bitset{$bit} = 1;
|
|
}
|
|
|
|
## before we perform any DB operations, validate input text
|
|
# (groups' names) for correctness so we can fail gracefully
|
|
if ($LJ::UNICODE) {
|
|
foreach my $bit (keys %{$req->{'set'}})
|
|
{
|
|
my $name = $req->{'set'}->{$bit}->{'name'};
|
|
return fail($err,207,"non-ASCII names require a Unicode-capable client")
|
|
if $req->{'ver'} < 1 and not LJ::is_ascii($name);
|
|
return fail($err,208,"Invalid group names. Please see $LJ::SITEROOT/support/encodings.bml for more information.")
|
|
unless LJ::text_in($name);
|
|
}
|
|
}
|
|
|
|
## figure out deletions we'll do later
|
|
foreach my $bit (@{$req->{'delete'}})
|
|
{
|
|
$bit += 0;
|
|
next unless ($bit >= 1 && $bit <= 30);
|
|
$bitset{$bit} = 0; # so later we replace into, not update.
|
|
}
|
|
|
|
## do additions/modifications ('set' hash)
|
|
my %added;
|
|
foreach my $bit (keys %{$req->{'set'}})
|
|
{
|
|
$bit += 0;
|
|
next unless ($bit >= 1 && $bit <= 30);
|
|
my $sa = $req->{'set'}->{$bit};
|
|
my $name = LJ::text_trim($sa->{'name'}, $bmax, $cmax);
|
|
|
|
# can't end with a slash
|
|
$name =~ s!/$!!;
|
|
|
|
# setting it to name is like deleting it.
|
|
unless ($name =~ /\S/) {
|
|
push @{$req->{'delete'}}, $bit;
|
|
next;
|
|
}
|
|
|
|
my $qname = $db->quote($name);
|
|
my $qsort = defined $sa->{'sort'} ? ($sa->{'sort'}+0) : 50;
|
|
my $qpublic = $db->quote(defined $sa->{'public'} ? ($sa->{'public'}+0) : 0);
|
|
|
|
if ($bitset{$bit}) {
|
|
# so update it
|
|
my $sets;
|
|
if (defined $sa->{'public'}) {
|
|
$sets .= ", is_public=$qpublic";
|
|
}
|
|
$db->do("UPDATE $fgtable SET groupname=$qname, sortorder=$qsort ".
|
|
"$sets WHERE userid=$userid AND groupnum=$bit");
|
|
} else {
|
|
$db->do("REPLACE INTO $fgtable (userid, groupnum, ".
|
|
"groupname, sortorder, is_public) VALUES ".
|
|
"($userid, $bit, $qname, $qsort, $qpublic)");
|
|
}
|
|
$added{$bit} = 1;
|
|
}
|
|
|
|
|
|
## do deletions ('delete' array)
|
|
my $dbcm = LJ::get_cluster_master($u);
|
|
|
|
# ignore bits that aren't integers or that are outside 1-30 range
|
|
my @delete_bits = grep {$_ >= 1 and $_ <= 30} map {$_+0} @{$req->{'delete'}};
|
|
my $delete_mask = 0;
|
|
foreach my $bit (@delete_bits) {
|
|
$delete_mask |= (1 << $bit)
|
|
}
|
|
|
|
# remove the bits for deleted groups from all friends groupmasks
|
|
my $dbh = LJ::get_db_writer();
|
|
if ($delete_mask) {
|
|
# TAG:FR:protocol:editfriendgroups_removemasks
|
|
$dbh->do("UPDATE friends".
|
|
" SET groupmask = groupmask & ~$delete_mask".
|
|
" WHERE userid = $userid");
|
|
}
|
|
|
|
foreach my $bit (@delete_bits)
|
|
{
|
|
# remove all posts from allowing that group:
|
|
my @posts_to_clean = ();
|
|
$sth = $dbcm->prepare("SELECT jitemid FROM logsec2 WHERE journalid=$userid AND allowmask & (1 << $bit)");
|
|
$sth->execute;
|
|
while (my ($id) = $sth->fetchrow_array) { push @posts_to_clean, $id; }
|
|
while (@posts_to_clean) {
|
|
my @batch;
|
|
if (scalar(@posts_to_clean) < 20) {
|
|
@batch = @posts_to_clean;
|
|
@posts_to_clean = ();
|
|
} else {
|
|
@batch = splice(@posts_to_clean, 0, 20);
|
|
}
|
|
|
|
my $in = join(",", @batch);
|
|
$u->do("UPDATE log2 SET allowmask=allowmask & ~(1 << $bit) ".
|
|
"WHERE journalid=$userid AND jitemid IN ($in) AND security='usemask'");
|
|
$u->do("UPDATE logsec2 SET allowmask=allowmask & ~(1 << $bit) ".
|
|
"WHERE journalid=$userid AND jitemid IN ($in)");
|
|
|
|
foreach my $id (@batch) {
|
|
LJ::MemCache::delete([$userid, "log2:$userid:$id"]);
|
|
}
|
|
LJ::MemCache::delete([$userid, "log2lt:$userid"]);
|
|
}
|
|
LJ::Tags::deleted_friend_group($u, $bit);
|
|
LJ::run_hooks('delete_friend_group', $u, $bit);
|
|
|
|
# remove the friend group, unless we just added it this transaction
|
|
unless ($added{$bit}) {
|
|
$db->do("DELETE FROM $fgtable WHERE ".
|
|
"userid=$userid AND groupnum=$bit");
|
|
}
|
|
}
|
|
|
|
## change friends' masks
|
|
# TAG:FR:protocol:editfriendgroups_changemasks
|
|
foreach my $friend (keys %{$req->{'groupmasks'}})
|
|
{
|
|
my $mask = int($req->{'groupmasks'}->{$friend}) | 1;
|
|
my $friendid = LJ::get_userid($dbh, $friend);
|
|
|
|
$dbh->do("UPDATE friends SET groupmask=$mask ".
|
|
"WHERE userid=$userid AND friendid=?",
|
|
undef, $friendid);
|
|
LJ::MemCache::set([$userid, "frgmask:$userid:$friendid"], $mask);
|
|
}
|
|
|
|
# invalidate memcache of friends/groups
|
|
LJ::memcache_kill($userid, "friends");
|
|
LJ::memcache_kill($userid, "fgrp");
|
|
LJ::mark_dirty($u, "friends");
|
|
|
|
# return value for this is nothing.
|
|
return {};
|
|
}
|
|
|
|
sub sessionexpire {
|
|
my ($req, $err, $flags) = @_;
|
|
return undef unless authenticate($req, $err, $flags);
|
|
my $u = $flags->{u};
|
|
|
|
# expunge one? or all?
|
|
if ($req->{expireall}) {
|
|
$u->kill_all_sessions;
|
|
return {};
|
|
}
|
|
|
|
# just expire a list
|
|
my $list = $req->{expire} || [];
|
|
return {} unless @$list;
|
|
return fail($err,502) unless $u->writer;
|
|
$u->kill_sessions(@$list);
|
|
return {};
|
|
}
|
|
|
|
sub sessiongenerate {
|
|
# generate a session
|
|
my ($req, $err, $flags) = @_;
|
|
return undef unless authenticate($req, $err, $flags);
|
|
|
|
# sanitize input
|
|
$req->{expiration} = 'short' unless $req->{expiration} eq 'long';
|
|
my $boundip;
|
|
$boundip = LJ::get_remote_ip() if $req->{bindtoip};
|
|
|
|
my $u = $flags->{u};
|
|
my $sess_opts = {
|
|
exptype => $req->{expiration},
|
|
ipfixed => $boundip,
|
|
};
|
|
|
|
# do not let locked people do this
|
|
return fail($err, 308) if $u->{statusvis} eq 'L';
|
|
|
|
my $sess = $u->generate_session($sess_opts);
|
|
|
|
# return our hash
|
|
return {
|
|
ljsession => "ws:$u->{user}:$sess->{sessid}:$sess->{auth}",
|
|
};
|
|
}
|
|
|
|
sub list_friends
|
|
{
|
|
my ($u, $opts) = @_;
|
|
|
|
my %hide_fo; # userid -> 1
|
|
if ($LJ::HIDE_FRIENDOF_VIA_BAN) {
|
|
if (my $list = LJ::load_rel_user($u, 'B')) {
|
|
$hide_fo{$_} = 1 foreach @$list;
|
|
}
|
|
}
|
|
|
|
# TAG:FR:protocol:list_friends
|
|
my $sql;
|
|
unless ($opts->{'friendof'}) {
|
|
$sql = "SELECT friendid, fgcolor, bgcolor, groupmask FROM friends WHERE userid=?";
|
|
} else {
|
|
$sql = "SELECT userid FROM friends WHERE friendid=?";
|
|
}
|
|
|
|
my $dbr = LJ::get_db_reader();
|
|
my $sth = $dbr->prepare($sql);
|
|
$sth->execute($u->{'userid'});
|
|
|
|
my @frow;
|
|
while (my @row = $sth->fetchrow_array) {
|
|
next if $hide_fo{$row[0]};
|
|
push @frow, [ @row ];
|
|
}
|
|
|
|
my $us = LJ::load_userids(map { $_->[0] } @frow);
|
|
my $limitnum = $opts->{'limit'}+0;
|
|
|
|
my $res = [];
|
|
foreach my $f (sort { $us->{$a->[0]}{'user'} cmp $us->{$b->[0]}{'user'} }
|
|
grep { $us->{$_->[0]} } @frow)
|
|
{
|
|
my $u = $us->{$f->[0]};
|
|
next if $opts->{'friendof'} && $u->{'statusvis'} ne 'V';
|
|
|
|
my $r = {
|
|
'username' => $u->{'user'},
|
|
'fullname' => $u->{'name'},
|
|
};
|
|
|
|
if ($opts->{'includebdays'} &&
|
|
$u->{'bdate'} &&
|
|
$u->{'bdate'} ne "0000-00-00" &&
|
|
$u->{'allow_infoshow'} eq 'Y')
|
|
{
|
|
$r->{'birthday'} = $u->{'bdate'};
|
|
}
|
|
|
|
unless ($opts->{'friendof'}) {
|
|
$r->{'fgcolor'} = LJ::color_fromdb($f->[1]);
|
|
$r->{'bgcolor'} = LJ::color_fromdb($f->[2]);
|
|
$r->{"groupmask"} = $f->[3] if $f->[3] != 1;
|
|
} else {
|
|
$r->{'fgcolor'} = "#000000";
|
|
$r->{'bgcolor'} = "#ffffff";
|
|
}
|
|
|
|
$r->{"type"} = {
|
|
'C' => 'community',
|
|
'Y' => 'syndicated',
|
|
'N' => 'news',
|
|
'S' => 'shared',
|
|
'I' => 'identity',
|
|
}->{$u->{'journaltype'}} if $u->{'journaltype'} ne 'P';
|
|
|
|
$r->{"status"} = {
|
|
'D' => "deleted",
|
|
'S' => "suspended",
|
|
'X' => "purged",
|
|
}->{$u->{'statusvis'}} if $u->{'statusvis'} ne 'V';
|
|
|
|
push @$res, $r;
|
|
# won't happen for zero limit (which means no limit)
|
|
last if @$res == $limitnum;
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
sub syncitems
|
|
{
|
|
my ($req, $err, $flags) = @_;
|
|
return undef unless authenticate($req, $err, $flags);
|
|
return undef unless check_altusage($req, $err, $flags);
|
|
return fail($err,506) if $LJ::DISABLED{'syncitems'};
|
|
|
|
my $ownerid = $flags->{'ownerid'};
|
|
my $uowner = $flags->{'u_owner'} || $flags->{'u'};
|
|
my $sth;
|
|
|
|
my $db = LJ::get_cluster_reader($uowner);
|
|
return fail($err,502) unless $db;
|
|
|
|
## have a valid date?
|
|
my $date = $req->{'lastsync'};
|
|
if ($date) {
|
|
return fail($err,203,"Invalid date format")
|
|
unless ($date =~ /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/);
|
|
} else {
|
|
$date = "0000-00-00 00:00:00";
|
|
}
|
|
|
|
my $LIMIT = 500;
|
|
|
|
my %item;
|
|
$sth = $db->prepare("SELECT jitemid, logtime FROM log2 WHERE ".
|
|
"journalid=? and logtime > ?");
|
|
$sth->execute($ownerid, $date);
|
|
while (my ($id, $dt) = $sth->fetchrow_array) {
|
|
$item{$id} = [ 'L', $id, $dt, "create" ];
|
|
}
|
|
|
|
my %cmt;
|
|
my $p_calter = LJ::get_prop("log", "commentalter");
|
|
my $p_revtime = LJ::get_prop("log", "revtime");
|
|
$sth = $db->prepare("SELECT jitemid, propid, FROM_UNIXTIME(value) ".
|
|
"FROM logprop2 WHERE journalid=? ".
|
|
"AND propid IN ($p_calter->{'id'}, $p_revtime->{'id'}) ".
|
|
"AND value+0 > UNIX_TIMESTAMP(?)");
|
|
$sth->execute($ownerid, $date);
|
|
while (my ($id, $prop, $dt) = $sth->fetchrow_array) {
|
|
if ($prop == $p_calter->{'id'}) {
|
|
$cmt{$id} = [ 'C', $id, $dt, "update" ];
|
|
} elsif ($prop == $p_revtime->{'id'}) {
|
|
$item{$id} = [ 'L', $id, $dt, "update" ];
|
|
}
|
|
}
|
|
|
|
my @ev = sort { $a->[2] cmp $b->[2] } (values %item, values %cmt);
|
|
|
|
my $res = {};
|
|
my $list = $res->{'syncitems'} = [];
|
|
$res->{'total'} = scalar @ev;
|
|
my $ct = 0;
|
|
while (my $ev = shift @ev) {
|
|
$ct++;
|
|
push @$list, { 'item' => "$ev->[0]-$ev->[1]",
|
|
'time' => $ev->[2],
|
|
'action' => $ev->[3], };
|
|
last if $ct >= $LIMIT;
|
|
}
|
|
$res->{'count'} = $ct;
|
|
return $res;
|
|
}
|
|
|
|
sub consolecommand
|
|
{
|
|
my ($req, $err, $flags) = @_;
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
return fail($err,502) unless $dbh;
|
|
|
|
# logging in isn't necessary, but most console commands do require it
|
|
my $remote = undef;
|
|
$remote = $flags->{'u'} if authenticate($req, $err, $flags);
|
|
|
|
# underage users can't do this, since we don't want to sanitize for
|
|
# what in particular they're trying to do, might as well disallow it
|
|
return fail($err, 310) if $remote->underage;
|
|
|
|
# do not let locked people do this
|
|
return fail($err, 308) if $remote->{statusvis} eq 'L';
|
|
|
|
my $res = {};
|
|
my $cmdout = $res->{'results'} = [];
|
|
|
|
foreach my $cmd (@{$req->{'commands'}})
|
|
{
|
|
# callee can pre-parse the args, or we can do it bash-style
|
|
$cmd = [ LJ::Con::parse_line($cmd) ] unless (ref $cmd eq "ARRAY");
|
|
|
|
my @output;
|
|
my $rv = LJ::Con::execute($dbh, $remote, $cmd, \@output);
|
|
push @{$cmdout}, {
|
|
'success' => $rv,
|
|
'output' => \@output,
|
|
};
|
|
}
|
|
|
|
return $res;
|
|
}
|
|
|
|
sub getchallenge
|
|
{
|
|
my ($req, $err, $flags) = @_;
|
|
my $res = {};
|
|
my $now = time();
|
|
my $etime = 60;
|
|
$res->{'challenge'} = LJ::challenge_generate($etime);
|
|
$res->{'server_time'} = $now;
|
|
$res->{'expire_time'} = $now + $etime;
|
|
$res->{'auth_scheme'} = "c0"; # fixed for now, might support others later
|
|
return $res;
|
|
}
|
|
|
|
sub login_message
|
|
{
|
|
my ($req, $res, $flags) = @_;
|
|
my $u = $flags->{'u'};
|
|
|
|
my $msg = sub {
|
|
my $code = shift;
|
|
my $args = shift || {};
|
|
$args->{'sitename'} = $LJ::SITENAME;
|
|
$args->{'siteroot'} = $LJ::SITEROOT;
|
|
$res->{'message'} = translate($u, $code, $args);
|
|
};
|
|
|
|
return $msg->("readonly") if LJ::get_cap($u, "readonly");
|
|
return $msg->("not_validated") if ($u->{'status'} eq "N" and not $LJ::EVERYONE_VALID);
|
|
return $msg->("must_revalidate") if ($u->{'status'} eq "T" and not $LJ::EVERYONE_VALID);
|
|
return $msg->("mail_bouncing") if $u->{'status'} eq "B";
|
|
|
|
my @checkpass = LJ::run_hooks("bad_password", $u);
|
|
return $msg->("bad_password") if (@checkpass and $checkpass[0]->[0]);
|
|
|
|
return $msg->("old_win32_client") if $req->{'clientversion'} =~ /^Win32-MFC\/(1.2.[0123456])$/;
|
|
return $msg->("old_win32_client") if $req->{'clientversion'} =~ /^Win32-MFC\/(1.3.[01234])\b/;
|
|
return $msg->("hello_test") if $u->{'user'} eq "test";
|
|
}
|
|
|
|
sub list_friendgroups
|
|
{
|
|
my $u = shift;
|
|
|
|
# get the groups for this user, return undef if error
|
|
my $groups = LJ::get_friend_group($u);
|
|
return undef unless $groups;
|
|
|
|
# we got all of the groups, so put them into an arrayref sorted by the
|
|
# group sortorder; also note that the map is used to construct a new hashref
|
|
# out of the old group hashref so that we have all of the field names converted
|
|
# to a format our callers can recognize
|
|
my @res = map { { id => $_->{groupnum}, name => $_->{groupname},
|
|
public => $_->{is_public}, sortorder => $_->{sortorder}, } }
|
|
sort { $a->{sortorder} <=> $b->{sortorder} }
|
|
values %$groups;
|
|
|
|
return \@res;
|
|
}
|
|
|
|
sub list_usejournals
|
|
{
|
|
my $u = shift;
|
|
|
|
my @res;
|
|
|
|
my $ids = LJ::load_rel_target($u, 'P');
|
|
my $us = LJ::load_userids(@$ids);
|
|
foreach (values %$us) {
|
|
next unless $_->{'statusvis'} eq "V";
|
|
push @res, $_->{user};
|
|
}
|
|
@res = sort @res;
|
|
return \@res;
|
|
}
|
|
|
|
sub hash_menus
|
|
{
|
|
my $u = shift;
|
|
my $user = $u->{'user'};
|
|
|
|
my $menu = [
|
|
{ 'text' => "Recent Entries",
|
|
'url' => "$LJ::SITEROOT/users/$user/", },
|
|
{ 'text' => "Calendar View",
|
|
'url' => "$LJ::SITEROOT/users/$user/calendar", },
|
|
{ 'text' => "Friends View",
|
|
'url' => "$LJ::SITEROOT/users/$user/friends", },
|
|
{ 'text' => "-", },
|
|
{ 'text' => "Your Profile",
|
|
'url' => "$LJ::SITEROOT/userinfo.bml?user=$user", },
|
|
{ 'text' => "Your To-Do List",
|
|
'url' => "$LJ::SITEROOT/todo/?user=$user", },
|
|
{ 'text' => "-", },
|
|
{ 'text' => "Change Settings",
|
|
'sub' => [ { 'text' => "Personal Info",
|
|
'url' => "$LJ::SITEROOT/editinfo.bml", },
|
|
{ 'text' => "Journal Settings",
|
|
'url' =>"$LJ::SITEROOT/modify.bml", }, ] },
|
|
{ 'text' => "-", },
|
|
{ 'text' => "Support",
|
|
'url' => "$LJ::SITEROOT/support/", }
|
|
];
|
|
|
|
LJ::run_hooks("modify_login_menu", {
|
|
'menu' => $menu,
|
|
'u' => $u,
|
|
'user' => $user,
|
|
});
|
|
|
|
return $menu;
|
|
}
|
|
|
|
sub list_pickws
|
|
{
|
|
my $u = shift;
|
|
|
|
my $pi = LJ::get_userpic_info($u);
|
|
my @res;
|
|
|
|
# FIXME: should be a utf-8 sort
|
|
foreach my $kw (sort keys %{$pi->{'kw'}}) {
|
|
my $pic = $pi->{'kw'}{$kw};
|
|
next if $pic->{'state'} eq "I";
|
|
push @res, [ $kw, $pic->{'picid'} ];
|
|
}
|
|
|
|
return \@res;
|
|
}
|
|
|
|
sub list_moods
|
|
{
|
|
my $mood_max = int(shift);
|
|
LJ::load_moods();
|
|
|
|
my $res = [];
|
|
return $res if $mood_max >= $LJ::CACHED_MOOD_MAX;
|
|
|
|
for (my $id = $mood_max+1; $id <= $LJ::CACHED_MOOD_MAX; $id++) {
|
|
next unless defined $LJ::CACHE_MOODS{$id};
|
|
my $mood = $LJ::CACHE_MOODS{$id};
|
|
next unless $mood->{'name'};
|
|
push @$res, { 'id' => $id,
|
|
'name' => $mood->{'name'},
|
|
'parent' => $mood->{'parent'} };
|
|
}
|
|
|
|
return $res;
|
|
}
|
|
|
|
sub check_altusage
|
|
{
|
|
my ($req, $err, $flags) = @_;
|
|
|
|
# see note in ljlib.pl::can_use_journal about why we return
|
|
# both 'ownerid' and 'u_owner' in $flags
|
|
|
|
my $alt = $req->{'usejournal'};
|
|
my $u = $flags->{'u'};
|
|
$flags->{'ownerid'} = $u->{'userid'};
|
|
|
|
# all good if not using an alt journal
|
|
return 1 unless $alt;
|
|
|
|
# complain if the username is invalid
|
|
return fail($err,206) unless LJ::canonical_username($alt);
|
|
|
|
my $r = eval { Apache->request };
|
|
|
|
# allow usage if we're told explicitly that it's okay
|
|
if ($flags->{'usejournal_okay'}) {
|
|
$flags->{'u_owner'} = LJ::load_user($alt);
|
|
$flags->{'ownerid'} = $flags->{'u_owner'}->{'userid'};
|
|
$r->notes("journalid" => $flags->{'ownerid'}) if $r && !$r->notes("journalid");
|
|
return 1 if $flags->{'ownerid'};
|
|
return fail($err,206);
|
|
}
|
|
|
|
# otherwise, check for access:
|
|
my $info = {};
|
|
my $canuse = LJ::can_use_journal($u->{'userid'}, $alt, $info);
|
|
$flags->{'ownerid'} = $info->{'ownerid'};
|
|
$flags->{'u_owner'} = $info->{'u_owner'};
|
|
$r->notes("journalid" => $flags->{'ownerid'}) if $r && !$r->notes("journalid");
|
|
|
|
return 1 if $canuse || $flags->{'ignorecanuse'};
|
|
|
|
# not allowed to access it
|
|
return fail($err,300);
|
|
}
|
|
|
|
sub authenticate
|
|
{
|
|
my ($req, $err, $flags) = @_;
|
|
|
|
my $username = $req->{'username'};
|
|
return fail($err,200) unless $username;
|
|
return fail($err,100) unless LJ::canonical_username($username);
|
|
|
|
my $u = $flags->{'u'};
|
|
unless ($u) {
|
|
my $dbr = LJ::get_db_reader();
|
|
return fail($err,502) unless $dbr;
|
|
$u = LJ::load_user($username);
|
|
}
|
|
|
|
return fail($err,100) unless $u;
|
|
return fail($err,100) if ($u->{'statusvis'} eq "X");
|
|
return fail($err,505) unless $u->{'clusterid'};
|
|
|
|
my $r = eval { Apache->request };
|
|
my $ip;
|
|
if ($r) {
|
|
$r->notes("ljuser" => $u->{'user'}) unless $r->notes("ljuser");
|
|
$r->notes("journalid" => $u->{'userid'}) unless $r->notes("journalid");
|
|
$ip = $r->connection->remote_ip;
|
|
}
|
|
|
|
my $ip_banned = 0;
|
|
my $chal_expired = 0;
|
|
my $auth_check = sub {
|
|
my $auth_meth = $req->{'auth_method'} || "clear";
|
|
if ($auth_meth eq "clear") {
|
|
return LJ::auth_okay($u,
|
|
$req->{'password'},
|
|
$req->{'hpassword'},
|
|
$u->{'password'},
|
|
\$ip_banned);
|
|
}
|
|
if ($auth_meth eq "challenge") {
|
|
my $chal_opts = {};
|
|
my $chall_ok = LJ::challenge_check_login($u,
|
|
$req->{'auth_challenge'},
|
|
$req->{'auth_response'},
|
|
\$ip_banned,
|
|
$chal_opts);
|
|
$chal_expired = 1 if $chal_opts->{expired};
|
|
return $chall_ok;
|
|
}
|
|
if ($auth_meth eq "cookie") {
|
|
return unless $r && $r->header_in("X-LJ-Auth") eq "cookie";
|
|
my $remote = LJ::get_remote();
|
|
return $remote && $remote->{'user'} eq $username ? 1 : 0;
|
|
}
|
|
};
|
|
|
|
# predefined allowed auths (no pw required)
|
|
my $post_wo_auth = $LJ::POST_WITHOUT_AUTH{$ip};
|
|
$flags->{'noauth'} = 1 if
|
|
$post_wo_auth &&
|
|
$post_wo_auth->{ $req->{usejournal} } &&
|
|
grep { $_ eq $req->{user} } @{ $post_wo_auth->{ $req->{usejournal} } };
|
|
|
|
unless ($flags->{'nopassword'} ||
|
|
$flags->{'noauth'} ||
|
|
$auth_check->() )
|
|
{
|
|
return fail($err,402) if $ip_banned;
|
|
return fail($err,105) if $chal_expired;
|
|
return fail($err,101);
|
|
}
|
|
|
|
# if there is a require TOS revision, check for it now
|
|
return fail($err, 156, LJ::tosagree_str('protocol' => 'text'))
|
|
unless $u->tosagree_verify;
|
|
|
|
# remember the user record for later.
|
|
$flags->{'u'} = $u;
|
|
return 1;
|
|
}
|
|
|
|
sub fail
|
|
{
|
|
my $err = shift;
|
|
my $code = shift;
|
|
my $des = shift;
|
|
$code .= ":$des" if $des;
|
|
$$err = $code if (ref $err eq "SCALAR");
|
|
return undef;
|
|
}
|
|
|
|
# PROBLEM: a while back we used auto_increment fields in our tables so that we could have
|
|
# automatically incremented itemids and such. this was eventually phased out in favor of
|
|
# the more portable alloc_user_counter function which uses the 'counter' table. when the
|
|
# counter table has no data, it finds the highest id already in use in the database and adds
|
|
# one to it.
|
|
#
|
|
# a problem came about when users who last posted before alloc_user_counter went
|
|
# and deleted all their entries and posted anew. alloc_user_counter would find no entries,
|
|
# this no ids, and thus assign id 1, thinking it's all clean and new. but, id 1 had been
|
|
# used previously, and now has comments attached to it.
|
|
#
|
|
# the comments would happen because there was an old bug that wouldn't delete comments when
|
|
# an entry was deleted. this has since been fixed. so this all combines to make this
|
|
# a necessity, at least until no buggy data exist anymore!
|
|
#
|
|
# this code here removes any comments that happen to exist for the id we're now using.
|
|
sub new_entry_cleanup_hack {
|
|
my ($u, $jitemid) = @_;
|
|
|
|
# sanitize input
|
|
$jitemid += 0;
|
|
return unless $jitemid;
|
|
my $ownerid = LJ::want_userid($u);
|
|
return unless $ownerid;
|
|
|
|
# delete logprops
|
|
$u->do("DELETE FROM logprop2 WHERE journalid=$ownerid AND jitemid=$jitemid");
|
|
|
|
# delete comments
|
|
my $ids = LJ::Talk::get_talk_data($u, 'L', $jitemid);
|
|
return unless ref $ids eq 'HASH' && %$ids;
|
|
my $list = join ',', map { $_+0 } keys %$ids;
|
|
$u->do("DELETE FROM talk2 WHERE journalid=$ownerid AND jtalkid IN ($list)");
|
|
$u->do("DELETE FROM talktext2 WHERE journalid=$ownerid AND jtalkid IN ($list)");
|
|
$u->do("DELETE FROM talkprop2 WHERE journalid=$ownerid AND jtalkid IN ($list)");
|
|
}
|
|
|
|
#### Old interface (flat key/values) -- wrapper aruond LJ::Protocol
|
|
package LJ;
|
|
|
|
sub do_request
|
|
{
|
|
# get the request and response hash refs
|
|
my ($req, $res, $flags) = @_;
|
|
|
|
# initialize some stuff
|
|
%{$res} = (); # clear the given response hash
|
|
$flags = {} unless (ref $flags eq "HASH");
|
|
|
|
# did they send a mode?
|
|
unless ($req->{'mode'}) {
|
|
$res->{'success'} = "FAIL";
|
|
$res->{'errmsg'} = "Client error: No mode specified.";
|
|
return;
|
|
}
|
|
|
|
# this method doesn't require auth
|
|
if ($req->{'mode'} eq "getchallenge") {
|
|
return getchallenge($req, $res, $flags);
|
|
}
|
|
|
|
# mode from here on out require a username
|
|
my $user = LJ::canonical_username($req->{'user'});
|
|
unless ($user) {
|
|
$res->{'success'} = "FAIL";
|
|
$res->{'errmsg'} = "Client error: No username sent.";
|
|
return;
|
|
}
|
|
|
|
### see if the server's under maintenance now
|
|
if ($LJ::SERVER_DOWN) {
|
|
$res->{'success'} = "FAIL";
|
|
$res->{'errmsg'} = $LJ::SERVER_DOWN_MESSAGE;
|
|
return;
|
|
}
|
|
|
|
## dispatch wrappers
|
|
if ($req->{'mode'} eq "login") {
|
|
return login($req, $res, $flags);
|
|
}
|
|
if ($req->{'mode'} eq "getfriendgroups") {
|
|
return getfriendgroups($req, $res, $flags);
|
|
}
|
|
if ($req->{'mode'} eq "getfriends") {
|
|
return getfriends($req, $res, $flags);
|
|
}
|
|
if ($req->{'mode'} eq "friendof") {
|
|
return friendof($req, $res, $flags);
|
|
}
|
|
if ($req->{'mode'} eq "checkfriends") {
|
|
return checkfriends($req, $res, $flags);
|
|
}
|
|
if ($req->{'mode'} eq "getdaycounts") {
|
|
return getdaycounts($req, $res, $flags);
|
|
}
|
|
if ($req->{'mode'} eq "postevent") {
|
|
return postevent($req, $res, $flags);
|
|
}
|
|
if ($req->{'mode'} eq "editevent") {
|
|
return editevent($req, $res, $flags);
|
|
}
|
|
if ($req->{'mode'} eq "syncitems") {
|
|
return syncitems($req, $res, $flags);
|
|
}
|
|
if ($req->{'mode'} eq "getevents") {
|
|
return getevents($req, $res, $flags);
|
|
}
|
|
if ($req->{'mode'} eq "editfriends") {
|
|
return editfriends($req, $res, $flags);
|
|
}
|
|
if ($req->{'mode'} eq "editfriendgroups") {
|
|
return editfriendgroups($req, $res, $flags);
|
|
}
|
|
if ($req->{'mode'} eq "consolecommand") {
|
|
return consolecommand($req, $res, $flags);
|
|
}
|
|
if ($req->{'mode'} eq "sessiongenerate") {
|
|
return sessiongenerate($req, $res, $flags);
|
|
}
|
|
if ($req->{'mode'} eq "sessionexpire") {
|
|
return sessionexpire($req, $res, $flags);
|
|
}
|
|
|
|
### unknown mode!
|
|
$res->{'success'} = "FAIL";
|
|
$res->{'errmsg'} = "Client error: Unknown mode ($req->{'mode'})";
|
|
return;
|
|
}
|
|
|
|
## flat wrapper
|
|
sub login
|
|
{
|
|
my ($req, $res, $flags) = @_;
|
|
|
|
my $err = 0;
|
|
my $rq = upgrade_request($req);
|
|
|
|
my $rs = LJ::Protocol::do_request("login", $rq, \$err, $flags);
|
|
unless ($rs) {
|
|
$res->{'success'} = "FAIL";
|
|
$res->{'errmsg'} = LJ::Protocol::error_message($err);
|
|
return 0;
|
|
}
|
|
|
|
$res->{'success'} = "OK";
|
|
$res->{'name'} = $rs->{'fullname'};
|
|
$res->{'message'} = $rs->{'message'} if $rs->{'message'};
|
|
$res->{'fastserver'} = 1 if $rs->{'fastserver'};
|
|
|
|
# shared journals
|
|
my $access_count = 0;
|
|
foreach my $user (@{$rs->{'usejournals'}}) {
|
|
$access_count++;
|
|
$res->{"access_${access_count}"} = $user;
|
|
}
|
|
if ($access_count) {
|
|
$res->{"access_count"} = $access_count;
|
|
}
|
|
|
|
# friend groups
|
|
populate_friend_groups($res, $rs->{'friendgroups'});
|
|
|
|
my $flatten = sub {
|
|
my ($prefix, $listref) = @_;
|
|
my $ct = 0;
|
|
foreach (@$listref) {
|
|
$ct++;
|
|
$res->{"${prefix}_$ct"} = $_;
|
|
}
|
|
$res->{"${prefix}_count"} = $ct;
|
|
};
|
|
|
|
### picture keywords
|
|
$flatten->("pickw", $rs->{'pickws'})
|
|
if defined $req->{"getpickws"};
|
|
$flatten->("pickwurl", $rs->{'pickwurls'})
|
|
if defined $req->{"getpickwurls"};
|
|
$res->{'defaultpicurl'} = $rs->{'defaultpicurl'} if $rs->{'defaultpicurl'};
|
|
|
|
### report new moods that this client hasn't heard of, if they care
|
|
if (defined $req->{"getmoods"}) {
|
|
my $mood_count = 0;
|
|
foreach my $m (@{$rs->{'moods'}}) {
|
|
$mood_count++;
|
|
$res->{"mood_${mood_count}_id"} = $m->{'id'};
|
|
$res->{"mood_${mood_count}_name"} = $m->{'name'};
|
|
$res->{"mood_${mood_count}_parent"} = $m->{'parent'};
|
|
}
|
|
if ($mood_count) {
|
|
$res->{"mood_count"} = $mood_count;
|
|
}
|
|
}
|
|
|
|
#### send web menus
|
|
if ($req->{"getmenus"} == 1) {
|
|
my $menu = $rs->{'menus'};
|
|
my $menu_num = 0;
|
|
populate_web_menu($res, $menu, \$menu_num);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
## flat wrapper
|
|
sub getfriendgroups
|
|
{
|
|
my ($req, $res, $flags) = @_;
|
|
|
|
my $err = 0;
|
|
my $rq = upgrade_request($req);
|
|
|
|
my $rs = LJ::Protocol::do_request("getfriendgroups", $rq, \$err, $flags);
|
|
unless ($rs) {
|
|
$res->{'success'} = "FAIL";
|
|
$res->{'errmsg'} = LJ::Protocol::error_message($err);
|
|
return 0;
|
|
}
|
|
$res->{'success'} = "OK";
|
|
populate_friend_groups($res, $rs->{'friendgroups'});
|
|
|
|
return 1;
|
|
}
|
|
|
|
## flat wrapper
|
|
sub getfriends
|
|
{
|
|
my ($req, $res, $flags) = @_;
|
|
|
|
my $err = 0;
|
|
my $rq = upgrade_request($req);
|
|
|
|
my $rs = LJ::Protocol::do_request("getfriends", $rq, \$err, $flags);
|
|
unless ($rs) {
|
|
$res->{'success'} = "FAIL";
|
|
$res->{'errmsg'} = LJ::Protocol::error_message($err);
|
|
return 0;
|
|
}
|
|
|
|
$res->{'success'} = "OK";
|
|
if ($req->{'includegroups'}) {
|
|
populate_friend_groups($res, $rs->{'friendgroups'});
|
|
}
|
|
if ($req->{'includefriendof'}) {
|
|
populate_friends($res, "friendof", $rs->{'friendofs'});
|
|
}
|
|
populate_friends($res, "friend", $rs->{'friends'});
|
|
|
|
return 1;
|
|
}
|
|
|
|
## flat wrapper
|
|
sub friendof
|
|
{
|
|
my ($req, $res, $flags) = @_;
|
|
|
|
my $err = 0;
|
|
my $rq = upgrade_request($req);
|
|
|
|
my $rs = LJ::Protocol::do_request("friendof", $rq, \$err, $flags);
|
|
unless ($rs) {
|
|
$res->{'success'} = "FAIL";
|
|
$res->{'errmsg'} = LJ::Protocol::error_message($err);
|
|
return 0;
|
|
}
|
|
|
|
$res->{'success'} = "OK";
|
|
populate_friends($res, "friendof", $rs->{'friendofs'});
|
|
return 1;
|
|
}
|
|
|
|
## flat wrapper
|
|
sub checkfriends
|
|
{
|
|
my ($req, $res, $flags) = @_;
|
|
|
|
my $err = 0;
|
|
my $rq = upgrade_request($req);
|
|
|
|
my $rs = LJ::Protocol::do_request("checkfriends", $rq, \$err, $flags);
|
|
unless ($rs) {
|
|
$res->{'success'} = "FAIL";
|
|
$res->{'errmsg'} = LJ::Protocol::error_message($err);
|
|
return 0;
|
|
}
|
|
|
|
$res->{'success'} = "OK";
|
|
$res->{'new'} = $rs->{'new'};
|
|
$res->{'lastupdate'} = $rs->{'lastupdate'};
|
|
$res->{'interval'} = $rs->{'interval'};
|
|
return 1;
|
|
}
|
|
|
|
## flat wrapper
|
|
sub getdaycounts
|
|
{
|
|
my ($req, $res, $flags) = @_;
|
|
|
|
my $err = 0;
|
|
my $rq = upgrade_request($req);
|
|
|
|
my $rs = LJ::Protocol::do_request("getdaycounts", $rq, \$err, $flags);
|
|
unless ($rs) {
|
|
$res->{'success'} = "FAIL";
|
|
$res->{'errmsg'} = LJ::Protocol::error_message($err);
|
|
return 0;
|
|
}
|
|
|
|
$res->{'success'} = "OK";
|
|
foreach my $d (@{ $rs->{'daycounts'} }) {
|
|
$res->{$d->{'date'}} = $d->{'count'};
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
## flat wrapper
|
|
sub syncitems
|
|
{
|
|
my ($req, $res, $flags) = @_;
|
|
|
|
my $err = 0;
|
|
my $rq = upgrade_request($req);
|
|
|
|
my $rs = LJ::Protocol::do_request("syncitems", $rq, \$err, $flags);
|
|
unless ($rs) {
|
|
$res->{'success'} = "FAIL";
|
|
$res->{'errmsg'} = LJ::Protocol::error_message($err);
|
|
return 0;
|
|
}
|
|
|
|
$res->{'success'} = "OK";
|
|
$res->{'sync_total'} = $rs->{'total'};
|
|
$res->{'sync_count'} = $rs->{'count'};
|
|
|
|
my $ct = 0;
|
|
foreach my $s (@{ $rs->{'syncitems'} }) {
|
|
$ct++;
|
|
foreach my $a (qw(item action time)) {
|
|
$res->{"sync_${ct}_$a"} = $s->{$a};
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
## flat wrapper: limited functionality. (1 command only, server-parsed only)
|
|
sub consolecommand
|
|
{
|
|
my ($req, $res, $flags) = @_;
|
|
|
|
my $err = 0;
|
|
my $rq = upgrade_request($req);
|
|
delete $rq->{'command'};
|
|
|
|
$rq->{'commands'} = [ $req->{'command'} ];
|
|
|
|
my $rs = LJ::Protocol::do_request("consolecommand", $rq, \$err, $flags);
|
|
unless ($rs) {
|
|
$res->{'success'} = "FAIL";
|
|
$res->{'errmsg'} = LJ::Protocol::error_message($err);
|
|
return 0;
|
|
}
|
|
|
|
$res->{'cmd_success'} = $rs->{'results'}->[0]->{'success'};
|
|
$res->{'cmd_line_count'} = 0;
|
|
foreach my $l (@{$rs->{'results'}->[0]->{'output'}}) {
|
|
$res->{'cmd_line_count'}++;
|
|
my $line = $res->{'cmd_line_count'};
|
|
$res->{"cmd_line_${line}_type"} = $l->[0]
|
|
if $l->[0];
|
|
$res->{"cmd_line_${line}"} = $l->[1];
|
|
}
|
|
|
|
$res->{'success'} = "OK";
|
|
|
|
}
|
|
|
|
## flat wrapper
|
|
sub getchallenge
|
|
{
|
|
my ($req, $res, $flags) = @_;
|
|
my $err = 0;
|
|
my $rs = LJ::Protocol::do_request("getchallenge", $req, \$err, $flags);
|
|
|
|
# stupid copy (could just return $rs), but it might change in the future
|
|
# so this protects us from future accidental harm.
|
|
foreach my $k (qw(challenge server_time expire_time auth_scheme)) {
|
|
$res->{$k} = $rs->{$k};
|
|
}
|
|
|
|
$res->{'success'} = "OK";
|
|
return $res;
|
|
}
|
|
|
|
## flat wrapper
|
|
sub editfriends
|
|
{
|
|
my ($req, $res, $flags) = @_;
|
|
|
|
my $err = 0;
|
|
my $rq = upgrade_request($req);
|
|
|
|
$rq->{'add'} = [];
|
|
$rq->{'delete'} = [];
|
|
|
|
foreach (keys %$req) {
|
|
if (/^editfriend_add_(\d+)_user$/) {
|
|
my $n = $1;
|
|
next unless ($req->{"editfriend_add_${n}_user"} =~ /\S/);
|
|
my $fa = { 'username' => $req->{"editfriend_add_${n}_user"},
|
|
'fgcolor' => $req->{"editfriend_add_${n}_fg"},
|
|
'bgcolor' => $req->{"editfriend_add_${n}_bg"},
|
|
'groupmask' => $req->{"editfriend_add_${n}_groupmask"},
|
|
};
|
|
push @{$rq->{'add'}}, $fa;
|
|
} elsif (/^editfriend_delete_(\w+)$/) {
|
|
push @{$rq->{'delete'}}, $1;
|
|
}
|
|
}
|
|
|
|
my $rs = LJ::Protocol::do_request("editfriends", $rq, \$err, $flags);
|
|
unless ($rs) {
|
|
$res->{'success'} = "FAIL";
|
|
$res->{'errmsg'} = LJ::Protocol::error_message($err);
|
|
return 0;
|
|
}
|
|
|
|
$res->{'success'} = "OK";
|
|
|
|
my $ct = 0;
|
|
foreach my $fa (@{ $rs->{'added'} }) {
|
|
$ct++;
|
|
$res->{"friend_${ct}_user"} = $fa->{'username'};
|
|
$res->{"friend_${ct}_name"} = $fa->{'fullname'};
|
|
}
|
|
|
|
$res->{'friends_added'} = $ct;
|
|
|
|
return 1;
|
|
}
|
|
|
|
## flat wrapper
|
|
sub editfriendgroups
|
|
{
|
|
my ($req, $res, $flags) = @_;
|
|
|
|
my $err = 0;
|
|
my $rq = upgrade_request($req);
|
|
|
|
$rq->{'groupmasks'} = {};
|
|
$rq->{'set'} = {};
|
|
$rq->{'delete'} = [];
|
|
|
|
foreach (keys %$req) {
|
|
if (/^efg_set_(\d+)_name$/) {
|
|
next unless ($req->{$_} ne "");
|
|
my $n = $1;
|
|
my $fs = {
|
|
'name' => $req->{"efg_set_${n}_name"},
|
|
'sort' => $req->{"efg_set_${n}_sort"},
|
|
};
|
|
if (defined $req->{"efg_set_${n}_public"}) {
|
|
$fs->{'public'} = $req->{"efg_set_${n}_public"};
|
|
}
|
|
$rq->{'set'}->{$n} = $fs;
|
|
}
|
|
elsif (/^efg_delete_(\d+)$/) {
|
|
if ($req->{$_}) {
|
|
# delete group if value is true
|
|
push @{$rq->{'delete'}}, $1;
|
|
}
|
|
}
|
|
elsif (/^editfriend_groupmask_(\w+)$/) {
|
|
$rq->{'groupmasks'}->{$1} = $req->{$_};
|
|
}
|
|
}
|
|
|
|
my $rs = LJ::Protocol::do_request("editfriendgroups", $rq, \$err, $flags);
|
|
unless ($rs) {
|
|
$res->{'success'} = "FAIL";
|
|
$res->{'errmsg'} = LJ::Protocol::error_message($err);
|
|
return 0;
|
|
}
|
|
|
|
$res->{'success'} = "OK";
|
|
return 1;
|
|
}
|
|
|
|
sub flatten_props
|
|
{
|
|
my ($req, $rq) = @_;
|
|
|
|
## changes prop_* to props hashref
|
|
foreach my $k (keys %$req) {
|
|
next unless ($k =~ /^prop_(.+)/);
|
|
$rq->{'props'}->{$1} = $req->{$k};
|
|
}
|
|
}
|
|
|
|
## flat wrapper
|
|
sub postevent
|
|
{
|
|
my ($req, $res, $flags) = @_;
|
|
|
|
my $err = 0;
|
|
my $rq = upgrade_request($req);
|
|
flatten_props($req, $rq);
|
|
|
|
my $rs = LJ::Protocol::do_request("postevent", $rq, \$err, $flags);
|
|
unless ($rs) {
|
|
$res->{'success'} = "FAIL";
|
|
$res->{'errmsg'} = LJ::Protocol::error_message($err);
|
|
return 0;
|
|
}
|
|
|
|
$res->{'message'} = $rs->{'message'} if $rs->{'message'};
|
|
$res->{'success'} = "OK";
|
|
$res->{'itemid'} = $rs->{'itemid'};
|
|
$res->{'anum'} = $rs->{'anum'} if defined $rs->{'anum'};
|
|
$res->{'url'} = $rs->{'url'} if defined $rs->{'url'};
|
|
return 1;
|
|
}
|
|
|
|
## flat wrapper
|
|
sub editevent
|
|
{
|
|
my ($req, $res, $flags) = @_;
|
|
|
|
my $err = 0;
|
|
my $rq = upgrade_request($req);
|
|
flatten_props($req, $rq);
|
|
|
|
my $rs = LJ::Protocol::do_request("editevent", $rq, \$err, $flags);
|
|
unless ($rs) {
|
|
$res->{'success'} = "FAIL";
|
|
$res->{'errmsg'} = LJ::Protocol::error_message($err);
|
|
return 0;
|
|
}
|
|
|
|
$res->{'success'} = "OK";
|
|
$res->{'itemid'} = $rs->{'itemid'};
|
|
$res->{'anum'} = $rs->{'anum'} if defined $rs->{'anum'};
|
|
$res->{'url'} = $rs->{'url'} if defined $rs->{'url'};
|
|
return 1;
|
|
}
|
|
|
|
## flat wrapper
|
|
sub sessiongenerate {
|
|
my ($req, $res, $flags) = @_;
|
|
|
|
my $err = 0;
|
|
my $rq = upgrade_request($req);
|
|
|
|
my $rs = LJ::Protocol::do_request('sessiongenerate', $rq, \$err, $flags);
|
|
unless ($rs) {
|
|
$res->{success} = 'FAIL';
|
|
$res->{errmsg} = LJ::Protocol::error_message($err);
|
|
}
|
|
|
|
$res->{success} = 'OK';
|
|
$res->{ljsession} = $rs->{ljsession};
|
|
return 1;
|
|
}
|
|
|
|
## flat wrappre
|
|
sub sessionexpire {
|
|
my ($req, $res, $flags) = @_;
|
|
|
|
my $err = 0;
|
|
my $rq = upgrade_request($req);
|
|
|
|
$rq->{expire} = [];
|
|
foreach my $k (keys %$rq) {
|
|
push @{$rq->{expire}}, $1
|
|
if $k =~ /^expire_id_(\d+)$/;
|
|
}
|
|
|
|
my $rs = LJ::Protocol::do_request('sessionexpire', $rq, \$err, $flags);
|
|
unless ($rs) {
|
|
$res->{success} = 'FAIL';
|
|
$res->{errmsg} = LJ::Protocol::error_message($err);
|
|
}
|
|
|
|
$res->{success} = 'OK';
|
|
return 1;
|
|
}
|
|
|
|
## flat wrapper
|
|
sub getevents
|
|
{
|
|
my ($req, $res, $flags) = @_;
|
|
|
|
my $err = 0;
|
|
my $rq = upgrade_request($req);
|
|
|
|
my $rs = LJ::Protocol::do_request("getevents", $rq, \$err, $flags);
|
|
unless ($rs) {
|
|
$res->{'success'} = "FAIL";
|
|
$res->{'errmsg'} = LJ::Protocol::error_message($err);
|
|
return 0;
|
|
}
|
|
|
|
my $ect = 0;
|
|
my $pct = 0;
|
|
foreach my $evt (@{$rs->{'events'}}) {
|
|
$ect++;
|
|
foreach my $f (qw(itemid eventtime security allowmask subject anum url poster)) {
|
|
if (defined $evt->{$f}) {
|
|
$res->{"events_${ect}_$f"} = $evt->{$f};
|
|
}
|
|
}
|
|
$res->{"events_${ect}_event"} = LJ::eurl($evt->{'event'});
|
|
|
|
if ($evt->{'props'}) {
|
|
foreach my $k (sort keys %{$evt->{'props'}}) {
|
|
$pct++;
|
|
$res->{"prop_${pct}_itemid"} = $evt->{'itemid'};
|
|
$res->{"prop_${pct}_name"} = $k;
|
|
$res->{"prop_${pct}_value"} = $evt->{'props'}->{$k};
|
|
}
|
|
}
|
|
}
|
|
|
|
unless ($req->{'noprops'}) {
|
|
$res->{'prop_count'} = $pct;
|
|
}
|
|
$res->{'events_count'} = $ect;
|
|
$res->{'success'} = "OK";
|
|
|
|
return 1;
|
|
}
|
|
|
|
|
|
sub populate_friends
|
|
{
|
|
my ($res, $pfx, $list) = @_;
|
|
my $count = 0;
|
|
foreach my $f (@$list)
|
|
{
|
|
$count++;
|
|
$res->{"${pfx}_${count}_name"} = $f->{'fullname'};
|
|
$res->{"${pfx}_${count}_user"} = $f->{'username'};
|
|
$res->{"${pfx}_${count}_birthday"} = $f->{'birthday'} if $f->{'birthday'};
|
|
$res->{"${pfx}_${count}_bg"} = $f->{'bgcolor'};
|
|
$res->{"${pfx}_${count}_fg"} = $f->{'fgcolor'};
|
|
if (defined $f->{'groupmask'}) {
|
|
$res->{"${pfx}_${count}_groupmask"} = $f->{'groupmask'};
|
|
}
|
|
if (defined $f->{'type'}) {
|
|
$res->{"${pfx}_${count}_type"} = $f->{'type'};
|
|
}
|
|
if (defined $f->{'status'}) {
|
|
$res->{"${pfx}_${count}_status"} = $f->{'status'};
|
|
}
|
|
}
|
|
$res->{"${pfx}_count"} = $count;
|
|
}
|
|
|
|
|
|
sub upgrade_request
|
|
{
|
|
my $r = shift;
|
|
my $new = { %{ $r } };
|
|
$new->{'username'} = $r->{'user'};
|
|
|
|
# but don't delete $r->{'user'}, as it might be, say, %FORM,
|
|
# that'll get reused in a later request in, say, update.bml after
|
|
# the login before postevent. whoops.
|
|
|
|
return $new;
|
|
}
|
|
|
|
## given a $res hashref and friend group subtree (arrayref), flattens it
|
|
sub populate_friend_groups
|
|
{
|
|
my ($res, $fr) = @_;
|
|
|
|
my $maxnum = 0;
|
|
foreach my $fg (@$fr)
|
|
{
|
|
my $num = $fg->{'id'};
|
|
$res->{"frgrp_${num}_name"} = $fg->{'name'};
|
|
$res->{"frgrp_${num}_sortorder"} = $fg->{'sortorder'};
|
|
if ($fg->{'public'}) {
|
|
$res->{"frgrp_${num}_public"} = 1;
|
|
}
|
|
if ($num > $maxnum) { $maxnum = $num; }
|
|
}
|
|
$res->{'frgrp_maxnum'} = $maxnum;
|
|
}
|
|
|
|
## given a menu tree, flattens it into $res hashref
|
|
sub populate_web_menu
|
|
{
|
|
my ($res, $menu, $numref) = @_;
|
|
my $mn = $$numref; # menu number
|
|
my $mi = 0; # menu item
|
|
foreach my $it (@$menu) {
|
|
$mi++;
|
|
$res->{"menu_${mn}_${mi}_text"} = $it->{'text'};
|
|
if ($it->{'text'} eq "-") { next; }
|
|
if ($it->{'sub'}) {
|
|
$$numref++;
|
|
$res->{"menu_${mn}_${mi}_sub"} = $$numref;
|
|
&populate_web_menu($res, $it->{'sub'}, $numref);
|
|
next;
|
|
|
|
}
|
|
$res->{"menu_${mn}_${mi}_url"} = $it->{'url'};
|
|
}
|
|
$res->{"menu_${mn}_count"} = $mi;
|
|
}
|
|
|
|
1;
|