# 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{\n}; } return $head; } # # 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. # 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 # # name: LJ::canonical_username # des: normalizes username. # info: # args: user # returns: the canonical username given, or blank if the username is not well-formed # 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; } # # 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. # 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 ); } # # 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. # 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; } } # # 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. # 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"; } } # # 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. # 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; } # # 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. # 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"); } # # name: LJ::load_userids # des: Simple interface to [func[LJ::load_userids_multiple]]. # args: userids # returns: hashref with keys ids, values $u refs. # sub load_userids { my %u; LJ::load_userids_multiple( [ map { $_ => \$u{$_} } grep { defined $_ } @_ ] ); return \%u; } # # 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. # Note: 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. # 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); } ); } } # # 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 # 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; } # # 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. # 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"); } # # 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. # 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;