1803 lines
52 KiB
Perl
1803 lines
52 KiB
Perl
# This code was forked from the LiveJournal project owned and operated
|
|
# by Live Journal, Inc. The code has been modified and expanded by
|
|
# Dreamwidth Studios, LLC. These files were originally licensed under
|
|
# the terms of the license supplied by Live Journal, Inc, which can
|
|
# currently be found at:
|
|
#
|
|
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
|
|
#
|
|
# In accordance with the original license, this code and all its
|
|
# modifications are provided under the GNU General Public License.
|
|
# A copy of that license can be found in the LICENSE file included as
|
|
# part of this distribution.
|
|
|
|
package LJ::User;
|
|
|
|
use strict;
|
|
use v5.10;
|
|
use Log::Log4perl;
|
|
my $log = Log::Log4perl->get_logger(__PACKAGE__);
|
|
|
|
use Carp qw/ confess /;
|
|
use LJ::DB;
|
|
use LJ::Identity;
|
|
|
|
use DW::Pay;
|
|
use DW::User::OpenID;
|
|
use DW::InviteCodes::Promo;
|
|
|
|
########################################################################
|
|
### 1. Creating and Deleting Accounts
|
|
|
|
=head1 LJ::User Methods
|
|
|
|
=head2 Creating and Deleting Accounts
|
|
=cut
|
|
|
|
sub can_expunge {
|
|
my $u = shift;
|
|
|
|
# must be already deleted
|
|
return 0 unless $u->is_deleted;
|
|
|
|
# and deleted 30 days ago
|
|
my $expunge_days = LJ::conf_test($LJ::DAYS_BEFORE_EXPUNGE) || 30;
|
|
return 0 unless $u->statusvisdate_unix < time() - 86400 * $expunge_days;
|
|
|
|
my $hook_rv = 0;
|
|
if ( LJ::Hooks::are_hooks( "can_expunge_user", $u ) ) {
|
|
$hook_rv = LJ::Hooks::run_hook( "can_expunge_user", $u );
|
|
return $hook_rv ? 1 : 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
# class method to create a new account.
|
|
sub create {
|
|
my ( $class, %opts ) = @_;
|
|
|
|
my $err = sub {
|
|
$log->warn(@_);
|
|
return undef;
|
|
};
|
|
|
|
my $username = LJ::canonical_username( $opts{user} ) or return;
|
|
|
|
my $cluster = $opts{cluster} || LJ::DB::new_account_cluster();
|
|
my $caps = $opts{caps} || $LJ::NEWUSER_CAPS;
|
|
my $journaltype = $opts{journaltype} || "P";
|
|
|
|
# non-clustered accounts aren't supported anymore
|
|
return $err->( 'Invalid cluster: ', $cluster )
|
|
unless $cluster;
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
|
|
$dbh->do(
|
|
'INSERT INTO user (user, name, clusterid, dversion, caps, journaltype) '
|
|
. 'VALUES (?, ?, ?, ?, ?, ?)',
|
|
undef, $username, '', $cluster, $LJ::MAX_DVERSION, $caps, $journaltype
|
|
);
|
|
return $err->( 'Database error: ', $dbh->errstr ) if $dbh->err;
|
|
|
|
my $userid = $dbh->{'mysql_insertid'};
|
|
return $err->('Failed to get userid') unless $userid;
|
|
|
|
$dbh->do( 'INSERT INTO useridmap (userid, user) VALUES (?, ?)', undef, $userid, $username );
|
|
return $err->( 'Database error: ', $dbh->errstr ) if $dbh->err;
|
|
|
|
$dbh->do( 'INSERT INTO userusage (userid, timecreate) VALUES (?, NOW())', undef, $userid );
|
|
return $err->( 'Database error: ', $dbh->errstr ) if $dbh->err;
|
|
|
|
my $u = LJ::load_userid( $userid, 'force' ) or return;
|
|
DW::Stats::increment( 'dw.action.account.create', 1,
|
|
[ 'journal_type:' . $u->journaltype_readable ] );
|
|
|
|
my $status = $opts{status} || 'N';
|
|
my $name = $opts{name} || $username;
|
|
my $bdate = $opts{bdate} || '0000-00-00';
|
|
my $email = $opts{email} || '';
|
|
my $password = $opts{password} || '';
|
|
|
|
$u->update_self(
|
|
{
|
|
status => $status,
|
|
name => $name,
|
|
bdate => $bdate,
|
|
email => $email,
|
|
password => $password,
|
|
%LJ::USER_INIT
|
|
}
|
|
);
|
|
|
|
my $remote = LJ::get_remote();
|
|
$u->log_event( 'account_create', { remote => $remote } );
|
|
|
|
# only s2 is supported
|
|
$u->set_prop( stylesys => 2 );
|
|
|
|
while ( my ( $name, $val ) = each %LJ::USERPROP_INIT ) {
|
|
$u->set_prop( $name, $val );
|
|
}
|
|
|
|
if ( $opts{extra_props} ) {
|
|
while ( my ( $key, $value ) = each( %{ $opts{extra_props} } ) ) {
|
|
$u->set_prop( $key => $value );
|
|
}
|
|
}
|
|
|
|
if ( $opts{status_history} ) {
|
|
my $system = LJ::load_user('system');
|
|
if ($system) {
|
|
while ( my ( $key, $value ) = each( %{ $opts{status_history} } ) ) {
|
|
LJ::statushistory_add( $u, $system, $key, $value );
|
|
}
|
|
}
|
|
}
|
|
|
|
LJ::Hooks::run_hooks(
|
|
'post_create',
|
|
{
|
|
userid => $userid,
|
|
user => $username,
|
|
code => undef,
|
|
news => $opts{get_news},
|
|
}
|
|
);
|
|
|
|
return $u;
|
|
}
|
|
|
|
sub create_community {
|
|
my ( $class, %opts ) = @_;
|
|
|
|
$opts{journaltype} = "C";
|
|
my $u = LJ::User->create(%opts) or return;
|
|
|
|
$u->set_prop( "nonmember_posting", $opts{nonmember_posting} + 0 );
|
|
$u->set_prop( "moderated", $opts{moderated} + 0 );
|
|
$u->set_prop( "adult_content", $opts{journal_adult_settings} )
|
|
if LJ::is_enabled('adult_content');
|
|
$u->set_default_style unless $LJ::_T_CONFIG;
|
|
|
|
my $admin =
|
|
$opts{admin_userid}
|
|
? LJ::load_userid( $opts{admin_userid} )
|
|
: LJ::get_remote();
|
|
|
|
if ($admin) {
|
|
LJ::set_rel( $u, $admin, "A" ); # maintainer
|
|
LJ::set_rel( $u, $admin, "M" ) if $opts{moderated}; # moderator if moderated
|
|
$admin->join_community( $u, 1, 1 ); # member
|
|
|
|
$u->set_comm_settings(
|
|
$admin,
|
|
{
|
|
membership => $opts{membership},
|
|
postlevel => $opts{postlevel}
|
|
}
|
|
) if exists $opts{membership} && exists $opts{postlevel};
|
|
}
|
|
return $u;
|
|
}
|
|
|
|
sub create_personal {
|
|
my ( $class, %opts ) = @_;
|
|
|
|
my $u = LJ::User->create(%opts) or return;
|
|
|
|
$u->set_prop( "init_bdate", $opts{bdate} );
|
|
|
|
# so birthday notifications get sent
|
|
$u->set_next_birthday;
|
|
|
|
# Set the default style
|
|
$u->set_default_style;
|
|
|
|
if ( $opts{inviter} ) {
|
|
|
|
# store inviter, if there was one
|
|
my $inviter = LJ::load_user( $opts{inviter} );
|
|
if ($inviter) {
|
|
LJ::set_rel( $u, $inviter, 'I' );
|
|
LJ::statushistory_add( $u, $inviter, 'create_from_invite', "Created new account." );
|
|
if ( $inviter->is_individual ) {
|
|
LJ::Event::InvitedFriendJoins->new( $inviter, $u )->fire;
|
|
}
|
|
}
|
|
}
|
|
|
|
# if we have initial subscriptions for new accounts, add them.
|
|
foreach my $user (@LJ::INITIAL_SUBSCRIPTIONS) {
|
|
my $userid = LJ::get_userid($user)
|
|
or next;
|
|
$u->add_edge( $userid, watch => {} );
|
|
}
|
|
|
|
# apply any paid time that this account should get
|
|
if ( $opts{code} ) {
|
|
my $code = $opts{code};
|
|
my $itemidref;
|
|
my $promo_code =
|
|
$LJ::USE_ACCT_CODES ? DW::InviteCodes::Promo->load( code => $code ) : undef;
|
|
if ($promo_code) {
|
|
$promo_code->apply_for_user($u);
|
|
}
|
|
elsif ( my $cart = DW::Shop::Cart->get_from_invite( $code, itemidref => \$itemidref ) ) {
|
|
my $item = $cart->get_item($itemidref);
|
|
if ( $item && $item->isa('DW::Shop::Item::Account') ) {
|
|
|
|
# first update the item's target user and the cart
|
|
$item->t_userid( $u->id );
|
|
$cart->save( no_memcache => 1 );
|
|
|
|
# now add paid time to the user
|
|
my $from_u = $item->from_userid ? LJ::load_userid( $item->from_userid ) : undef;
|
|
if ( DW::Pay::add_paid_time( $u, $item->class, $item->months ) ) {
|
|
LJ::statushistory_add( $u, $from_u, 'paid_from_invite',
|
|
sprintf( "Created new '%s' from order #%d.", $item->class, $item->cartid )
|
|
);
|
|
}
|
|
else {
|
|
my $paid_error = DW::Pay::error_text() || $@ || 'unknown error';
|
|
LJ::statushistory_add(
|
|
$u, $from_u,
|
|
'paid_from_invite',
|
|
sprintf(
|
|
"Failed to create new '%s' account from order #%d: %s",
|
|
$item->class, $item->cartid, $paid_error
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# subscribe to default events
|
|
$u->subscribe( event => 'OfficialPost', method => 'Inbox' );
|
|
$u->subscribe( event => 'OfficialPost', method => 'Email' ) if $opts{get_news};
|
|
$u->subscribe( event => 'JournalNewComment', journal => $u, method => 'Inbox' );
|
|
$u->subscribe( event => 'JournalNewComment', journal => $u, method => 'Email' );
|
|
$u->subscribe( event => 'AddedToCircle', journal => $u, method => 'Inbox' );
|
|
$u->subscribe( event => 'AddedToCircle', journal => $u, method => 'Email' );
|
|
|
|
# inbox notifications for PMs are on for everyone automatically
|
|
$u->subscribe( event => 'UserMessageRecvd', journal => $u, method => 'Email' );
|
|
$u->subscribe( event => 'InvitedFriendJoins', journal => $u, method => 'Inbox' );
|
|
$u->subscribe( event => 'InvitedFriendJoins', journal => $u, method => 'Email' );
|
|
$u->subscribe( event => 'CommunityInvite', journal => $u, method => 'Inbox' );
|
|
$u->subscribe( event => 'CommunityInvite', journal => $u, method => 'Email' );
|
|
$u->subscribe( event => 'CommunityJoinRequest', journal => $u, method => 'Inbox' );
|
|
$u->subscribe( event => 'CommunityJoinRequest', journal => $u, method => 'Email' );
|
|
|
|
return $u;
|
|
}
|
|
|
|
sub create_syndicated {
|
|
my ( $class, %opts ) = @_;
|
|
|
|
return unless $opts{feedurl};
|
|
|
|
$opts{caps} = $LJ::SYND_CAPS;
|
|
$opts{cluster} = LJ::DB::new_account_cluster();
|
|
$opts{journaltype} = "Y";
|
|
|
|
my $u = LJ::User->create(%opts) or return;
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
$dbh->do( "INSERT INTO syndicated (userid, synurl, checknext) VALUES (?, ?, NOW())",
|
|
undef, $u->id, $opts{feedurl} );
|
|
die $dbh->errstr if $dbh->err;
|
|
|
|
my $remote = LJ::get_remote();
|
|
LJ::statushistory_add( $u, $remote, "synd_create", "acct: " . $u->user );
|
|
|
|
return $u;
|
|
}
|
|
|
|
sub delete_and_purge_completely {
|
|
my $u = shift;
|
|
|
|
# FIXME: delete from user tables
|
|
# FIXME: delete from global tables
|
|
my $dbh = LJ::get_db_writer();
|
|
|
|
my @tables = qw(user useridmap reluser priv_map infohistory email password);
|
|
foreach my $table (@tables) {
|
|
$dbh->do( "DELETE FROM $table WHERE userid=?", undef, $u->id );
|
|
}
|
|
|
|
$dbh->do( "DELETE FROM wt_edges WHERE from_userid = ? OR to_userid = ?", undef, $u->id,
|
|
$u->id );
|
|
$dbh->do( "DELETE FROM reluser WHERE targetid=?", undef, $u->id );
|
|
$u->delete_email_alias;
|
|
|
|
$dbh->do( "DELETE FROM community WHERE userid=?", undef, $u->id )
|
|
if $u->is_community;
|
|
$dbh->do( "DELETE FROM syndicated WHERE userid=?", undef, $u->id )
|
|
if $u->is_syndicated;
|
|
|
|
return 1;
|
|
}
|
|
|
|
# checks against the file containing our regular expressions to determine if a
|
|
# given username is disallowed
|
|
sub is_protected_username {
|
|
my ( $class, $username ) = @_;
|
|
|
|
# site admins (people with siteadmin:usernames) can override this check and
|
|
# create any account they want
|
|
my $remote = LJ::get_remote();
|
|
return 0 if $remote && $remote->has_priv( siteadmin => 'usernames' );
|
|
|
|
my @res = grep { $_ } split( /\r?\n/, LJ::load_include('reserved-usernames') );
|
|
foreach my $re (@res) {
|
|
return 1 if $username =~ /$re/;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
sub who_invited {
|
|
my $u = shift;
|
|
my $inviterid = LJ::load_rel_user( $u, 'I' );
|
|
|
|
return LJ::load_userid($inviterid);
|
|
}
|
|
|
|
########################################################################
|
|
### 2. Statusvis and Account Types
|
|
|
|
=head2 Statusvis and Account Types
|
|
=cut
|
|
|
|
sub get_previous_statusvis {
|
|
my $u = shift;
|
|
|
|
my $extra = $u->selectcol_arrayref(
|
|
"SELECT extra FROM userlog WHERE userid=? AND action='accountstatus' ORDER BY logtime DESC",
|
|
undef, $u->userid
|
|
);
|
|
my @statusvis;
|
|
foreach my $e (@$extra) {
|
|
my %fields;
|
|
LJ::decode_url_string( $e, \%fields, [] );
|
|
push @statusvis, $fields{old};
|
|
}
|
|
return @statusvis;
|
|
}
|
|
|
|
sub is_approved {
|
|
my $u = $_[0];
|
|
|
|
# if a dev server hasn't configured this, assume they don't want it
|
|
return 1 if $LJ::IS_DEV_SERVER && !exists $LJ::DISABLED{approvenew};
|
|
return 1 unless LJ::is_enabled('approvenew');
|
|
return $u->prop('not_approved') ? 0 : 1;
|
|
}
|
|
|
|
sub is_deleted {
|
|
my $u = shift;
|
|
return $u->statusvis eq 'D';
|
|
}
|
|
|
|
sub is_expunged {
|
|
my $u = shift;
|
|
return $u->statusvis eq 'X' || $u->clusterid == 0;
|
|
}
|
|
|
|
sub is_inactive {
|
|
my $u = shift;
|
|
my $statusvis = $u->statusvis;
|
|
|
|
# true if deleted, expunged or suspended
|
|
return $statusvis eq 'D' || $statusvis eq 'X' || $statusvis eq 'S';
|
|
}
|
|
|
|
sub is_locked {
|
|
my $u = shift;
|
|
return $u->statusvis eq 'L';
|
|
}
|
|
|
|
sub is_memorial {
|
|
my $u = shift;
|
|
return $u->statusvis eq 'M';
|
|
}
|
|
|
|
sub is_readonly {
|
|
my $u = shift;
|
|
return $u->statusvis eq 'O' || $u->get_cap('readonly');
|
|
}
|
|
|
|
sub is_renamed {
|
|
my $u = shift;
|
|
return $u->statusvis eq 'R';
|
|
}
|
|
|
|
sub is_rp_account {
|
|
my $u = shift;
|
|
return $u->prop('opt_rpacct');
|
|
}
|
|
|
|
sub is_suspended {
|
|
my $u = shift;
|
|
return $u->statusvis eq 'S';
|
|
}
|
|
|
|
# returns if this user is considered visible
|
|
sub is_visible {
|
|
my $u = shift;
|
|
return $u->statusvis eq 'V';
|
|
}
|
|
|
|
sub set_deleted {
|
|
my $u = shift;
|
|
my $res = $u->set_statusvis('D');
|
|
|
|
# run any account cancellation hooks
|
|
LJ::Hooks::run_hooks( "account_delete", $u );
|
|
return $res;
|
|
}
|
|
|
|
sub set_expunged {
|
|
my $u = shift;
|
|
return $u->set_statusvis('X');
|
|
}
|
|
|
|
sub set_locked {
|
|
my $u = shift;
|
|
return $u->set_statusvis('L');
|
|
}
|
|
|
|
sub set_memorial {
|
|
my $u = shift;
|
|
return $u->set_statusvis('M');
|
|
}
|
|
|
|
sub set_readonly {
|
|
my $u = shift;
|
|
return $u->set_statusvis('O');
|
|
}
|
|
|
|
sub set_renamed {
|
|
my $u = shift;
|
|
return $u->set_statusvis('R');
|
|
}
|
|
|
|
# set_statusvis only change statusvis parameter, all accompanied actions are done in set_* methods
|
|
sub set_statusvis {
|
|
my ( $u, $statusvis ) = @_;
|
|
|
|
Carp::croak "Invalid statusvis: $statusvis"
|
|
unless $statusvis =~ /^(?:
|
|
V| # visible
|
|
D| # deleted
|
|
X| # expunged
|
|
S| # suspended
|
|
L| # locked
|
|
M| # memorial
|
|
O| # read-only
|
|
R # renamed
|
|
)$/x;
|
|
|
|
# log the change to userlog
|
|
$u->log_event(
|
|
'accountstatus',
|
|
{
|
|
# remote looked up by log_event
|
|
old => $u->statusvis,
|
|
new => $statusvis,
|
|
}
|
|
);
|
|
|
|
# do update
|
|
return $u->update_self(
|
|
{
|
|
statusvis => $statusvis,
|
|
raw => 'statusvisdate=NOW()'
|
|
}
|
|
);
|
|
}
|
|
|
|
sub set_suspended {
|
|
my ( $u, $who, $reason, $errref ) = @_;
|
|
die "Not enough parameters for LJ::User::set_suspended call" unless $who and $reason;
|
|
|
|
my $res = $u->set_statusvis('S');
|
|
unless ($res) {
|
|
$$errref = "DB error while setting statusvis to 'S'" if ref $errref;
|
|
return $res;
|
|
}
|
|
|
|
LJ::statushistory_add( $u, $who, "suspend", $reason );
|
|
|
|
# if not_approved was set, clear it
|
|
$u->set_prop( not_approved => 0 );
|
|
|
|
LJ::Hooks::run_hooks( "account_cancel", $u );
|
|
|
|
return $res; # success
|
|
}
|
|
|
|
# sets a user to visible, but also does all of the stuff necessary when a suspended account is unsuspended
|
|
# this can only be run on a suspended account
|
|
sub set_unsuspended {
|
|
my ( $u, $who, $reason, $errref ) = @_;
|
|
die "Not enough parameters for LJ::User::set_unsuspended call" unless $who and $reason;
|
|
|
|
unless ( $u->is_suspended ) {
|
|
$$errref = "User isn't suspended" if ref $errref;
|
|
return 0;
|
|
}
|
|
|
|
my $res = $u->set_visible;
|
|
unless ($res) {
|
|
$$errref = "DB error while setting statusvis to 'V'" if ref $errref;
|
|
return $res;
|
|
}
|
|
|
|
LJ::statushistory_add( $u, $who, "unsuspend", $reason );
|
|
|
|
# if not_approved was set, clear it
|
|
$u->set_prop( not_approved => 0 );
|
|
|
|
return $res; # success
|
|
}
|
|
|
|
sub set_visible {
|
|
my $u = shift;
|
|
|
|
my $old_statusvis = $u->statusvis;
|
|
my $ret = $u->set_statusvis('V');
|
|
|
|
LJ::Hooks::run_hooks( "account_makevisible", $u, old_statusvis => $old_statusvis );
|
|
|
|
return $ret;
|
|
}
|
|
|
|
sub statusvis {
|
|
my $u = shift;
|
|
return $u->{statusvis};
|
|
}
|
|
|
|
sub statusvisdate {
|
|
my $u = shift;
|
|
return $u->{statusvisdate};
|
|
}
|
|
|
|
sub statusvisdate_unix {
|
|
my $u = shift;
|
|
return LJ::mysqldate_to_time( $u->statusvisdate );
|
|
}
|
|
|
|
########################################################################
|
|
### 3. Working with All Types of Account
|
|
|
|
=head2 Working with All Types of Account
|
|
=cut
|
|
|
|
# this will return a hash of information about this user.
|
|
# this is useful for JavaScript endpoints which need to dump
|
|
# JSON data about users.
|
|
sub info_for_js {
|
|
my $u = shift;
|
|
|
|
my %ret = (
|
|
username => $u->user,
|
|
display_username => $u->display_username,
|
|
display_name => $u->display_name,
|
|
userid => $u->userid,
|
|
url_journal => $u->journal_base,
|
|
url_profile => $u->profile_url,
|
|
url_allpics => $u->allpics_base,
|
|
ljuser_tag => $u->ljuser_display,
|
|
is_comm => $u->is_comm,
|
|
is_person => $u->is_person,
|
|
is_syndicated => $u->is_syndicated,
|
|
is_identity => $u->is_identity,
|
|
);
|
|
|
|
LJ::Hooks::run_hook( "extra_info_for_js", $u, \%ret );
|
|
|
|
my $up = $u->userpic;
|
|
|
|
if ($up) {
|
|
$ret{url_userpic} = $up->url;
|
|
$ret{userpic_w} = $up->width;
|
|
$ret{userpic_h} = $up->height;
|
|
}
|
|
|
|
return %ret;
|
|
}
|
|
|
|
sub is_community {
|
|
return $_[0]->{journaltype} eq "C";
|
|
}
|
|
*is_comm = \&is_community;
|
|
|
|
sub is_identity {
|
|
return $_[0]->{journaltype} eq "I";
|
|
}
|
|
|
|
# return true if the user is either a personal journal or an identity journal
|
|
sub is_individual {
|
|
my $u = shift;
|
|
return $u->is_personal || $u->is_identity ? 1 : 0;
|
|
}
|
|
|
|
sub is_official {
|
|
my $u = shift;
|
|
return $LJ::OFFICIAL_JOURNALS{ $u->username } ? 1 : 0;
|
|
}
|
|
|
|
sub is_paid {
|
|
my $u = shift;
|
|
return 0 if $u->is_identity || $u->is_syndicated;
|
|
return DW::Pay::get_account_type($u) ne 'free' ? 1 : 0;
|
|
}
|
|
|
|
sub is_perm {
|
|
my $u = shift;
|
|
return 0 if $u->is_identity || $u->is_syndicated;
|
|
return DW::Pay::get_account_type($u) eq 'seed' ? 1 : 0;
|
|
}
|
|
|
|
sub is_person {
|
|
return $_[0]->{journaltype} eq "P";
|
|
}
|
|
*is_personal = \&is_person;
|
|
|
|
sub is_redirect {
|
|
return $_[0]->{journaltype} eq "R";
|
|
}
|
|
|
|
sub is_syndicated {
|
|
return $_[0]->{journaltype} eq "Y";
|
|
}
|
|
|
|
sub journal_base {
|
|
return LJ::journal_base(@_);
|
|
}
|
|
|
|
sub journaltype {
|
|
return $_[0]->{journaltype};
|
|
}
|
|
|
|
# return the journal type as a name
|
|
sub journaltype_readable {
|
|
my $u = shift;
|
|
|
|
return {
|
|
R => 'redirect',
|
|
I => 'identity',
|
|
P => 'personal',
|
|
Y => 'syndicated',
|
|
C => 'community',
|
|
}->{ $u->journaltype };
|
|
}
|
|
|
|
sub last_updated {
|
|
|
|
# Given a user object, returns a string detailing when that journal
|
|
# was last updated, or "never" if never updated.
|
|
|
|
my ($u) = @_;
|
|
|
|
return undef unless $u->is_person || $u->is_community;
|
|
|
|
my $lastupdated = substr( LJ::mysql_time( $u->timeupdate ), 0, 10 );
|
|
my $ago_text = LJ::diff_ago_text( $u->timeupdate );
|
|
|
|
if ( $u->timeupdate ) {
|
|
return LJ::Lang::ml( 'lastupdated.ago',
|
|
{ timestamp => $lastupdated, agotext => $ago_text } );
|
|
}
|
|
else {
|
|
return LJ::Lang::ml('lastupdated.never');
|
|
}
|
|
}
|
|
|
|
# returns LJ::User class of a random user, undef if we couldn't get one
|
|
# my $random_u = LJ::User->load_random_user(type);
|
|
# If type is null, assumes a person.
|
|
sub load_random_user {
|
|
my $class = shift;
|
|
my $type = shift || 'P';
|
|
|
|
# get a random database, but make sure to try them all if one is down or not
|
|
# responding or similar
|
|
my $dbcr;
|
|
foreach ( List::Util::shuffle(@LJ::CLUSTERS) ) {
|
|
$dbcr = LJ::get_cluster_reader($_);
|
|
last if $dbcr;
|
|
}
|
|
die "Unable to get database cluster reader handle\n" unless $dbcr;
|
|
|
|
# get a selection of users around a random time
|
|
my $when = time() - int( rand( $LJ::RANDOM_USER_PERIOD * 24 * 60 * 60 ) ); # days -> seconds
|
|
my $uids = $dbcr->selectcol_arrayref(
|
|
qq{
|
|
SELECT userid FROM random_user_set
|
|
WHERE posttime > $when
|
|
AND journaltype = ?
|
|
ORDER BY posttime
|
|
LIMIT 10
|
|
}, undef, $type
|
|
);
|
|
die "Failed to execute query: " . $dbcr->errstr . "\n" if $dbcr->err;
|
|
return undef unless $uids && @$uids;
|
|
|
|
# try the users we got
|
|
foreach my $uid (@$uids) {
|
|
my $u = LJ::load_userid($uid)
|
|
or next;
|
|
|
|
# situational checks to ensure this user is a good one to show
|
|
next unless $u->is_visible; # no suspended/deleted/etc users
|
|
next unless $u->is_approved; # ignore accounts in holding pen
|
|
next if $u->prop('latest_optout'); # they have chosen to be excluded
|
|
|
|
# they've passed the checks, return this user
|
|
return $u;
|
|
}
|
|
|
|
# must have failed
|
|
return undef;
|
|
}
|
|
|
|
# des: Given a user hashref, loads the values of the given named properties
|
|
# into that user hashref.
|
|
# args: u, opts?, propname*
|
|
# des-opts: hashref of opts. set key 'use_master' to use cluster master.
|
|
# des-propname: the name of a property from the [dbtable[userproplist]] table.
|
|
# leave undef to preload all userprops
|
|
sub preload_props {
|
|
my $u = shift;
|
|
return unless LJ::isu($u);
|
|
return if $u->is_expunged;
|
|
|
|
my $opts = ref $_[0] ? shift : {};
|
|
my (@props) = @_;
|
|
|
|
my ( $sql, $sth );
|
|
LJ::load_props("user");
|
|
|
|
## user reference
|
|
my $uid = $u->userid + 0;
|
|
$uid = LJ::get_userid( $u->user ) unless $uid;
|
|
|
|
my $mem = {};
|
|
my $use_master = 0;
|
|
my $used_slave = 0; # set later if we ended up using a slave
|
|
|
|
if (@LJ::MEMCACHE_SERVERS) {
|
|
my @keys;
|
|
foreach (@props) {
|
|
next if exists $u->{$_};
|
|
my $p = LJ::get_prop( "user", $_ );
|
|
die "Invalid userprop $_ passed to preload_props." unless $p;
|
|
push @keys, [ $uid, "uprop:$uid:$p->{'id'}" ];
|
|
}
|
|
$mem = LJ::MemCache::get_multi(@keys) || {};
|
|
$use_master = 1;
|
|
}
|
|
|
|
$use_master = 1 if $opts->{'use_master'};
|
|
|
|
my @needwrite; # [propid, propname] entries we need to save to memcache later
|
|
|
|
my %loadfrom;
|
|
my %multihomed; # ( $propid => 0/1 ) # 0 if we haven't loaded it, 1 if we have
|
|
unless (@props) {
|
|
|
|
# case 1: load all props for a given user.
|
|
# multihomed props are stored on userprop and userproplite2, but since they
|
|
# should always be in sync, it doesn't matter which gets loaded first, the
|
|
# net results should be the same. see doc/server/lj.int.multihomed_userprops.html
|
|
# for more information.
|
|
$loadfrom{'userprop'} = 1;
|
|
$loadfrom{'userproplite'} = 1;
|
|
$loadfrom{'userproplite2'} = 1;
|
|
$loadfrom{'userpropblob'} = 1;
|
|
}
|
|
else {
|
|
# case 2: load only certain things
|
|
foreach (@props) {
|
|
next if exists $u->{$_};
|
|
my $p = LJ::get_prop( "user", $_ );
|
|
die "Invalid userprop $_ passed to preload_props." unless $p;
|
|
if ( defined $mem->{"uprop:$uid:$p->{'id'}"} ) {
|
|
$u->{$_} = $mem->{"uprop:$uid:$p->{'id'}"};
|
|
next;
|
|
}
|
|
push @needwrite, [ $p->{'id'}, $_ ];
|
|
my $source = $p->{'indexed'} ? "userprop" : "userproplite";
|
|
if ( $p->{datatype} eq 'blobchar' ) {
|
|
$source = "userpropblob"; # clustered blob
|
|
}
|
|
elsif ( $p->{'cldversion'} && $u->dversion >= $p->{'cldversion'} ) {
|
|
$source = "userproplite2"; # clustered
|
|
}
|
|
elsif ( $p->{multihomed} ) {
|
|
$multihomed{ $p->{id} } = 0;
|
|
$source = "userproplite2";
|
|
}
|
|
push @{ $loadfrom{$source} }, $p->{'id'};
|
|
}
|
|
}
|
|
|
|
foreach my $table (qw{userproplite userproplite2 userpropblob userprop}) {
|
|
next unless exists $loadfrom{$table};
|
|
my $db;
|
|
if ($use_master) {
|
|
$db =
|
|
( $table =~ m{userprop(lite2|blob)} )
|
|
? LJ::get_cluster_master($u)
|
|
: LJ::get_db_writer();
|
|
}
|
|
unless ($db) {
|
|
$db =
|
|
( $table =~ m{userprop(lite2|blob)} )
|
|
? LJ::get_cluster_reader($u)
|
|
: LJ::get_db_reader();
|
|
$used_slave = 1;
|
|
}
|
|
confess "No database handle available" unless $db;
|
|
|
|
$sql = "SELECT upropid, value FROM $table WHERE userid=$uid";
|
|
if ( ref $loadfrom{$table} ) {
|
|
$sql .= " AND upropid IN (" . join( ",", @{ $loadfrom{$table} } ) . ")";
|
|
}
|
|
die "No db\n" unless $db;
|
|
|
|
$sth = $db->prepare($sql);
|
|
$sth->execute;
|
|
while ( my ( $id, $v ) = $sth->fetchrow_array ) {
|
|
delete $multihomed{$id} if $table eq 'userproplite2';
|
|
$u->{ $LJ::CACHE_PROPID{'user'}->{$id}->{'name'} } = $v;
|
|
}
|
|
|
|
# push back multihomed if necessary
|
|
if ( $table eq 'userproplite2' ) {
|
|
push @{ $loadfrom{userprop} }, $_ foreach keys %multihomed;
|
|
}
|
|
}
|
|
|
|
# see if we failed to get anything above and need to hit the master.
|
|
# this usually happens the first time a multihomed prop is hit. this
|
|
# code will propagate that prop down to the cluster.
|
|
if (%multihomed) {
|
|
|
|
# verify that we got the database handle before we try propagating data
|
|
if ( $u->writer ) {
|
|
my @values;
|
|
foreach my $id ( keys %multihomed ) {
|
|
my $pname = $LJ::CACHE_PROPID{user}{$id}{name};
|
|
if ( defined $u->{$pname} && $u->{$pname} ) {
|
|
push @values, "($uid, $id, " . $u->quote( $u->{$pname} ) . ")";
|
|
}
|
|
else {
|
|
push @values, "($uid, $id, '')";
|
|
}
|
|
}
|
|
$u->do( "REPLACE INTO userproplite2 VALUES " . join ',', @values );
|
|
}
|
|
}
|
|
|
|
# Add defaults to user object.
|
|
|
|
# If this was called with no @props, then the function tried
|
|
# to load all metadata. but we don't know what's missing, so
|
|
# try to apply all defaults.
|
|
unless (@props) { @props = keys %LJ::USERPROP_DEF; }
|
|
|
|
foreach my $prop (@props) {
|
|
next if ( defined $u->{$prop} );
|
|
$u->{$prop} = $LJ::USERPROP_DEF{$prop};
|
|
}
|
|
|
|
unless ($used_slave) {
|
|
my $expire = time() + 3600 * 24;
|
|
foreach my $wr (@needwrite) {
|
|
my ( $id, $name ) = ( $wr->[0], $wr->[1] );
|
|
LJ::MemCache::set( [ $uid, "uprop:$uid:$id" ], $u->{$name} || "", $expire );
|
|
}
|
|
}
|
|
}
|
|
|
|
# class method. returns remote (logged in) user object. or undef if
|
|
# no session is active.
|
|
sub remote {
|
|
my ( $class, $opts ) = @_;
|
|
return LJ::get_remote($opts);
|
|
}
|
|
|
|
# class method. set the remote user ($u or undef) for the duration of this request.
|
|
# once set, it'll never be reloaded, unless "unset_remote" is called to forget it.
|
|
sub set_remote {
|
|
my ( $class, $remote ) = @_;
|
|
$LJ::CACHED_REMOTE = 1;
|
|
$LJ::CACHE_REMOTE = $remote;
|
|
1;
|
|
}
|
|
|
|
# when was this account created?
|
|
# returns unixtime
|
|
sub timecreate {
|
|
my $u = shift;
|
|
|
|
return $u->{_cache_timecreate} if $u->{_cache_timecreate};
|
|
|
|
my $memkey = [ $u->id, "tc:" . $u->id ];
|
|
my $timecreate = LJ::MemCache::get($memkey);
|
|
if ($timecreate) {
|
|
$u->{_cache_timecreate} = $timecreate;
|
|
return $timecreate;
|
|
}
|
|
|
|
my $dbr = LJ::get_db_reader() or die "No db";
|
|
my $when =
|
|
$dbr->selectrow_array( "SELECT timecreate FROM userusage WHERE userid=?", undef, $u->id );
|
|
|
|
$timecreate = LJ::mysqldate_to_time($when);
|
|
$u->{_cache_timecreate} = $timecreate;
|
|
LJ::MemCache::set( $memkey, $timecreate, 60 * 60 * 24 );
|
|
|
|
return $timecreate;
|
|
}
|
|
|
|
# when was last time this account updated?
|
|
# returns unixtime
|
|
sub timeupdate {
|
|
my $u = shift;
|
|
my $timeupdate = LJ::get_timeupdate_multi( $u->id );
|
|
return $timeupdate->{ $u->id };
|
|
}
|
|
|
|
# class method. forgets the cached remote user.
|
|
sub unset_remote {
|
|
my $class = shift;
|
|
$LJ::CACHED_REMOTE = 0;
|
|
$LJ::CACHE_REMOTE = undef;
|
|
1;
|
|
}
|
|
|
|
########################################################################
|
|
### 19. OpenID and Identity Users
|
|
|
|
=head2 OpenID and Identity Users
|
|
=cut
|
|
|
|
# returns a true value if user has a reserved 'ext' name.
|
|
sub external {
|
|
my $u = shift;
|
|
return $u->user =~ /^ext_/;
|
|
}
|
|
|
|
# returns LJ::Identity object
|
|
sub identity {
|
|
my $u = shift;
|
|
return $u->{_identity} if $u->{_identity};
|
|
return undef unless $u->is_identity;
|
|
|
|
my $memkey = [ $u->userid, "ident:" . $u->userid ];
|
|
my $ident = LJ::MemCache::get($memkey);
|
|
if ($ident) {
|
|
my $i = LJ::Identity->new(
|
|
typeid => $ident->[0],
|
|
value => $ident->[1],
|
|
);
|
|
|
|
return $u->{_identity} = $i;
|
|
}
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
$ident = $dbh->selectrow_arrayref(
|
|
"SELECT idtype, identity FROM identitymap " . "WHERE userid=? LIMIT 1",
|
|
undef, $u->userid );
|
|
if ($ident) {
|
|
LJ::MemCache::set( $memkey, $ident );
|
|
my $i = LJ::Identity->new(
|
|
typeid => $ident->[0],
|
|
value => $ident->[1],
|
|
);
|
|
return $i;
|
|
}
|
|
return undef;
|
|
}
|
|
|
|
# class function - load an identity user, but only if they're already known to us
|
|
sub load_existing_identity_user {
|
|
my ( $type, $ident ) = @_;
|
|
|
|
my $dbh = LJ::get_db_reader();
|
|
my $uid;
|
|
|
|
# if given an https URL, also look for existing http account
|
|
# (we should have stripped the protocol before storing these, sigh)
|
|
if ( $ident =~ s/^https:// ) {
|
|
my $secure_ident = "https:$ident";
|
|
$ident = "http:$ident";
|
|
|
|
# do the secure lookup first; if it fails, try the fallback below
|
|
$uid = $dbh->selectrow_array(
|
|
"SELECT userid FROM identitymap WHERE " . "idtype=? AND identity=?",
|
|
undef, $type, $secure_ident );
|
|
}
|
|
|
|
unless ($uid) {
|
|
$uid = $dbh->selectrow_array(
|
|
"SELECT userid FROM identitymap WHERE " . "idtype=? AND identity=?",
|
|
undef, $type, $ident );
|
|
}
|
|
|
|
return $uid ? LJ::load_userid($uid) : undef;
|
|
}
|
|
|
|
# class function - load an identity user, and if we've never seen them before create a user account for them
|
|
sub load_identity_user {
|
|
my ( $type, $ident, $vident ) = @_;
|
|
|
|
my $u = load_existing_identity_user( $type, $ident );
|
|
|
|
# If the user is marked as expunged, move identity mapping aside
|
|
# and continue to create new account.
|
|
# Otherwise return user if it exists.
|
|
if ($u) {
|
|
if ( $u->is_expunged ) {
|
|
return undef unless ( $u->rename_identity );
|
|
}
|
|
else {
|
|
return $u;
|
|
}
|
|
}
|
|
|
|
# increment ext_ counter until we successfully create an LJ
|
|
# account. hard cap it at 10 tries. (arbitrary, but we really
|
|
# shouldn't have *any* failures here, let alone 10 in a row)
|
|
|
|
for ( 1 .. 10 ) {
|
|
my $extuser = 'ext_' . LJ::alloc_global_counter('E');
|
|
|
|
my $name = $extuser;
|
|
if ( $type eq "O" && ref $vident ) {
|
|
$name = $vident->display;
|
|
}
|
|
|
|
$u = LJ::User->create(
|
|
caps => undef,
|
|
user => $extuser,
|
|
name => $ident,
|
|
journaltype => 'I',
|
|
);
|
|
last if $u;
|
|
select undef, undef, undef, .10; # lets not thrash over this
|
|
}
|
|
|
|
return undef unless $u;
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
return undef
|
|
unless $dbh->do( "INSERT INTO identitymap (idtype, identity, userid) VALUES (?,?,?)",
|
|
undef, $type, $ident, $u->id );
|
|
|
|
# set default style
|
|
$u->set_default_style;
|
|
|
|
# record create information
|
|
my $remote = LJ::get_remote();
|
|
$u->log_event( 'account_create', { remote => $remote } );
|
|
|
|
return $u;
|
|
}
|
|
|
|
# class function - refactoring verification of Net::OpenID::Consumer object
|
|
# returns the associated user object, or undef on failure
|
|
sub load_from_consumer {
|
|
my ( $csr, $errorref ) = @_;
|
|
|
|
my $err = sub { $$errorref = LJ::Lang::ml(@_) if defined $errorref && ref $errorref };
|
|
|
|
my $vident = eval { $csr->verified_identity; };
|
|
|
|
unless ($vident) {
|
|
my $msg = $@ ? $@ : $csr->err;
|
|
$err->( '/openid/login.tt.error.notverified', { error => $msg } );
|
|
return;
|
|
}
|
|
|
|
my $url = $vident->url;
|
|
|
|
if ( $url =~ /[\<\>\s]/ ) {
|
|
$err->('/openid/login.tt.error.invalidcharacters');
|
|
return;
|
|
}
|
|
|
|
my $u = load_identity_user( "O", $url, $vident );
|
|
|
|
unless ($u) {
|
|
$err->( '/openid/login.tt.error.notvivified', { url => LJ::ehtml($url) } );
|
|
return;
|
|
}
|
|
|
|
return $u;
|
|
}
|
|
|
|
# journal_base replacement for OpenID accounts. since they don't have
|
|
# a journal, redirect to /read.
|
|
sub openid_journal_base {
|
|
return $_[0]->journal_base . "/read";
|
|
}
|
|
|
|
# returns a URL if account is an OpenID identity. undef otherwise.
|
|
sub openid_identity {
|
|
my $u = shift;
|
|
my $ident = $u->identity;
|
|
return undef unless $ident && $ident->typeid eq 'O';
|
|
return $ident->value;
|
|
}
|
|
|
|
# prepare OpenId part of html-page, if needed
|
|
sub openid_tags {
|
|
my $u = shift;
|
|
|
|
my $head = '';
|
|
|
|
# OpenID Server
|
|
if ( defined $u ) {
|
|
my $journalbase = $u->journal_base;
|
|
$head .= qq{<link rel="openid.server" href="$LJ::OPENID_SERVER" />\n};
|
|
}
|
|
|
|
return $head;
|
|
}
|
|
|
|
# <LJFUNC>
|
|
# name: LJ::User::rename_identity
|
|
# des: Change an identity user's 'identity', update DB,
|
|
# clear memcache and log change.
|
|
# args: user
|
|
# returns: Success or failure.
|
|
# </LJFUNC>
|
|
sub rename_identity {
|
|
my $u = shift;
|
|
return 0 unless ( $u && $u->is_identity && $u->is_expunged );
|
|
|
|
my $id = $u->identity;
|
|
return 0 unless $id;
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
|
|
# generate a new identity value that looks like ex_oldidvalue555
|
|
my $tempid = sub {
|
|
my ( $ident, $idtype ) = @_;
|
|
my $temp = ( length($ident) > 249 ) ? substr( $ident, 0, 249 ) : $ident;
|
|
my $exid;
|
|
|
|
for ( 1 .. 10 ) {
|
|
$exid = "ex_$temp" . int( rand(999) );
|
|
|
|
# check to see if this identity already exists
|
|
unless (
|
|
$dbh->selectrow_array(
|
|
"SELECT COUNT(*) FROM identitymap WHERE identity=? AND idtype=? LIMIT 1",
|
|
undef, $exid, $idtype
|
|
)
|
|
)
|
|
{
|
|
# name doesn't already exist, use this one
|
|
last;
|
|
}
|
|
|
|
# name existed, try and get another
|
|
|
|
if ( $_ >= 10 ) {
|
|
return 0;
|
|
}
|
|
}
|
|
return $exid;
|
|
};
|
|
|
|
my $from = $id->value;
|
|
my $to = $tempid->( $id->value, $id->typeid );
|
|
|
|
return 0 unless $to;
|
|
|
|
$dbh->do( "UPDATE identitymap SET identity=? WHERE identity=? AND idtype=?",
|
|
undef, $to, $from, $id->typeid );
|
|
|
|
LJ::memcache_kill( $u, "userid" );
|
|
|
|
$u->infohistory_add( 'identity', $from );
|
|
|
|
return 1;
|
|
}
|
|
|
|
########################################################################
|
|
### 26. Syndication-Related Functions
|
|
|
|
=head2 Syndication-Related Functions
|
|
=cut
|
|
|
|
# generate tag URI for user's atom id (RFC 4151)
|
|
sub atomid {
|
|
my ($u) = @_;
|
|
my $journalcreated = LJ::mysql_date( $u->timecreate, 1 );
|
|
return "tag:$LJ::DOMAIN,$journalcreated:$u->{userid}";
|
|
}
|
|
|
|
sub atom_service_document {
|
|
return "$LJ::SITEROOT/interface/atom";
|
|
}
|
|
|
|
sub atom_base {
|
|
my ($u) = @_;
|
|
return $u->journal_base . "/interface/atom";
|
|
}
|
|
|
|
# retrieve hash of basic syndicated info
|
|
sub get_syndicated {
|
|
my $u = shift;
|
|
|
|
return unless $u->is_syndicated;
|
|
my $userid = $u->userid;
|
|
my $memkey = [ $userid, "synd:$userid" ];
|
|
|
|
my $synd = {};
|
|
$synd = LJ::MemCache::get($memkey);
|
|
unless ($synd) {
|
|
my $dbr = LJ::get_db_reader();
|
|
return unless $dbr;
|
|
$synd = $dbr->selectrow_hashref("SELECT * FROM syndicated WHERE userid=$userid");
|
|
LJ::MemCache::set( $memkey, $synd, 60 * 30 ) if $synd;
|
|
}
|
|
|
|
return $synd;
|
|
}
|
|
|
|
########################################################################
|
|
### End LJ::User functions
|
|
|
|
########################################################################
|
|
### Begin LJ functions
|
|
|
|
package LJ;
|
|
|
|
use Carp;
|
|
|
|
########################################################################
|
|
### 3. Working with All Types of Accounts
|
|
|
|
=head2 Working with All Types of Accounts (LJ)
|
|
=cut
|
|
|
|
# <LJFUNC>
|
|
# name: LJ::canonical_username
|
|
# des: normalizes username.
|
|
# info:
|
|
# args: user
|
|
# returns: the canonical username given, or blank if the username is not well-formed
|
|
# </LJFUNC>
|
|
sub canonical_username {
|
|
my $input = lc( $_[0] // '' );
|
|
my $user = "";
|
|
if ( $input =~ /^\s*([a-z0-9_\-]{1,$LJ::USERNAME_MAXLENGTH})\s*$/ ) {
|
|
$user = $1;
|
|
$user =~ s/-/_/g;
|
|
}
|
|
return $user;
|
|
}
|
|
|
|
# <LJFUNC>
|
|
# name: LJ::get_userid
|
|
# des: Returns a userid given a username.
|
|
# info: Results cached in memory. On miss, does DB call. Not advised
|
|
# to use this many times in a row... only once or twice perhaps
|
|
# per request. Tons of serialized db requests, even when small,
|
|
# are slow. Opposite of [func[LJ::get_username]].
|
|
# args: dbarg?, user
|
|
# des-user: Username whose userid to look up.
|
|
# returns: Userid, or 0 if invalid user.
|
|
# </LJFUNC>
|
|
sub get_userid {
|
|
my $user = LJ::canonical_username( $_[0] );
|
|
|
|
if ( $LJ::CACHE_USERID{$user} ) { return $LJ::CACHE_USERID{$user}; }
|
|
|
|
my $userid = LJ::MemCache::get("uidof:$user");
|
|
return $LJ::CACHE_USERID{$user} = $userid if $userid;
|
|
|
|
my $dbr = LJ::get_db_reader();
|
|
$userid = $dbr->selectrow_array( "SELECT userid FROM useridmap WHERE user=?", undef, $user );
|
|
|
|
if ($userid) {
|
|
$LJ::CACHE_USERID{$user} = $userid;
|
|
LJ::MemCache::set( "uidof:$user", $userid );
|
|
}
|
|
|
|
return ( defined $userid ? $userid + 0 : 0 );
|
|
}
|
|
|
|
# <LJFUNC>
|
|
# name: LJ::get_username
|
|
# des: Returns a username given a userid.
|
|
# info: Results cached in memory. On miss, does DB call. Not advised
|
|
# to use this many times in a row... only once or twice perhaps
|
|
# per request. Tons of serialized db requests, even when small,
|
|
# are slow. Opposite of [func[LJ::get_userid]].
|
|
# args: dbarg?, user
|
|
# des-user: Username whose userid to look up.
|
|
# returns: Userid, or 0 if invalid user.
|
|
# </LJFUNC>
|
|
sub get_username {
|
|
my $userid = $_[0] + 0;
|
|
|
|
# Checked the cache first.
|
|
if ( $LJ::CACHE_USERNAME{$userid} ) { return $LJ::CACHE_USERNAME{$userid}; }
|
|
|
|
# if we're using memcache, it's faster to just query memcache for
|
|
# an entire $u object and just return the username. otherwise, we'll
|
|
# go ahead and query useridmap
|
|
if (@LJ::MEMCACHE_SERVERS) {
|
|
my $u = LJ::load_userid($userid);
|
|
return undef unless $u;
|
|
|
|
$LJ::CACHE_USERNAME{$userid} = $u->user;
|
|
return $u->user;
|
|
}
|
|
|
|
my $dbr = LJ::get_db_reader();
|
|
my $user = $dbr->selectrow_array( "SELECT user FROM useridmap WHERE userid=?", undef, $userid );
|
|
|
|
# Fall back to master if it doesn't exist.
|
|
unless ( defined $user ) {
|
|
my $dbh = LJ::get_db_writer();
|
|
$user =
|
|
$dbh->selectrow_array( "SELECT user FROM useridmap WHERE userid=?", undef, $userid );
|
|
}
|
|
|
|
return undef unless defined $user;
|
|
|
|
$LJ::CACHE_USERNAME{$userid} = $user;
|
|
return $user;
|
|
}
|
|
|
|
# is a user object (at least a hashref)
|
|
sub isu {
|
|
return unless ref $_[0];
|
|
return 1 if UNIVERSAL::isa( $_[0], "LJ::User" );
|
|
|
|
if ( ref $_[0] eq "HASH" && $_[0]->{userid} ) {
|
|
carp "User HASH objects are deprecated from use." if $LJ::IS_DEV_SERVER;
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
# <LJFUNC>
|
|
# name: LJ::journal_base
|
|
# des: Returns URL of a user's journal.
|
|
# info: The tricky thing is that users with underscores in their usernames
|
|
# can't have some_user.example.com as a hostname, so that's changed into
|
|
# some-user.example.com.
|
|
# args: uuser, vhost?
|
|
# des-uuser: User hashref or username of user whose URL to make.
|
|
# des-vhost: What type of URL. Acceptable options: "users", to make a
|
|
# http://user.example.com/ URL; "tilde" for http://example.com/~user/;
|
|
# "community" for http://example.com/community/user; or the default
|
|
# will be http://example.com/users/user. If unspecified and uuser
|
|
# is a user hashref, then the best/preferred vhost will be chosen.
|
|
# returns: scalar; a URL.
|
|
# </LJFUNC>
|
|
sub journal_base {
|
|
my ( $user, %opts ) = @_;
|
|
my $vhost = $opts{vhost} // "";
|
|
|
|
my $u = LJ::isu($user) ? $user : LJ::load_user($user);
|
|
$user = $u->user if $u;
|
|
|
|
if ( $u && LJ::Hooks::are_hooks("journal_base") ) {
|
|
my $hookurl = LJ::Hooks::run_hook( "journal_base", $u, $vhost );
|
|
return $hookurl if $hookurl;
|
|
|
|
unless ( defined $vhost ) {
|
|
if ( $u->is_person ) {
|
|
$vhost = "";
|
|
}
|
|
elsif ( $u->is_community ) {
|
|
$vhost = "community";
|
|
}
|
|
}
|
|
}
|
|
|
|
my $rule = $u ? $LJ::SUBDOMAIN_RULES->{ $u->journaltype } : undef;
|
|
$rule ||= $LJ::SUBDOMAIN_RULES->{P};
|
|
|
|
# if no rule, then we don't have any idea what to do ...
|
|
die "Site misconfigured, no %LJ::SUBDOMAIN_RULES."
|
|
unless $rule && ref $rule eq 'ARRAY';
|
|
|
|
if ( $rule->[0] && $user !~ /^\_/ && $user !~ /\_$/ ) {
|
|
$user =~ s/_/-/g;
|
|
return "$LJ::PROTOCOL://$user.$LJ::DOMAIN";
|
|
}
|
|
elsif ( !$rule->[1] && $LJ::IS_DEV_SERVER ) {
|
|
|
|
# Dev container: path-based URLs from request host
|
|
my $host = 'localhost';
|
|
my $r = eval { DW::Request->get };
|
|
$host = $r->host if $r;
|
|
return "$LJ::PROTOCOL://$host/~$user";
|
|
}
|
|
else {
|
|
return "$LJ::PROTOCOL://$rule->[1]/$user";
|
|
}
|
|
}
|
|
|
|
# <LJFUNC>
|
|
# name: LJ::load_user
|
|
# des: Loads a user record, from the [dbtable[user]] table, given a username.
|
|
# args: dbarg?, user, force?
|
|
# des-user: Username of user to load.
|
|
# des-force: if set to true, won't return cached user object and will
|
|
# query a dbh.
|
|
# returns: LJ::User object, whose keys are columns of [dbtable[user]] table.
|
|
# </LJFUNC>
|
|
sub load_user {
|
|
my ( $user, $force ) = @_;
|
|
|
|
$user = LJ::canonical_username($user);
|
|
return undef unless length $user;
|
|
|
|
my $get_user = sub {
|
|
my $use_dbh = shift;
|
|
my $db = $use_dbh ? LJ::get_db_writer() : LJ::get_db_reader();
|
|
my $u = _load_user_raw( $db, "user", $user )
|
|
or return undef;
|
|
|
|
# set caches since we got a u from the master
|
|
LJ::memcache_set_u($u) if $use_dbh;
|
|
|
|
return _set_u_req_cache($u);
|
|
};
|
|
|
|
# caller is forcing a master, return now
|
|
return $get_user->("master") if $force || $LJ::_PRAGMA_FORCE_MASTER;
|
|
|
|
my $u;
|
|
|
|
# return process cache if we have one
|
|
if ( $u = $LJ::REQ_CACHE_USER_NAME{$user} ) {
|
|
$u->selfassert;
|
|
return $u;
|
|
}
|
|
|
|
# check memcache
|
|
{
|
|
my $uid = LJ::MemCache::get("uidof:$user");
|
|
$u = LJ::memcache_get_u( [ $uid, "userid:$uid" ] ) if $uid;
|
|
return _set_u_req_cache($u) if $u;
|
|
}
|
|
|
|
# try to load from master if using memcache, otherwise from slave
|
|
$u = $get_user->( scalar @LJ::MEMCACHE_SERVERS );
|
|
return $u if $u;
|
|
|
|
return undef;
|
|
}
|
|
|
|
sub load_user_or_identity {
|
|
my $arg = shift;
|
|
|
|
my $user = LJ::canonical_username($arg);
|
|
return LJ::load_user($user) if $user;
|
|
|
|
# return undef if not dot in arg (can't be a URL)
|
|
return undef unless $arg =~ /\./;
|
|
|
|
my $url = lc($arg);
|
|
$url = "http://$url" unless $url =~ m!^https?://!;
|
|
$url .= "/" unless $url =~ m!/$!;
|
|
|
|
# get from memcache
|
|
{
|
|
# overload the uidof memcache key to accept both display name and name
|
|
my $uid = LJ::MemCache::get("uidof:$url");
|
|
my $u = $uid ? LJ::memcache_get_u( [ $uid, "userid:$uid" ] ) : undef;
|
|
return _set_u_req_cache($u) if $u;
|
|
}
|
|
|
|
my $u = LJ::User::load_existing_identity_user( 'O', $url );
|
|
|
|
# set user in memcache
|
|
if ($u) {
|
|
|
|
# memcache URL-to-userid for identity users
|
|
LJ::MemCache::set( "uidof:$url", $u->id, 1800 );
|
|
LJ::memcache_set_u($u);
|
|
return _set_u_req_cache($u);
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
|
|
# <LJFUNC>
|
|
# name: LJ::load_userid
|
|
# des: Loads a user record, from the [dbtable[user]] table, given a userid.
|
|
# args: dbarg?, userid, force?
|
|
# des-userid: Userid of user to load.
|
|
# des-force: if set to true, won't return cached user object and will
|
|
# query a dbh
|
|
# returns: LJ::User object.
|
|
# </LJFUNC>
|
|
sub load_userid {
|
|
my ( $userid, $force ) = @_;
|
|
return undef unless $userid;
|
|
|
|
my $get_user = sub {
|
|
my $use_dbh = shift;
|
|
my $db = $use_dbh ? LJ::get_db_writer() : LJ::get_db_reader();
|
|
my $u = _load_user_raw( $db, "userid", $userid )
|
|
or return undef;
|
|
|
|
LJ::memcache_set_u($u) if $use_dbh;
|
|
return _set_u_req_cache($u);
|
|
};
|
|
|
|
# user is forcing master, return now
|
|
return $get_user->("master") if $force || $LJ::_PRAGMA_FORCE_MASTER;
|
|
|
|
my $u;
|
|
|
|
# check process cache
|
|
$u = $LJ::REQ_CACHE_USER_ID{$userid};
|
|
if ($u) {
|
|
$u->selfassert;
|
|
return $u;
|
|
}
|
|
|
|
# check memcache
|
|
$u = LJ::memcache_get_u( [ $userid, "userid:$userid" ] );
|
|
return _set_u_req_cache($u) if $u;
|
|
|
|
# get from master if using memcache
|
|
return $get_user->("master") if @LJ::MEMCACHE_SERVERS;
|
|
|
|
# check slave
|
|
$u = $get_user->();
|
|
return $u if $u;
|
|
|
|
# if we didn't get a u from the reader, fall back to master
|
|
return $get_user->("master");
|
|
}
|
|
|
|
# <LJFUNC>
|
|
# name: LJ::load_userids
|
|
# des: Simple interface to [func[LJ::load_userids_multiple]].
|
|
# args: userids
|
|
# returns: hashref with keys ids, values $u refs.
|
|
# </LJFUNC>
|
|
sub load_userids {
|
|
my %u;
|
|
LJ::load_userids_multiple( [ map { $_ => \$u{$_} } grep { defined $_ } @_ ] );
|
|
return \%u;
|
|
}
|
|
|
|
# <LJFUNC>
|
|
# name: LJ::load_userids_multiple
|
|
# des: Loads a number of users at once, efficiently.
|
|
# info: loads a few users at once, their userids given in the keys of $map
|
|
# listref (not hashref: can't have dups). values of $map listref are
|
|
# scalar refs to put result in. $have is an optional listref of user
|
|
# object caller already has, but is too lazy to sort by themselves.
|
|
# <strong>Note</strong>: The $have parameter is deprecated,
|
|
# as is $memcache_only; but it is still preserved for now.
|
|
# Really, this whole API (i.e. LJ::load_userids_multiple) is clumsy.
|
|
# Use [func[LJ::load_userids]] instead.
|
|
# args: dbarg?, map, have, memcache_only?
|
|
# des-map: Arrayref of pairs (userid, destination scalarref).
|
|
# des-have: Arrayref of user objects caller already has.
|
|
# des-memcache_only: Flag to only retrieve data from memcache.
|
|
# returns: Nothing.
|
|
# </LJFUNC>
|
|
sub load_userids_multiple {
|
|
|
|
# the $have parameter is deprecated, as is $memcache_only, but it's still preserved for now.
|
|
# actually this whole API is crap. use LJ::load_userids() instead.
|
|
my ( $map, undef, $memcache_only ) = @_;
|
|
|
|
my $sth;
|
|
my @have;
|
|
my %need;
|
|
while (@$map) {
|
|
my $id = shift @$map;
|
|
my $ref = shift @$map;
|
|
next unless defined $id;
|
|
next unless int($id);
|
|
push @{ $need{$id} }, $ref;
|
|
|
|
if ( $LJ::REQ_CACHE_USER_ID{$id} ) {
|
|
push @have, $LJ::REQ_CACHE_USER_ID{$id};
|
|
}
|
|
}
|
|
|
|
my $satisfy = sub {
|
|
my $u = shift;
|
|
next unless ref $u eq "LJ::User";
|
|
|
|
# this could change the $u returned to an
|
|
# existing one we already have loaded in memory,
|
|
# once it's been upgraded. then everybody points
|
|
# to the same one.
|
|
$u = _set_u_req_cache($u);
|
|
|
|
foreach ( @{ $need{ $u->userid } } ) {
|
|
|
|
# check if existing target is defined and not what we already have.
|
|
if ( my $eu = $$_ ) {
|
|
LJ::assert_is( $u->userid, $eu->userid );
|
|
}
|
|
$$_ = $u;
|
|
}
|
|
|
|
delete $need{ $u->userid };
|
|
};
|
|
|
|
unless ($LJ::_PRAGMA_FORCE_MASTER) {
|
|
foreach my $u (@have) {
|
|
$satisfy->($u);
|
|
}
|
|
|
|
if (%need) {
|
|
foreach ( LJ::memcache_get_u( map { [ $_, "userid:$_" ] } keys %need ) ) {
|
|
$satisfy->($_);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( %need && !$memcache_only ) {
|
|
my $db = @LJ::MEMCACHE_SERVERS
|
|
|| $LJ::_PRAGMA_FORCE_MASTER ? LJ::get_db_writer() : LJ::get_db_reader();
|
|
|
|
_load_user_raw(
|
|
$db, "userid",
|
|
[ keys %need ],
|
|
sub {
|
|
my $u = shift;
|
|
LJ::memcache_set_u($u);
|
|
$satisfy->($u);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
# <LJFUNC>
|
|
# name: LJ::make_user_active
|
|
# des: Record user activity per cluster, on [dbtable[clustertrack2]], to
|
|
# make per-activity cluster stats easier.
|
|
# args: userid, type
|
|
# des-userid: source userobj ref
|
|
# des-type: currently unused
|
|
# </LJFUNC>
|
|
sub mark_user_active {
|
|
my ( $u, $type ) = @_; # not currently using type
|
|
return 0 unless $u; # do not auto-vivify $u
|
|
my $uid = $u->userid;
|
|
return 0 unless $uid && $u->clusterid;
|
|
|
|
# FIXME: return 1 instead? Callers don't use the return value, so I'm not
|
|
# sure whether 0 means "some error happened" or just "nothing done"
|
|
return 0 unless $u->is_personal || $u->is_community || $u->is_identity;
|
|
|
|
# Update the clustertrack2 table, but not if we've done it for this
|
|
# user in the last hour. if no memcache servers are configured
|
|
# we don't do the optimization and just always log the activity info
|
|
if ( @LJ::MEMCACHE_SERVERS == 0
|
|
|| LJ::MemCache::add( "rate:tracked:$uid", 1, 3600 ) )
|
|
{
|
|
|
|
return 0 unless $u->writer;
|
|
my $active = time();
|
|
$u->do(
|
|
qq{ REPLACE INTO clustertrack2
|
|
SET userid=?, timeactive=?, clusterid=?,
|
|
accountlevel=?, journaltype=? },
|
|
undef, $uid, $active, $u->clusterid,
|
|
DW::Pay::get_current_account_status($uid), $u->journaltype
|
|
) or return 0;
|
|
|
|
my $memkey = [ $u->userid, "timeactive:" . $u->userid ];
|
|
LJ::MemCache::set( $memkey, $active, 86400 );
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
# <LJFUNC>
|
|
# name: LJ::want_user
|
|
# des: Returns user object when passed either userid or user object. Useful to functions that
|
|
# want to accept either.
|
|
# args: user
|
|
# des-user: Either a userid or a user hash with the userid in its 'userid' key.
|
|
# returns: The user object represented by said userid or username.
|
|
# </LJFUNC>
|
|
sub want_user {
|
|
my $uuid = shift;
|
|
return undef unless $uuid;
|
|
return $uuid if ref $uuid;
|
|
return LJ::load_userid($uuid) if $uuid =~ /^\d+$/;
|
|
Carp::croak("Bogus caller of LJ::want_user with non-ref/non-numeric parameter");
|
|
}
|
|
|
|
# <LJFUNC>
|
|
# name: LJ::want_userid
|
|
# des: Returns userid when passed either userid or the user hash. Useful to functions that
|
|
# want to accept either. Forces its return value to be a number (for safety).
|
|
# args: userid
|
|
# des-userid: Either a userid, or a user hash with the userid in its 'userid' key.
|
|
# returns: The userid, guaranteed to be a numeric value.
|
|
# </LJFUNC>
|
|
sub want_userid {
|
|
my $uuserid = shift;
|
|
return ( $uuserid->{userid} + 0 ) if ref $uuserid;
|
|
return ( $uuserid ? $uuserid + 0 : 0 );
|
|
}
|
|
|
|
########################################################################
|
|
### 19. OpenID and Identity Functions
|
|
|
|
=head2 OpenID and Identity Functions (LJ)
|
|
=cut
|
|
|
|
# given a LJ userid/u, return a hashref of:
|
|
# type, extuser, extuserid
|
|
# returns undef if user isn't an externally mapped account.
|
|
sub get_extuser_map {
|
|
my $uid = LJ::want_userid(shift);
|
|
return undef unless $uid;
|
|
|
|
my $dbr = LJ::get_db_reader();
|
|
return undef unless $dbr;
|
|
|
|
my $sql = "SELECT * FROM extuser WHERE userid=?";
|
|
my $ret = $dbr->selectrow_hashref( $sql, undef, $uid );
|
|
return undef unless $ret;
|
|
|
|
my $type = 'unknown';
|
|
foreach ( keys %LJ::EXTERNAL_NAMESPACE ) {
|
|
$type = $_ if $LJ::EXTERNAL_NAMESPACE{$_}->{id} == $ret->{siteid};
|
|
}
|
|
|
|
$ret->{type} = $type;
|
|
return $ret;
|
|
}
|
|
|
|
# given an extuserid or extuser, return the LJ uid.
|
|
# return undef if there is no mapping.
|
|
sub get_extuser_uid {
|
|
my ( $type, $opts, $force ) = @_;
|
|
return undef unless $type && $LJ::EXTERNAL_NAMESPACE{$type}->{id};
|
|
return undef
|
|
unless ref $opts
|
|
&& ( $opts->{extuser} || defined $opts->{extuserid} );
|
|
|
|
my $dbh = $force ? LJ::get_db_writer() : LJ::get_db_reader();
|
|
return undef unless $dbh;
|
|
|
|
my $sql = "SELECT userid FROM extuser WHERE siteid=?";
|
|
my @bind = ( $LJ::EXTERNAL_NAMESPACE{$type}->{id} );
|
|
|
|
if ( $opts->{extuser} ) {
|
|
$sql .= " AND extuser=?";
|
|
push @bind, $opts->{extuser};
|
|
}
|
|
|
|
if ( $opts->{extuserid} ) {
|
|
$sql .= $opts->{extuser} ? ' OR ' : ' AND ';
|
|
$sql .= "extuserid=?";
|
|
push @bind, $opts->{extuserid} + 0;
|
|
}
|
|
|
|
return $dbh->selectrow_array( $sql, undef, @bind );
|
|
}
|
|
|
|
1;
|