#!/usr/bin/perl
#
# LiveJournal.com-specific library
#
# This file is NOT licensed under the GPL. As with everything in the
# "ljcom" CVS repository, this file is the property of Danga
# Interactive and is made available to the public only as a reference
# as to the best way to modify/extend the base LiveJournal server code
# (which is licensed under the GPL).
#
# Feel free to read and learn from things in "ljcom", but don't use it verbatim
# because we don't want your site looking like LiveJournal.com (our logo
# and site scheme are our identity and we don't want to confuse users)
# and we're sick of getting everybody's payment notifications when
# they use our payment system without any modifications.
#
BEGIN {
# kill the LJ definition of LJ::is_utf8 so we can override it without warnings
{
no strict;
local $^W = 0;
*stab = *{"main::LJ::"};
undef $stab{is_utf8};
}
}
@LJ::USER_TABLES_LOCAL = ("phonepostentry", "phoneposttrans");
$LJ::BML_DENY_CONFIG = "guide, clients, files";
$LJ::ACCOUNTS_EMAIL = "accounts\@livejournal.com";
%LJ::FIXED_ALIAS = (
'lj' => 'lj', # discarded
'lj_notify' => 'lj', # also discarded
'test' => 'brad@danga.com',
'postmaster' => 'lisa@grrl.org',
'webmaster' => 'lj',
'support' => 'lj',
'abuse' => 'lj',
'privacy' => 'lj',
'feedback' => 'lj',
'press' => 'lj',
'bradfitz' => 'brad@danga.com',
'paypal' => 'brad@danga.com',
'accounts' => 'lj',
'frank' => 'brad@danga.com',
'lj_coreadmins' => 'brad@danga.com, lisa@grrl.org, nbarkas@moduli.net',
'cvs-commits' => 'brad@danga.com, whitaker@danga.com, jproulx@livejournal.com, '.
'mellon@pobox.com, martine@danga.com, mahlon@danga.com, ged@danga.com',
'moodthemes' => 'evan@livejournal.com', # aliases to aliases!
'bot-watchers' => 'brad@danga.com',
);
@LJ::LANGS = qw(en_LJ en_GB de da es fr it ru ja pt eo he nl hu ga is fi nb sv pl zh lv tr ms)
unless @LJ::LANGS > 1;
# Useful untainting regexen
%LJ::REGEX = (
httpuri => qr{(http://(?:(?:(?:(?:(?:[a-zA-Z\d](?:(?:[a-zA-Z\d]|-)*[a-zA-Z\d])?)\.)*(?:[a-zA-Z](?:(?:[a-zA-Z\d]|-)*[a-zA-Z\d])?))|(?:(?:\d+)(?:\.(?:\d+)){3}))(?::(?:\d+))?)(?:/(?:(?:(?:(?:[a-zA-Z\d\$\_.+!*'(),-]|(?:%[a-fA-F\d]{2}))|[;:@&=])*)(?:/(?:(?:(?:[a-zA-Z\d\$\_.+!*'(),-]|(?:%[a-fA-F\d]{2}))|[;:@&=])*))*)(?:\?(?:(?:(?:[a-zA-Z\d\$\_.+!*'(),-]|(?:%[a-fA-F\d]{2}))|[;:@&=])*))?)?)}x,
);
package LJ::Contrib;
# is the given user an acked contributor themselves?
sub is_acked
{
my ($userid) = @_;
my $dbr = LJ::get_db_reader();
return undef unless $dbr and $userid;
return $dbr->selectrow_array("SELECT COUNT(*) FROM contributed WHERE userid=? AND acks > 0",
undef, $userid);
}
# make $coid acked by $userid
sub ack
{
my ($coid, $userid) = @_;
my $dbh = LJ::get_db_writer();
return undef unless $dbh and $userid and $coid;
# see if contribution exists
my $co = $dbh->selectrow_hashref("SELECT * FROM contributed WHERE coid=?",
undef, $coid);
return 0 unless $co;
## Lock the Tables
$dbh->do("LOCK TABLES contributedack WRITE, contributed WRITE");
## add the ack
$dbh->do("REPLACE INTO contributedack (coid, ackuserid) VALUES (?,?)",
undef, $coid, $userid);
## see how many acks it has now.
my $newcount = $dbh->selectrow_array("SELECT COUNT(*) FROM contributedack WHERE coid=?",
undef, $coid);
$newcount += 0;
## update the contributed table
$dbh->do("UPDATE contributed SET acks=? WHERE coid=?", undef,
$newcount, $coid);
## Unlock tables
$dbh->do("UNLOCK TABLES");
return 1;
}
package LJ::LJcom;
use Inline (C => 'DATA',
DIRECTORY => $ENV{LJ_INLINE_DIR} || "$ENV{'LJHOME'}/Inline",
);
use strict;
{
eval {
Inline->init();
};
if ($@) {
die "You seem to have Inline.pm, but you haven't run \$LJHOME/bin/lj-inline.pl\n";
}
}
sub country_of_ip {
my $ip = shift;
return undef unless $LJ::OPTMOD_GEOIP;
my $gi = $LJ::CACHE_GEOIP_HANDLE ||= Geo::IP::PurePerl->open("$LJ::HOME/cgi-bin/GeoIP.dat");
return $gi->country_code_by_addr($ip);
}
# old-name (off, on, paid, early (& new))
sub acct_name_short {
my $caps = shift;
if ($caps & 0x10) {
return "on";
} elsif ($caps & 0x08) {
return "paid";
} elsif ($caps & 0x04) {
return "early";
} elsif ($caps & 0x02) {
return "off";
} elsif ($caps & 0x01) {
return "new";
}
return "??";
}
sub acct_name {
my $caps = shift;
my $paiduntil = shift;
my $v;
if ($paiduntil)
{
$v = $caps & 0x08 && $caps & 0x04 ?
BML::ml('ljcom.userinfo.types.paid_early_expiring', { 'paiduntil' => $paiduntil }) :
BML::ml('ljcom.userinfo.types.paid_expiring', { 'paiduntil' => $paiduntil });
} else {
$v = $caps & 0x10 && $caps & 0x04 ?
$BML::ML{'ljcom.userinfo.types.permanent_early'} :
$caps & 0x10 ? $BML::ML{'ljcom.userinfo.types.permanent'} :
$caps & 0x08 && $caps & 0x04 ?
$BML::ML{'ljcom.userinfo.types.paid_early'} :
$caps & 0x08 ? $BML::ML{'ljcom.userinfo.types.paid'} :
$caps & 0x04 ? $BML::ML{'ljcom.userinfo.types.early'} :
$caps & 0x02 ? $BML::ML{'ljcom.userinfo.types.free'} :
$caps & 0x01 ? $BML::ML{'ljcom.userinfo.types.trial'} :
undef;
}
return $v;
}
sub is_goatvote_poll {
my ($po, $qs) = @_;
# to be a goatvote poll:
# the name must have "GoatVote:" prepended
# the poster must have siteadmin:goatvote
# after that, we don't care. but if a poll is in this format, we can process
# the data as if it's a goatvote poll.
# load the questions
return 0 unless $po->{name} =~ /^GoatVote:/i;
# now check user permissions
my $u = LJ::load_userid($po->{posterid});
return 0 unless LJ::check_priv($u, 'siteadmin', 'goatvote');
# now make sure the format is right
@$qs = sort { $a->{pollqid} <=> $b->{pollqid} } @$qs;
# check one two...
return 0 unless scalar @$qs >= 2;
return 0 unless $qs->[0]{type} eq 'radio';
return 0 unless $qs->[1]{type} eq 'text';
# okay, it is!
return 1;
}
sub expresslane_html_comment {
my ($u, $r) = @_;
return '' unless $r && $u && LJ::get_cap($u, 'paid');
my ($free_ct, $free_age) = ($r->header_in('X-Queue-Count')+0, $r->header_in('X-Queue-Age')+0);
return "\n";
};
LJ::register_setter("latest_optout", sub {
&LJ::nodb;
my ($u, $remote, $key, $value, $err) = @_;
unless ($value =~ /^(?:yes|no)$/i) {
$$err = "Illegal value. Must be 'yes' or 'no'.";
return 0;
}
$value = lc $value eq 'yes' ? 1 : 0;
LJ::set_userprop($u, "latest_optout", $value);
return 1;
});
LJ::register_setter("no_mail_alias", sub {
&LJ::nodb;
my ($u, $remote, $key, $value, $err) = @_;
my $dbh = LJ::get_db_writer();
unless ($value =~ /^[01]$/) {
$$err = "Illegal value. Must be '0' or '1'.";
return 0;
}
if ($value) {
$dbh->do("DELETE FROM email_aliases WHERE alias=?", undef,
"$u->{'user'}\@$LJ::USER_DOMAIN");
} elsif ($u->{'status'} eq "A" && LJ::get_cap($u, "useremail")) {
$dbh->do("REPLACE INTO email_aliases (alias, rcpt) VALUES (?,?)",
undef, "$u->{'user'}\@$LJ::USER_DOMAIN", $u->{'email'});
}
LJ::set_userprop($u, "no_mail_alias", $value);
return 1;
});
LJ::clear_hooks();
LJ::register_hook("name_caps", \&acct_name);
LJ::register_hook("name_caps_short", \&acct_name_short);
LJ::register_hook('s2_head_content_extra', \&expresslane_html_comment);
# if a user gets marked underage, we need to clear out their personally
# identifying information
LJ::register_hook('set_underage', sub {
my $opts = shift;
return unless $opts->{on}; # only care if turned on
# update records in the user table
my $u = $opts->{u};
LJ::update_user($u, {
name => $u->{user},
bdate => undef,
allow_infoshow => 'N',
allow_contactshow => 'N',
has_bio => 'N',
txtmsg_status => 'off',
status => 'T',
});
# the only thing we have left on
return if $u->{statusvis} eq 'X';
# now empty their bio information
$u->do("DELETE FROM userbio WHERE userid=?", undef, $u->{'userid'});
$u->dudata_set('B', 0, 0);
# clear a ton of userprops
my @toclear = qw(
country state city zip icq aolim yahoo msn
url urlname gender jabber journaltitle journalsubtitle
friendspagetitle external_foaf_url
);
foreach my $prop (@toclear) {
LJ::set_userprop($u, $prop, undef);
}
});
# hook to handle creating a button to email someone about spam
LJ::register_hook('spamreport_notification', sub {
my ($remote, $opts) = @_;
# they can send in either 'ip => foo' or 'posterid => foo' but we
# only care about posterid for now
my $posterid;
return unless $posterid = $opts->{posterid};
my $poster = LJ::want_user($posterid);
# verify we got the remote user and a poster
$remote = LJ::want_user($remote);
return undef unless $remote && $poster;
if ($poster->openid_identity) {
return "
WARNING: The account you are viewing (" .
LJ::ljuser($poster) . ") is an OpenID identity and has no email address.
";
}
# step 1) find related users by email
my $dbr = LJ::get_db_reader();
return ""
unless $dbr;
my $users = $dbr->selectall_hashref('SELECT * FROM user WHERE email = ?', 'userid', undef, $poster->{email});
return ""
unless $users && ref $users eq 'HASH' && %$users;
# now see if any of these have been warned
my $in = join(',', map { ref $_ ? ($_->{userid} + 0) : 0 } values %$users);
my $warnings = $dbr->selectall_arrayref("SELECT adminid, shdate, userid FROM statushistory " .
"WHERE userid IN ($in) AND shtype = 'spam_warning'");
return ""
if $dbr->err || !defined $warnings;
# now construct html
my ($ret, %emailcounts, %emails);
foreach my $warning (@$warnings) {
my $date = $warning->[1];
if ($date =~ /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/) {
$date = "$1-$2-$3 $4:$5:$6";
}
my $admin = LJ::load_userid($warning->[0]);
my $warned = LJ::load_userid($warning->[2]);
# come up with notes about this warning
my $notes = 'none';
if (LJ::u_equals($poster, $warned)) {
$notes = 'user match';
} elsif (lc $poster->{email} eq lc $warned->{email}) {
$notes = 'email match';
if ($warned->{status} ne 'A') {
$notes .= ' (unvalidated email)';
}
}
# query if we haven't queried based on this email address before
if (!$emailcounts{$warned->{email}}) {
my $rows = $dbr->selectall_arrayref("SELECT mailid, userid, timesent, subject FROM abuse_mail " .
"WHERE type='abuse' AND mailto = ?", undef, $warned->{email});
$emailcounts{$warned->{email}} = 1;
foreach my $row (@{$rows || []}) {
my ($mailid, $userid, $timesent, $subject) = @$row;
my $u = LJ::load_userid($userid);
$emails{$timesent} = "
";
}
# get message to put into body for sending
my $message = LJ::load_include('spam-warning');
$message =~ s/\[\[user\]\]/$poster->{user}/ig;
# now construct the parts of the email
$ret .= " ";
return $ret;
});
### Fetch the value for the given I from the specified I,
### untaint it with the given I, and return the results. If the
### I has match-groups in it, the values matched with them will be the
### returned values. Otherwise, the entire input will be returned.
sub untaint {
my ( $arghash, $field, $pattern ) = @_;
return '' unless exists $arghash->{$field} && defined $arghash->{$field};
my $input = $arghash->{$field};
$pattern = qr{$pattern}i unless ref $pattern eq 'Regexp';
my @matches = ( $input =~ $pattern ) or return '';
return @matches if $1;
return $input;
}
# hook to do transforms for posting pictures from FB
LJ::register_hook('transform_update_postpics', sub {
my ($GET, $POST) = @_;
my (
@ids,
$picsize,
$columns,
$caporient,
$border,
@pics,
@rows,
$row,
$nextrow,
$pic,
$imgtag,
$imgcell,
$capcell,
@html,
);
# Untaint and split the picture ids to post
@ids = split /:/, $1 if exists $POST->{ids} && $POST->{ids} =~ m{^([\d:]+)};
return unless $POST->{'wizard-picsize'} =~ m{^([tsf])$}i;
$picsize = $1;
$columns = $1 if exists $POST->{'wizard-columns'}
&& $POST->{'wizard-columns'} =~ m{^([1-4])$};
$caporient = $1 if exists $POST->{'wizard-caporient'}
&& $POST->{'wizard-caporient'} =~ m{^([arbl0])$};
$border = 1 if $POST->{'wizard-border'};
# Default/bound some values if not defined or valid -- Large picsize
if ( $picsize eq 'f' ) {
$columns = 1;
}
# Medium picsize
elsif ( $picsize eq 's' ) {
$columns = 1 if $columns < 1;
$columns = 2 if $columns > 2;
}
# Thumbnail picsize
else {
$columns = 1 if $columns < 1;
$columns = 4 if $columns > 4;
}
# Make sure the caption orientation will work with the number of columns
# defined.
if ( $columns == 1 || $columns == 3 ) {
$caporient = 'b' unless $caporient eq '0' || $caporient eq 'a';
}
# Build the array of ids to flow into the chosen layout
@pics = map {{
id => $_,
captitle => untaint( $POST, "subj$_", qr{([\t\r\n\x20-\xff]*)} ),
capdesc => untaint( $POST, "desc$_", qr{([\t\r\n\x20-\xff]*)} ),
img => untaint( $POST, "${picsize}img$_", $LJ::REGEX{httpuri} ),
imgwidth => untaint( $POST, "${picsize}w$_", qr{(\d+)} ),
imgheight => untaint( $POST, "${picsize}h$_", qr{(\d+)} ),
url => untaint( $POST, "url$_", $LJ::REGEX{httpuri} ),
}} @ids;
# Build a table for the pics and their captions
while ( @pics ) {
$row = [];
$nextrow = [];
# Fill up each row with the specified number of columns
while ( @$row < $columns ) {
$pic = shift @pics;
# If there's a picture to add, do so
if ( $pic ) {
$imgtag = sprintf q{},
@{$pic}{qw[img captitle imgheight imgwidth]};
$imgcell = sprintf q{%s},
$pic->{url}, $imgtag;
$capcell = sprintf qq{%s \n\t\t%s},
@{$pic}{qw[captitle capdesc]};
}
# Otherwise it'll be a blank cell
else {
$imgcell = '';
$capcell = '';
}
## Now arrange the caption relative to the pic if there is a caption
# Above
if ( $caporient eq 'a' ) {
push @$row, $capcell;
push @$nextrow, $imgcell;
}
# Below
elsif ( $caporient eq 'b' ) {
push @$row, $imgcell;
push @$nextrow, $capcell;
}
# Leftish captions, then the image, then rightish captions
else {
push @$row, $capcell if $caporient eq 'l';
push @$row, $imgcell;
push @$row, $capcell if $caporient eq 'r';
}
}
push @rows, $row;
push @rows, $nextrow if @$nextrow;
}
# Mangle the rows into an indented HTML table
@html = ();
push @html, (
" ",
"
"
);
foreach my $row ( @rows ) {
push @html, (
"
",
(map { "
$_
" } @$row),
"
",
);
}
push @html, "
\n \n\n";
# Stick the results into the posted event
$POST->{event} = join "\n", @html;
return;
});
# hook to hit akamai to remove a userpic
LJ::register_hook('expunge_userpic', sub {
my ($picid, $userid) = @_;
$picid += 0;
$userid += 0;
return undef unless $picid && $userid;
# now hit akamai
my $res = SOAP::Lite
->service($LJ::AKAMAI{service})
->purgeRequest($LJ::AKAMAI{username}, $LJ::AKAMAI{password}, $LJ::AKAMAI{network},
[''], ["$LJ::USERPIC_ROOT/$picid/$userid"]);
# see if there was an error
my ($code, $msg) = ($res->{resultCode}, $res->{resultMsg});
if ($code == 300) {
return [ 'info', "Akamai cache purged successfully." ];
} else {
return [ 'error', "Error $code: $msg (CACHE NOT PURGED)" ];
}
});
# cluster definition hook
LJ::register_hook('cluster_description', sub {
my $clusterid = $_[0]+0;
my ($ob, $cb) = $_[1] ? ('', '') : ('', '');
my ($cid, $scid) = ($clusterid, undef);
($cid, $scid) = ($1, $2) if $clusterid =~ /^(\d)(\d+)$/;
my $text = $ob . ($LJ::CLUSTERNAME{$cid} || $cid) . $cb;
$text .= ", subcluster $ob$scid$cb" if defined $scid;
return $text;
});
# hook to override show_poll for GoatVote polls
LJ::register_hook('alternate_show_poll_html', sub {
my ($po, $mode, $qs) = @_;
# if mode is enter, we don't handle
return undef if $mode eq 'enter';
# if it's not a goatvote...
return undef unless LJ::LJcom::is_goatvote_poll($po, $qs);
# return a link to the goatvote page
LJ::Poll::clean_poll(\$po->{name}) if $po->{name};
my $ret = "{pollid}\">Poll \#$po->{pollid}:" .
"$po->{name} Open to: $po->{whovote}, results viewable " .
"to: $po->{whoview} This is a GoatVote poll. Current results are " .
"{pollid}\">available elsewhere. You " .
"can also {pollid}&mode=enter\">fill out the poll.";
return $ret;
});
# extra viewing line HTML
LJ::register_hook('extra_poll_description', sub {
my ($po, $qs) = @_;
# if it's a goatvote
return '' unless LJ::LJcom::is_goatvote_poll($po, $qs);
# okay return our string
return "You may view the {pollid}\">" .
'GoatVote results in progress.';
});
# hook to show goatvote in poll pregenerator
LJ::register_hook('poll_pregeneration_html', sub {
my ($u, $is_authas) = @_;
return unless LJ::check_priv($u, 'siteadmin', 'goatvote');
# okay, show them the options
my $getextra = $is_authas ? "authas=$u->{'user'}&" : '';
my $body = "";
$body .= "
";
$body .= "";
return $body;
});
# actually pregenerate a poll
LJ::register_hook('pregenerate_poll', sub {
my ($u, $pgid) = @_;
return undef unless $pgid && LJ::check_priv($u, 'siteadmin', 'goatvote'); # only one option for now
# throw a goatvoate together
if ($pgid == 1) {
return {
count => 3,
name => 'GoatVote: ',
whovote => 'all',
whoview => 'all',
pq => [
{
type => 'radio',
question => 'What point of view do you most agree with?',
opts => 5,
opt => [ '', '', '', '', '' ],
}, {
type => 'text',
question => 'What URL supports this point of view best?',
size => 30,
maxlength => 255,
}, {
type => 'text',
question => 'What other URL supports this point of view?',
size => 30,
maxlength => 255,
}
],
};
}
# caller expects a hashref
return {};
});
LJ::register_hook("get_cap_bit", sub {
my $name = shift;
return undef unless $name;
return $LJ::Pay::capinf{$name}->{'bit'};
});
# Register hooks for turning on/off certain cap bits
# This needs to be expanded to activate/deactivate bonus
# features, etc, but right now we just be sure to run
# LJ::activate_userpics()
LJ::register_hook("modify_caps", sub {
my $arg = shift;
# 1-4 = free-perm, 9 = extra userpics
foreach (1..4, 9) {
next unless $arg->{'cap_on_mod'}->{$_} || $arg->{'cap_off_mod'}->{$_};
# only run once, with newest caps...
$arg->{'u'}->{'caps'} = $arg->{'newcaps'};
return LJ::activate_userpics($arg->{'u'});
}
return 1;
});
# check for a user diskquota cap. returning undef defers the check to normal means
LJ::register_hook("check_cap_disk_quota", sub {
my $u = shift;
return undef unless $u;
my $dbr = LJ::get_db_reader();
my $size = $dbr->selectrow_array("SELECT size FROM paidexp WHERE userid=? AND item='diskquota'",
undef, $u->{'userid'});
return $size || undef;
});
LJ::register_hook("ssl_check", sub {
my $r = $_[0]{r};
return
$r->header_in("X-LJ-SSL") ||
($LJ::IS_DEV_SERVER && $r->header_in("Host") eq "secure.$LJ::DOMAIN");
});
LJ::register_hook("post_create", \&LJ::Pay::post_create);
LJ::register_hook("create.bml_opts", sub {
my $ar = shift;
my $ret = $ar->{ret};
my $get = $ar->{get};
my $post = $ar->{post};
$$ret .= "
$BML::ML{'ljcom.accounttype'}
";
$$ret .= "
";
my $valid_code = 0;
my $code = $get->{code} || $post->{code};
if ($code) {
my $dbr = LJ::get_db_reader();
my ($acid, $auth) = LJ::acct_code_decode($code);
if (my $piid = $dbr->selectrow_array("SELECT piid FROM acctpayitem WHERE acid=?",
undef, $acid))
{
my ($item, $qty) = $dbr->selectrow_array("SELECT item, qty FROM payitems ".
"WHERE piid=?", undef, $piid);
if ($item eq 'perm') {
$$ret .= "Permanent Account";
} else {
$$ret .= "Paid for " . ($qty+0) . " Months";
}
$$ret .= ", from code: $code";
$valid_code++;
}
}
unless ($valid_code) {
my @atypes = ([ 0, $BML::ML{'ljcom.account.free'} ],
[ 2, $BML::ML{'ljcom.account.paid2'} ],
[ 6, $BML::ML{'ljcom.account.paid6'} ],
[ 12, $BML::ML{'ljcom.account.paid12'} ]);
my $cur_type = $post->{'ljcom_atype'}+0;
foreach my $at (@atypes) {
$$ret .= LJ::html_check({ name => 'ljcom_atype', id => "ljcom_atype_$at->[0]",
value => $at->[0],
type => 'radio', selected => ($cur_type == $at->[0]) });
$$ret .= " \n";
}
}
$$ret .= "
";
# if they've already got a code for paid time, no reason to show the feature list
return if $valid_code;
$$ret .= '
";
}
});
LJ::register_hook("recent_action_flags", sub {
# these flags live in their own site-local namespace
# and must be prepended with '_' to avoid collisions
return { phonepost => '_F', # 'F'onepost, meh
phonepost_mp3 => '_M' }->{$_[0]}; # 'M'p3
});
LJ::register_hook("postpost", sub {
my $arg = shift;
my $uo = $arg->{'journal'};
return if $uo->{'journaltype'} eq "Y"; # no syndicated
my $up = $arg->{'poster'};
# if the poster has opted out, don't record the post
LJ::load_user_props($up, "latest_optout");
return if $up->{latest_optout};
# setup security
my $security = $arg->{'security'};
$security = $arg->{'allowmask'} == 1 ? 'friends' : 'custom'
if ($security eq 'usemask');
# see if it has a public image in it. heuristic: it's an http://
# URL in an image tag, or it's alone and has a popular image extension
my $img;
if ($security eq "public" &&
($arg->{'event'} =~ m!{'event'} =~ m!\'\"]!i ||
$arg->{'event'} =~ m!(http://\S+\.(?:gif|jpe?g|png)\b)!i)) {
$img = $2 || $1;
# make sure image is good and hasn't been used in last 4 hours
unless (length($img) < 100 && $img !~ /[\n\r]/ &&
LJ::MemCache::add("ljcom_imgused:$img", 1, 3600*4)) {
undef $img;
}
}
LJ::cmd_buffer_add($uo->{clusterid}, $uo->{'userid'}, "ljcom_newpost", {
'timepost' => time(),
'journalid' => $uo->{'userid'},
'posterid' => $up->{'userid'},
'itemid' => $arg->{'itemid'},
'anum' => $arg->{'anum'},
'security' => $security,
'img' => $img,
'taglist' => $arg->{'props'}->{'taglist'},
});
});
# TEMP: Log unknown8bit posts to decide if they can be disabled later
# see also table definition in update-db-local.pl
LJ::register_hook("postpost", sub {
my $entry = shift;
return unless $LJ::DEBUG{'survey_8bit'} && $entry->{'props'}->{'unknown8bit'};
my $dbh = LJ::get_db_writer();
$dbh->do("REPLACE INTO survey_v0_8bit (userid, timepost) VALUES (?, UNIX_TIMESTAMP())",
undef, $entry->{'poster'}->{'userid'});
});
LJ::register_hook("cmdbuf:ljcom_newpost:start", sub {
my ($dbh) = @_;
$LJ::CACHE_RECENTPOSTS_CHANGES = 0;
$LJ::CACHE_RECENTPOSTS ||= LJ::MemCache::get("blob:ljcom_latestposts2") || [];
$LJ::CACHE_RECENTIMG_CHANGES = 0;
$LJ::CACHE_RECENTIMG ||= LJ::MemCache::get("blob:ljcom_latestimg") || [];
});
LJ::register_hook("cmdbuf:ljcom_newpost:too_old", sub { 60*60*2 });
LJ::register_hook("cmdbuf:ljcom_newpost:run", sub {
my ($dbh, $db, $c) = @_;
my $args = $c->{'args'};
my $recent = $LJ::CACHE_RECENTPOSTS;
my $uj = LJ::load_userid($args->{'journalid'});
my $up = LJ::load_userid($args->{'posterid'});
return unless $uj->{'statusvis'} eq "V" && $up->{'statusvis'} eq "V";
LJ::load_user_props($uj, "journaltitle");
my $rp = {};
$rp->{$_} = $args->{$_}+0 foreach qw(timepost itemid anum);
$rp->{security} = $args->{security};
$rp->{clusterid} = $up->{clusterid}+0;
$rp->{tags} = [ split(/\s*,\s*/, $args->{taglist}) ];
$rp->{journalu} = {
user => $uj->{user},
userid => $uj->{userid}+0,
journaltype => $uj->{journaltype},
},
$rp->{journalp} = {
user => $up->{user},
userid => $up->{userid}+0,
name => $up->{name},
};
push @$recent, $rp;
$LJ::CACHE_RECENTPOSTS_CHANGES++;
if ($args->{'img'}) {
my $rimg = $LJ::CACHE_RECENTIMG;
push @$rimg, [ $args->{'img'}, $rp->{journalu}, $args->{'itemid'}, $args->{'anum'} ];
$LJ::CACHE_RECENTIMG_CHANGES++;
}
});
LJ::register_hook("cmdbuf:ljcom_newpost:finish", sub {
my ($dbh) = @_;
return unless $LJ::CACHE_RECENTPOSTS_CHANGES;
my $recent = $LJ::CACHE_RECENTPOSTS;
my $rimg = $LJ::CACHE_RECENTIMG;
my $show_max = $LJ::STATS_LATESTPOSTS_MAX || 1000;
@$recent = sort { $b->{'timepost'} <=> $a->{'timepost'} } @$recent;
splice(@$recent, $show_max) if @$recent > $show_max;
LJ::MemCache::set("blob:ljcom_latestposts2", $recent);
if ($LJ::CACHE_RECENTIMG_CHANGES) {
my $size = @$rimg;
splice(@$rimg, 0, $size - $show_max) if $size > $show_max;
LJ::MemCache::set("blob:ljcom_latestimg", $rimg);
}
LJ::MemCache::set("blob:ljcom_latestposts_stats",
[ scalar(@$recent),
$recent->[0]->{timepost},
$recent->[-1]->{timepost} ]);
});
# returns 1 if too fast, 0 if okay
LJ::register_hook("ccpay_rate_check", sub {
my ($tries, $lasttry) = @_;
# TRIES : LIMIT
# - 1.. 3: 0 sec
# - 4.. 6: 5 sec
# - 7..10: 15 sec
# - 11..19: 60 sec
# - 20....: 30 min
my $now = time();
return 0 if $tries <= 3;
return 0 if $tries <= 6 && $lasttry < $now - 5;
return 0 if $tries <= 10 && $lasttry < $now - 15;
return 0 if $tries <= 19 && $lasttry < $now - 60;
return 0 if $lasttry < $now - 1800;
return 1;
});
LJ::register_hook("cmdbuf:pay_fb_xmlrpc:run", sub {
my ($dbh, $db, $c) = @_;
return undef unless $LJ::FB_SITEROOT && $LJ::FB_QUOTA_NOTIFY;
my $args = $c->{args};
my $userid = $c->{journalid};
# args: item, size, exp
my $u = LJ::load_userid($userid);
return undef unless $u;
eval "use XMLRPC::Lite (); 1;"
or return undef;
return XMLRPC::Lite
->new( proxy => "$LJ::FB_SITEROOT/interface/xmlrpc",
timeout => 5 )
->call('FB.XMLRPC.set_quota', # xml-rpc method call
{ user => $u->{user},
item => $args->{item},
size => $args->{size},
exptime => $args->{exptime},
});
});
# given an S2 context, give back a BML langid.
LJ::register_hook("set_s2bml_lang", sub {
my ($ctx, $langref) = @_;
my $lang = S2::get_property_value($ctx, 'lang_current');
$lang = 'en' unless grep(/$lang/, @LJ::LANGS);
$lang = 'en_LJ' if ($lang eq 'en');
$$langref = $lang;
});
# what tables bin/moveucluster.pl should move that aren't general code
LJ::register_hook("moveucluster_local_tables", sub {
return {
'phonepostentry' => 'userid',
'phoneposttrans' => 'journalid',
};
});
# remove transcription group userprop
LJ::register_hook("delete_friend_group", sub {
my ($u, $bit) = @_;
LJ::load_user_props($u, 'pp_transallow');
LJ::set_userprop($u, 'pp_transallow', -1) if $bit == $u->{pp_transallow};
});
LJ::register_hook("userinfo_join_community", sub {
my $o = shift;
my $r = $o->{'ret'};
my $u = $o->{'u'};
$$r .= BML::ml('/userinfo.bml.membership.paidmembers') if $u->{'user'} eq "paidmembers";
return;
});
LJ::register_hook("forbid_request", sub {
my $r = shift;
my $ua = $r->header_in("User-Agent");
my $ip = $r->connection->remote_ip;
# @BAN_UA can be either scalar substrings of user-agents, or
# an arrayref of [ $substr, $ip ] which makes the substring
# match conditional on it matching that IP
foreach (@LJ::BAN_UA) {
if (ref) {
return 1 if $_->[1] eq $ip && index($ua, $_->[0]) != -1;
} else {
return 1 if index($ua, $_) != -1;
}
}
return 0;
});
LJ::register_hook("bot_director", sub {
my ($pre, $post) = @_;
return "$pre If you are running a bot please visit this policy page outlining rules you must respect. $LJ::SITEROOT/bots/ $post"
});
# control panel nag box
LJ::register_hook('control_panel_extra_info', sub {
my ($u, $ret) = @_;
$$ret .= "
";
if (LJ::get_cap($u, "paid")) {
$$ret .= "For a complete summary of the LiveJournal.com services to which you are currently subscribed, visit the ";
$$ret .= "Paid Account Status page.";
# render account summary
$$ret .= LJ::Pay::account_summary($u);
} else {
$$ret .= "Nearly all of LiveJournal's functionality is available free of charge. However, if you're happy ";
$$ret .= "with the service you're being provided, we encourage you to show your support and get a ";
$$ret .= "paid account.";
}
$$ret .= "
";
});
# control panel extra column
LJ::register_hook('control_panel_column', sub {
my ($u, $ret) = @_;
my $authas = ref $u ? "?authas=$u->{user}" : "";
$$ret .= BML::fill_template('block', {
'HEADER' => "Paid Account Information",
'ABOUT' => "Additional information and options for paid accounts.",
'LIST' => "
\n";
return $ret;
});
# args: { userid , ppid }
# appends enclosure or "" to rss $ret string
LJ::register_hook('pp_rss_enclosure', sub {
my $opts = shift;
return LJ::PhonePost::make_link( undef, $opts->{userid},
( $opts->{ppid} >> 8 ), 'rss' );
});
# args: name
# return: formatted name for local config
LJ::register_hook('identity_display_name', sub {
my $name = shift;
$name =~ s/\[(live|dead)journal\.com\]/\[${1}journal\]/;
$name =~ s/^(.+)\.(live|dead)journal\.com$/${1} \[${2}journal\]/;
return $name;
});
LJ::register_hook('offsite_journal_search', sub {
my $u = shift;
my $ret;
my $user = LJ::ehtml($u->{'user'});
$ret .= qq{
Note: This search is provided by an independent company, LJSeek, and is provided solely as a convenience. We are not responsible for the resulting content or search results.
p?>
Search Term:
exact phrase
standout?>
};
return $ret;
});
__DATA__
__C__
/*
* Copyright 2001 Unicode, Inc.
*
* Disclaimer
*
* This source code is provided as is by Unicode, Inc. No claims are
* made as to fitness for any particular purpose. No warranties of any
* kind are expressed or implied. The recipient agrees to determine
* applicability of information provided. If this file has been
* purchased on magnetic or optical media from Unicode, Inc., the
* sole remedy for any claim will be exchange of defective media
* within 90 days of receipt.
*
* Limitations on Rights to Redistribute This Code
*
* Unicode, Inc. hereby grants the right to freely use the information
* supplied in this file in the creation of products supporting the
* Unicode Standard, and to make copies of this file in any form
* for internal or external distribution as long as this notice
* remains attached.
*/
typedef unsigned char UTF8;
typedef unsigned char Boolean;
#define false 0
#define true 1
static const char trailingBytesForUTF8[256] = {
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,4,4,4,4,5,5,5,5
};
static Boolean isLegalUTF8(UTF8 *source, int length) {
UTF8 a;
UTF8 *srcptr = source+length;
switch (length) {
default: return false;
/* Everything else falls through when "true"... */
case 4: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return false;
case 3: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return false;
case 2: if ((a = (*--srcptr)) > 0xBF) return false;
switch (*source) {
/* no fall-through in this inner switch */
case 0xE0: if (a < 0xA0) return false; break;
case 0xF0: if (a < 0x90) return false; break;
case 0xF4: if (a > 0x8F) return false; break;
default: if (a < 0x80) return false;
}
case 1: if (*source >= 0x80 && *source < 0xC2) return false;
if (*source > 0xF4) return false;
}
return true;
}
/********************* End code from Unicode, Inc. ***************/
/*
* Author: Brad Fitzpatrick
*
*/
Boolean isLegalUTF8String(char *str, int len)
{
UTF8 *cp = str;
int i;
while (*cp) {
/* how many bytes follow this character? */
int length = trailingBytesForUTF8[*cp]+1;
/* check for early termination of string: */
for (i=1; i